commit c46c7ddbf0ee0c0fe93db95c86df65642440d558 Author: naomi Date: Fri Dec 10 12:03:04 2021 +0000 initial commit diff --git a/assets/css/_animation.scss b/assets/css/_animation.scss new file mode 100644 index 0000000..406e994 --- /dev/null +++ b/assets/css/_animation.scss @@ -0,0 +1,9 @@ +/** + * _animation.scss + * Custom WooCommerce Animations. + */ +@keyframes spin { + 100% { + transform: rotate( 360deg ); + } +} diff --git a/assets/css/_fonts.scss b/assets/css/_fonts.scss new file mode 100644 index 0000000..bf71d1c --- /dev/null +++ b/assets/css/_fonts.scss @@ -0,0 +1,25 @@ +/** + * _fonts.scss + * Custom WooCommerce fonts. + */ +@font-face { + font-family: 'star'; + src: url('../fonts/star.eot'); + src: url('../fonts/star.eot?#iefix') format('embedded-opentype'), + url('../fonts/star.woff') format('woff'), + url('../fonts/star.ttf') format('truetype'), + url('../fonts/star.svg#star') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'WooCommerce'; + src: url('../fonts/WooCommerce.eot'); + src: url('../fonts/WooCommerce.eot?#iefix') format('embedded-opentype'), + url('../fonts/WooCommerce.woff') format('woff'), + url('../fonts/WooCommerce.ttf') format('truetype'), + url('../fonts/WooCommerce.svg#WooCommerce') format('svg'); + font-weight: normal; + font-style: normal; +} \ No newline at end of file diff --git a/assets/css/_mixins.scss b/assets/css/_mixins.scss new file mode 100644 index 0000000..85a1eee --- /dev/null +++ b/assets/css/_mixins.scss @@ -0,0 +1,288 @@ +/** + * Deprecated + * Fallback for bourbon equivalent + */ +@mixin clearfix() { + *zoom: 1; + + &::before, + &::after { + content: " "; + display: table; + } + + &::after { + clear: both; + } +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius($radius: 4px) { + border-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius_right($radius: 4px) { + border-top-right-radius: $radius; + border-bottom-right-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius_left($radius: 4px) { + border-top-left-radius: $radius; + border-bottom-left-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius_bottom($radius: 4px) { + border-bottom-left-radius: $radius; + border-bottom-right-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin border_radius_top($radius: 4px) { + border-top-left-radius: $radius; + border-top-right-radius: $radius; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin opacity( $opacity: 0.75 ) { + opacity: $opacity; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin box_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_in: 3px, $shadow_color: #888) { + box-shadow: $shadow_x $shadow_y $shadow_rad $shadow_in $shadow_color; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin inset_box_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_in: 3px, $shadow_color: #888) { + box-shadow: inset $shadow_x $shadow_y $shadow_rad $shadow_in $shadow_color; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin text_shadow($shadow_x: 3px, $shadow_y: 3px, $shadow_rad: 3px, $shadow_color: #fff) { + text-shadow: $shadow_x $shadow_y $shadow_rad $shadow_color; +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin vertical_gradient($from: #000, $to: #fff) { + background-color: $from; + background: -webkit-linear-gradient($from, $to); +} + +/** + * Deprecated + * Vendor prefix no longer required. + */ +@mixin transition($selector: all, $animation: ease-in-out, $duration: 0.2s) { + transition: $selector $animation $duration; +} + +/** + * Deprecated + * Use bourbon mixin instead `@include transform(scale(1.5));` + */ +@mixin scale($ratio: 1.5) { + -webkit-transform: scale($ratio); + transform: scale($ratio); +} + +/** + * Deprecated + * Use bourbon mixin instead `@include box-sizing(border-box);` + */ +@mixin borderbox() { + box-sizing: border-box; +} + +@mixin darkorlighttextshadow($a, $opacity: 0.8) { + + @if lightness($a) >= 65% { + + @include text_shadow(0, -1px, 0, rgba(0, 0, 0, $opacity)); + } + + @else { + + @include text_shadow(0, 1px, 0, rgba(255, 255, 255, $opacity)); + } +} + +/** + * Objects + */ +@mixin menu() { + + @include clearfix(); + + li { + display: inline-block; + } +} + +@mixin mediaright() { + + @include clearfix(); + + img { + float: right; + height: auto; + } +} + +@mixin medialeft() { + + @include clearfix(); + + img { + float: right; + height: auto; + } +} + +@mixin ir() { + display: block; + text-indent: -9999px; + position: relative; + height: 1em; + width: 1em; +} + +@mixin icon( $glyph: "\e001" ) { + font-family: "WooCommerce"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + margin: 0; + text-indent: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + content: $glyph; +} + +@mixin icon_dashicons( $glyph: "\f333" ) { + font-family: "Dashicons"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + margin: 0; + text-indent: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + content: $glyph; +} + +@mixin iconbefore( $glyph: "\e001" ) { + font-family: "WooCommerce"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + margin-right: 0.618em; + content: $glyph; + text-decoration: none; +} + +@mixin iconbeforedashicons( $glyph: "\f333" ) { + font-family: "Dashicons"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + content: $glyph; + text-decoration: none; +} + +@mixin iconafter( $glyph: "\e001" ) { + font-family: "WooCommerce"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + margin-left: 0.618em; + content: $glyph; + text-decoration: none; +} + +@mixin loader() { + + &::before { + height: 1em; + width: 1em; + display: block; + position: absolute; + top: 50%; + left: 50%; + margin-left: -0.5em; + margin-top: -0.5em; + content: ""; + animation: spin 1s ease-in-out infinite; + background: url("../images/icons/loader.svg") center center; + background-size: cover; + line-height: 1; + text-align: center; + font-size: 2em; + color: rgba(#000, 0.75); + } +} + +@mixin inversebuttoncolors { + background-color: transparent !important; + color: var(--button--color-text-hover) !important; + + &:hover { + background-color: var(--button--color-background) !important; + color: var(--button--color-text) !important; + text-decoration: none !important; + } +} diff --git a/assets/css/_variables.scss b/assets/css/_variables.scss new file mode 100644 index 0000000..54ab622 --- /dev/null +++ b/assets/css/_variables.scss @@ -0,0 +1,38 @@ +/** + * WooCommerce CSS Variables + */ + +$woocommerce: #a46497 !default; +$green: #7ad03a !default; +$red: #a00 !default; +$orange: #ffba00 !default; +$blue: #2ea2cc !default; + +$primary: #a46497 !default; // Primary color for buttons (alt) +$primarytext: desaturate(lighten($primary, 50%), 18%) !default; // Text on primary color bg + +$secondary: desaturate(lighten($primary, 40%), 21%) !default; // Secondary buttons +$secondarytext: desaturate(darken($secondary, 60%), 21%) !default; // Text on secondary color bg + +$highlight: adjust-hue($primary, 150deg) !default; // Prices, In stock labels, sales flash +$highlightext: desaturate(lighten($highlight, 50%), 18%) !default; // Text on highlight color bg + +$contentbg: #fff !default; // Content BG - Tabs (active state) +$subtext: #767676 !default; // small, breadcrumbs etc + +// export vars as CSS vars +:root { + --woocommerce: #{$woocommerce}; + --wc-green: #{$green}; + --wc-red: #{$red}; + --wc-orange: #{$orange}; + --wc-blue: #{$blue}; + --wc-primary: #{$primary}; + --wc-primary-text: #{$primarytext}; + --wc-secondary: #{$secondary}; + --wc-secondary-text: #{$secondarytext}; + --wc-highlight: #{$highlight}; + --wc-highligh-text: #{$highlightext}; + --wc-content-bg: #{$contentbg}; + --wc-subtext: #{$subtext}; +} diff --git a/assets/css/activation-rtl.css b/assets/css/activation-rtl.css new file mode 100644 index 0000000..f8f53ff --- /dev/null +++ b/assets/css/activation-rtl.css @@ -0,0 +1 @@ +div.woocommerce-message{overflow:hidden;position:relative}div.woocommerce-message.updated{border-right-color:#cc99c2!important}.woocommerce-message .button-primary,p.woocommerce-actions .button-primary{background:#bb77ae;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597;color:#fff;text-shadow:0 -1px 1px #a36597,-1px 0 1px #a36597,0 1px 1px #a36597,1px 0 1px #a36597}.woocommerce-message .button-primary:active,.woocommerce-message .button-primary:focus,.woocommerce-message .button-primary:hover,p.woocommerce-actions .button-primary:active,p.woocommerce-actions .button-primary:focus,p.woocommerce-actions .button-primary:hover{background:#a36597;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597}.woocommerce-message a.woocommerce-message-close,p.woocommerce-actions a.woocommerce-message-close{position:static;float:left;top:0;left:0;padding:0 28px 10px 15px;margin-top:-10px;font-size:13px;line-height:1.23076923;text-decoration:none}.woocommerce-message a.woocommerce-message-close::before,p.woocommerce-actions a.woocommerce-message-close::before{position:relative;top:18px;right:-20px;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out}.woocommerce-message .button-primary,.woocommerce-message .button-secondary,p.woocommerce-actions .button-primary,p.woocommerce-actions .button-secondary{text-decoration:none!important}.woocommerce-message .twitter-share-button,p.woocommerce-actions .twitter-share-button{margin-top:-3px;margin-right:3px;vertical-align:middle}.woocommerce-about-text,p.woocommerce-actions{margin-bottom:1em!important} \ No newline at end of file diff --git a/assets/css/activation.css b/assets/css/activation.css new file mode 100644 index 0000000..15e7b97 --- /dev/null +++ b/assets/css/activation.css @@ -0,0 +1 @@ +div.woocommerce-message{overflow:hidden;position:relative}div.woocommerce-message.updated{border-left-color:#cc99c2!important}.woocommerce-message .button-primary,p.woocommerce-actions .button-primary{background:#bb77ae;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597;color:#fff;text-shadow:0 -1px 1px #a36597,1px 0 1px #a36597,0 1px 1px #a36597,-1px 0 1px #a36597}.woocommerce-message .button-primary:active,.woocommerce-message .button-primary:focus,.woocommerce-message .button-primary:hover,p.woocommerce-actions .button-primary:active,p.woocommerce-actions .button-primary:focus,p.woocommerce-actions .button-primary:hover{background:#a36597;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597}.woocommerce-message a.woocommerce-message-close,p.woocommerce-actions a.woocommerce-message-close{position:static;float:right;top:0;right:0;padding:0 15px 10px 28px;margin-top:-10px;font-size:13px;line-height:1.23076923;text-decoration:none}.woocommerce-message a.woocommerce-message-close::before,p.woocommerce-actions a.woocommerce-message-close::before{position:relative;top:18px;left:-20px;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out}.woocommerce-message .button-primary,.woocommerce-message .button-secondary,p.woocommerce-actions .button-primary,p.woocommerce-actions .button-secondary{text-decoration:none!important}.woocommerce-message .twitter-share-button,p.woocommerce-actions .twitter-share-button{margin-top:-3px;margin-left:3px;vertical-align:middle}.woocommerce-about-text,p.woocommerce-actions{margin-bottom:1em!important} \ No newline at end of file diff --git a/assets/css/activation.scss b/assets/css/activation.scss new file mode 100644 index 0000000..30ad30b --- /dev/null +++ b/assets/css/activation.scss @@ -0,0 +1,71 @@ +/** + * activation.scss + * Styles applied to elements displayed on activation + */ + +/** + * Styling begins + */ +div.woocommerce-message { + overflow: hidden; + position: relative; + + &.updated { + border-left-color: #cc99c2 !important; + } +} + +p.woocommerce-actions, +.woocommerce-message { + + .button-primary { + background: #bb77ae; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + color: #fff; + text-shadow: 0 -1px 1px #a36597, 1px 0 1px #a36597, 0 1px 1px #a36597, -1px 0 1px #a36597; + + &:hover, + &:focus, + &:active { + background: #a36597; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + } + } + + a.woocommerce-message-close { + position: static; + float: right; + top: 0; + right: 0; + padding: 0 15px 10px 28px; + margin-top: -10px; + font-size: 13px; + line-height: 1.23076923; + text-decoration: none; + + &::before { + position: relative; + top: 18px; + left: -20px; + transition: all 0.1s ease-in-out; + } + } + + .button-primary, + .button-secondary { + text-decoration: none !important; + } + + .twitter-share-button { + margin-top: -3px; + margin-left: 3px; + vertical-align: middle; + } +} + +p.woocommerce-actions, +.woocommerce-about-text { + margin-bottom: 1em !important; +} diff --git a/assets/css/admin-rtl.css b/assets/css/admin-rtl.css new file mode 100644 index 0000000..de844d9 --- /dev/null +++ b/assets/css/admin-rtl.css @@ -0,0 +1,2 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;margin:0 0 -4px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir=rtl] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left;padding:0}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin:0;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:#fff;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected],.select2-results__option[data-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff}.select2-hidden-accessible{border:0!important;clip:rect(0 0 0 0)!important;height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:700}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir=rtl] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir=rtl] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:#fff;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none;margin:5px 5px 0 0}.select2-container--default .select2-selection--multiple .select2-selection__rendered li:before{content:'';display:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:700;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:700;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-search--inline,.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__placeholder{float:right}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid #000 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple,.select2-container--default.select2-container--open.select2-container--above .select2-selection--single{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple,.select2-container--default.select2-container--open.select2-container--below .select2-selection--single{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:0 0;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[data-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#0073aa;color:#fff}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(50%,#fff),to(#eee));background-image:linear-gradient(to bottom,#fff 50%,#eee 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--single:focus{border:1px solid #0073aa}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:700;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(50%,#eee),to(#ccc));background-image:linear-gradient(to bottom,#eee 50%,#ccc 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir=rtl] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir=rtl] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #0073aa}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:0 0;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-gradient(linear,left top,left bottom,from(white),color-stop(50%,#eee));background-image:linear-gradient(to bottom,#fff 0,#eee 50%);background-repeat:repeat-x}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(50%,#eee),to(white));background-image:linear-gradient(to bottom,#eee 50%,#fff 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--multiple{background-color:#fff;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #0073aa}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:700;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #0073aa}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected],.select2-container--classic .select2-results__option--highlighted[data-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#0073aa} +@charset "UTF-8";:root{--woocommerce:#a46497;--wc-green:#7ad03a;--wc-red:#a00;--wc-orange:#ffba00;--wc-blue:#2ea2cc;--wc-primary:#a46497;--wc-primary-text:white;--wc-secondary:#ebe9eb;--wc-secondary-text:#515151;--wc-highlight:#77a464;--wc-highligh-text:white;--wc-content-bg:#fff;--wc-subtext:#767676}@-webkit-keyframes spin{100%{-webkit-transform:rotate(-360deg);transform:rotate(-360deg)}}@keyframes spin{100%{-webkit-transform:rotate(-360deg);transform:rotate(-360deg)}}@font-face{font-family:star;src:url(../fonts/star.eot);src:url(../fonts/star.eot?#iefix) format("embedded-opentype"),url(../fonts/star.woff) format("woff"),url(../fonts/star.ttf) format("truetype"),url(../fonts/star.svg#star) format("svg");font-weight:400;font-style:normal}@font-face{font-family:WooCommerce;src:url(../fonts/WooCommerce.eot);src:url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"),url(../fonts/WooCommerce.woff) format("woff"),url(../fonts/WooCommerce.ttf) format("truetype"),url(../fonts/WooCommerce.svg#WooCommerce) format("svg");font-weight:400;font-style:normal}.blockUI.blockOverlay::before{height:1em;width:1em;display:block;position:absolute;top:50%;right:50%;margin-right:-.5em;margin-top:-.5em;content:"";-webkit-animation:spin 1s ease-in-out infinite;animation:spin 1s ease-in-out infinite;background:url(../images/icons/loader.svg) center center;background-size:cover;line-height:1;text-align:center;font-size:2em;color:rgba(0,0,0,.75)}.wc-addons-wrap .marketplace-header{background-image:url(../images/marketplace-header-bg@2x.png);background-position:left;background-size:cover;box-sizing:border-box;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:center;justify-content:center;min-height:216px;padding:24px 16px;width:100%}.wc-addons-wrap .marketplace-header__title{color:#fff;font-size:32px;font-style:normal;font-weight:400;line-height:1.15;margin-bottom:8px;padding:0}.wc-addons-wrap .marketplace-header__description{color:#fff;font-size:16px;line-height:24px;margin-bottom:24px;margin-top:0}.wc-addons-wrap .marketplace-header__search-form{clear:both;display:block;max-width:318px;position:relative}.wc-addons-wrap .marketplace-header__search-form input{border:1px solid #ddd;box-shadow:none;font-size:13px;height:48px;padding-right:16px;padding-left:50px;width:100%;margin:0}.wc-addons-wrap .marketplace-header__search-form button{background:0 0;border:none;cursor:pointer;height:48px;position:absolute;left:0;width:53px}.wc-addons-wrap .top-bar{background:#fff;box-shadow:inset 0 -1px 0 #ccc;display:block;height:60px;margin:0 0 16px}@media only screen and (min-width:768px){.wc-addons-wrap .top-bar{margin-bottom:24px}}.wc-addons-wrap .top-bar .current-section-dropdown{position:relative;width:100%}@media only screen and (min-width:600px){.wc-addons-wrap .top-bar .current-section-dropdown{margin-right:70px;width:288px}}.wc-addons-wrap .top-bar .current-section-name{cursor:pointer;font-weight:600;font-size:14px;line-height:20px;padding:20px 16px;position:relative}.wc-addons-wrap .top-bar .current-section-name::after{background-image:url(../images/icons/gridicons-chevron-down.svg);background-size:contain;content:"";display:block;height:20px;position:absolute;left:20px;top:20px;width:20px}.wc-addons-wrap .top-bar ul{background:#fff;border-radius:2px;display:none;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:left;justify-content:left;right:0;margin:0;padding:14px 0;position:absolute;top:50px;width:100%;z-index:10}@media only screen and (min-width:600px){.wc-addons-wrap .top-bar ul{border:1px solid #1e1e1e}}@media only screen and (min-width:1100px){.wc-addons-wrap .top-bar ul{-webkit-box-pack:center;justify-content:center}}.wc-addons-wrap .top-bar ul li{font-size:13px;line-height:16px;margin:0}.wc-addons-wrap .top-bar ul a,.wc-addons-wrap .top-bar ul a:focus,.wc-addons-wrap .top-bar ul a:hover,.wc-addons-wrap .top-bar ul a:visited{border:none;box-shadow:none;box-sizing:border-box;color:#1e1e1e;display:inline-block;text-decoration:none;outline:0;padding:14px 18px;position:relative;width:100%}@media only screen and (min-width:600px){.wc-addons-wrap .top-bar ul a,.wc-addons-wrap .top-bar ul a:focus,.wc-addons-wrap .top-bar ul a:hover,.wc-addons-wrap .top-bar ul a:visited{padding:10px 18px}}.wc-addons-wrap .top-bar ul a.current::after{background-image:url(../images/icons/gridicons-checkmark.svg);content:"";display:block;height:20px;position:absolute;left:20px;top:7px;width:20px}.wc-addons-wrap .top-bar .current-section-dropdown.is-open ul{display:-webkit-box;display:flex}.wc-addons-wrap .top-bar .current-section-dropdown.is-open .current-section-name::after{-webkit-transform:rotate(-.5turn);-ms-transform:rotate(-.5turn);transform:rotate(-.5turn)}.wc-addons-wrap .update-plugins .update-count{background-color:#d54e21;border-radius:10px;color:#fff;display:inline-block;font-size:9px;font-weight:600;line-height:17px;margin:1px 2px 0 0;padding:0 6px;vertical-align:text-top}.wc-addons-wrap h1.search-form-title{clear:right;font-size:20px;font-family:sans-serif;line-height:1.2;margin:48px 0 16px;padding:0}.wc-addons-wrap .addons-featured{margin:0}.wc-addons-wrap ul.subsubsub.subsubsub{margin:-2px 0 12px}.wc-addons-wrap .subsubsub li::after{content:"|"}.wc-addons-wrap .subsubsub li:last-child::after{content:""}.wc-addons-wrap .addons-button{border-radius:3px;cursor:pointer;display:block;height:37px;line-height:37px;margin-top:16px;text-align:center;text-decoration:none;width:124px}.wc-addons-wrap .addons-wcs-banner-block{-webkit-box-align:center;align-items:center;background:#fff;border:1px solid #ddd;display:-webkit-box;display:flex;margin:0 0 1em 0;padding:2em 2em 1em}.wc-addons-wrap .addons-wcs-banner-block-image{background:#f7f7f7;border:1px solid #e6e6e6;margin-left:2em;padding:4em;max-width:200px}.wc-addons-wrap .addons-wcs-banner-block-image .addons-img{max-height:86px;max-width:97px}.wc-addons-wrap .addons-wcs-banner-block-image.is-full-image{padding:0;background:0 0;border:none}.wc-addons-wrap .addons-wcs-banner-block-image.is-full-image .addons-img{max-height:100%;max-width:100%}.wc-addons-wrap .addons-shipping-methods .addons-wcs-banner-block{margin-right:0;margin-left:0;margin-top:1em}.wc-addons-wrap .addons-wcs-banner-block-content{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;justify-content:space-around;align-self:stretch;padding:1em 0}.wc-addons-wrap .addons-wcs-banner-block-content h1{padding-bottom:0}.wc-addons-wrap .addons-wcs-banner-block-content p{margin-bottom:0}.wc-addons-wrap .addons-wcs-banner-block-content .wcs-logos-container{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-pack:center;justify-content:center}@media screen and (min-width:500px){.wc-addons-wrap .addons-wcs-banner-block-content .wcs-logos-container{-webkit-box-pack:left;justify-content:left}}.wc-addons-wrap .addons-wcs-banner-block-content .wcs-logos-container li{margin-left:8px}.wc-addons-wrap .addons-wcs-banner-block-content .wcs-logos-container li:last-child{margin-left:0}.wc-addons-wrap .addons-wcs-banner-block-content .wcs-service-logo{max-width:45px}.wc-addons-wrap .addons-column{-webkit-box-flex:1;flex:1;width:50%;padding:0 .5em}.wc-addons-wrap .addons-column:nth-child(2){margin-left:0}.wc-addons-wrap .addons-small-dark-items{display:-webkit-box;display:flex;flex-wrap:wrap;justify-content:space-around}.wc-addons-wrap .addons-small-dark-item{margin:0 0 20px}.wc-addons-wrap .addons-small-dark-item-icon img{height:30px}.wc-addons-wrap .addons-small-dark-item a{margin:28px auto 0}.wc-addons-wrap .addons-button-solid{background-color:#674399;color:#fff}.wc-addons-wrap .addons-button-promoted{float:left;width:auto;padding:0 20px;margin-top:0}.wc-addons-wrap .addons-button-promoted:hover{opacity:.8}.wc-addons-wrap .addons-button-expandable{display:inline-block;padding:0 16px;width:auto}.wc-addons-wrap .addons-button-solid:hover{color:#fff;opacity:.8}.wc-addons-wrap .addons-button-outline-green{border:1px solid #73ae39;color:#73ae39}.wc-addons-wrap .addons-button-outline-green:hover{color:#73ae39;opacity:.8}.wc-addons-wrap .addons-button-outline-purple{border:1px solid #674399;color:#674399}.wc-addons-wrap .addons-button-outline-purple:hover{color:#674399;opacity:.8}.wc-addons-wrap .addons-button-outline-white{border:1px solid #fff;color:#fff}.wc-addons-wrap .addons-button-outline-white:hover{color:#fff;opacity:.8}.wc-addons-wrap .addons-button-installed{background:#e6e6e6;color:#3c3c3c}.wc-addons-wrap .addons-button-installed:hover{color:#3c3c3c;opacity:.8}@media only screen and (max-width:400px){.wc-addons-wrap .addons-featured{margin:-1% -5%}.wc-addons-wrap .addons-button{width:100%}.wc-addons-wrap .addons-small-dark-item{width:100%}}.wc-addons-wrap .marketplace-content-wrapper{font-family:helveticaneue-light,"Helvetica Neue Light","Helvetica Neue",sans-serif;margin:0 auto;max-width:1032px;width:100%}.wc-addons-wrap .addon-product-group-title{font-family:sans-serif;letter-spacing:.38px}.wc-addons-wrap .addon-product-group-description-container{-webkit-box-align:center;align-items:center;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;font-size:14px;-webkit-box-pack:justify;justify-content:space-between;line-height:20px}.wc-addons-wrap .addon-product-group-description-container .addon-product-group-see-more,.wc-addons-wrap .addon-product-group-description-container .addon-product-group-see-more:visited{color:#007cba;display:block;font-size:13px;text-decoration:none}.wc-addons-wrap .products{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row;flex-wrap:wrap;font-weight:400;-webkit-box-pack:justify;justify-content:space-between;margin:0;max-width:1032px;overflow:hidden}.wc-addons-wrap .products .product.addons-buttons-banner,.wc-addons-wrap .products .product.addons-product-banner{max-width:calc(100% - 2px)}@media screen and (min-width:960px){.wc-addons-wrap .products.addons-products-three-column li.product{max-width:calc(33.33% - 12px)}.wc-addons-wrap .products.addons-products-three-column li.product h2,.wc-addons-wrap .products.addons-products-three-column li.product h3{font-size:16px}}.wc-addons-wrap .products li{background:#fff;border:1px solid #dcdcde;border-radius:2px;display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1 0 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:justify;justify-content:space-between;margin:12px 0;max-width:calc(50% - 12px);min-width:280px;min-height:220px;overflow:hidden;padding:0;vertical-align:top}.wc-addons-wrap .products li.addons-full-width{max-width:100%}@media only screen and (max-width:768px){.wc-addons-wrap .products li{max-width:none;width:100%}}.wc-addons-wrap .products li a{text-decoration:none}.wc-addons-wrap .products li .product-details{padding:24px;position:relative}.wc-addons-wrap .products li .product-details .product-img-wrap{display:block;margin-right:24px;position:absolute;left:24px;top:24px}.wc-addons-wrap .products li .product-details .product-img-wrap img{border-radius:3px;display:block;margin:0;max-width:48px;max-height:48px}.wc-addons-wrap .products li .product-details.addon-product-banner-details{-webkit-box-align:center;align-items:center;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-pack:justify;justify-content:space-between}.wc-addons-wrap .products li .product-details.addon-product-banner-details .product-img-wrap{position:unset}.wc-addons-wrap .products li .product-details.addon-product-banner-details .product-img-wrap img{max-width:150px;max-height:150px}.wc-addons-wrap .products li .product-details h2,.wc-addons-wrap .products li .product-details h3{color:#007cba;font-size:20px;font-weight:400;letter-spacing:-.32px;line-height:28px;margin:0!important;max-width:calc(100% - 48px)}.wc-addons-wrap .products li .product-details .addons-buttons-banner-details h2{color:#1d2327}.wc-addons-wrap .products li .product-details.featured .label,.wc-addons-wrap .products li .product-details.promoted .label{-webkit-box-align:center;align-items:center;border-radius:2px;background:#dcdcde;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;height:20px;-webkit-box-pack:end;justify-content:flex-end;margin-bottom:8px;max-width:52px;padding:3px 12px;top:28px;left:24px;text-align:center}.wc-addons-wrap .products li .product-details.featured .label.promoted,.wc-addons-wrap .products li .product-details.promoted .label.promoted{float:left;max-width:58px}.wc-addons-wrap .products li .product-details.featured h2,.wc-addons-wrap .products li .product-details.promoted h2{color:#2c3338}.wc-addons-wrap .products li .product-details p{color:#2c3338;font-size:14px;line-height:20px;margin:14px 0 0 64px;width:100%}.wc-addons-wrap .products li .product-details .addons-buttons-banner-details p{font-size:14px;margin-bottom:14px;max-width:none}.wc-addons-wrap .products li .product-details .product-developed-by{color:#50575e;font-size:12px;line-height:20px;margin-top:4px}.wc-addons-wrap .products li .product-details .product-developed-by .product-vendor-link{color:#50575e}.wc-addons-wrap .products li .product-details .product-developed-by{color:#50575e;font-size:12px;font-family:sans-serif;line-height:20px;margin-top:4px}.wc-addons-wrap .products li .product-details .product-developed-by .product-vendor-link{color:#50575e}.wc-addons-wrap .products li .product-footer{-webkit-box-align:center;align-items:center;border-top:1px solid #dcdcde;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-pack:justify;justify-content:space-between;padding:24px}.wc-addons-wrap .products li .product-footer .price{font-size:16px;color:#1d2327}.wc-addons-wrap .products li .product-footer .price-suffix{color:#646970}.wc-addons-wrap .products li .product-footer .product-reviews-block{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;margin-top:4px}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-rating-star{background-repeat:no-repeat;background-size:contain;height:16px;margin:4px 0 4px 4px;width:17px}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-rating-star__fill{background-image:url(../images/icons/star-golden.svg)}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-rating-star__half-fill{background-image:url(../images/icons/star-half-filled.svg)}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-rating-star__no-fill{background-image:url(../images/icons/star-gray.svg)}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-reviews-count{color:#646970;font-size:12px;font-family:sans-serif;line-height:24px;letter-spacing:-.154px;margin-right:4px}.wc-addons-wrap .products li .product-footer .button{background-color:#fff;border-color:#007cba;color:#007cba;float:left;font-size:13px;height:36px;line-height:30px;padding:2px 14px}.wc-addons-wrap .products .product-footer-promoted{-webkit-box-align:end;align-items:flex-end;display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;padding:24px}.wc-addons-wrap .products .product-footer-promoted .icon img{border-radius:4px;width:80px}.wc-addons-wrap .products .addons-buttons-banner{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row}.wc-addons-wrap .products .addons-buttons-banner .addons-buttons-banner-image{background-repeat:no-repeat;background-size:cover;height:190px;margin:24px;width:200px}.wc-addons-wrap .products .addons-buttons-banner .addons-buttons-banner-details-container{padding-right:0;width:calc(100% - 198px - 24px - 24px)}.wc-addons-wrap .products .addons-buttons-banner .addons-buttons-banner-details-container{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:justify;justify-content:space-between}.wc-addons-wrap .products .addons-buttons-banner .button.addons-buttons-banner-button,.wc-addons-wrap .products .addons-buttons-banner .button.addons-buttons-banner-button:hover{background:#fff;border:1.5px solid #624594;color:#624594;padding:4px 12px;margin-left:16px}.wc-addons-wrap .products .addons-buttons-banner .button.addons-buttons-banner-button.addons-buttons-banner-button-primary,.wc-addons-wrap .products .addons-buttons-banner .button.addons-buttons-banner-button:hover.addons-buttons-banner-button-primary{background-color:#624594;color:#fff}.wc-addons-wrap .storefront{max-width:990px;background:url(../images/storefront-bg.jpg) bottom left #f6f6f6;border:1px solid #ddd;margin:1em auto;padding:24px;overflow:hidden;zoom:1}.wc-addons-wrap .storefront img{display:block;width:100%;max-width:400px;height:auto;margin:0 auto 16px;box-shadow:0 1px 6px rgba(0,0,0,.1)}.wc-addons-wrap .storefront p:last-of-type{margin-bottom:0}.wc-addons-wrap .storefront p{max-width:750px}.no-js .wc-addons-wrap .current-section-dropdown:hover ul,.no-touch .wc-addons-wrap .current-section-dropdown:hover ul{display:-webkit-box;display:flex}.no-js .wc-addons-wrap .current-section-dropdown:hover .current-section-name::after,.no-touch .wc-addons-wrap .current-section-dropdown:hover .current-section-name::after{-webkit-transform:rotate(-.5turn);-ms-transform:rotate(-.5turn);transform:rotate(-.5turn)}.wc-subscriptions-wrap{max-width:1200px}.woocommerce-page-wc-marketplace .notice{margin-right:20px;margin-left:20px}.woocommerce-page-wc-marketplace.woocommerce-page .wrap{margin-top:32px}.woocommerce-page-wc-subscriptions #wpbody-content .screen-reader-text+.notice{margin-top:32px}.woocommerce-embed-page.woocommerce-page-wc-marketplace #screen-meta-links{position:absolute;left:0}.woocommerce-BlankState a.button-primary,.woocommerce-BlankState button.button-primary,.woocommerce-message a.button-primary,.woocommerce-message button.button-primary{background:#bb77ae;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597;color:#fff;text-shadow:0 -1px 1px #a36597,-1px 0 1px #a36597,0 1px 1px #a36597,1px 0 1px #a36597;display:inline-block}.woocommerce-BlankState a.button-primary:active,.woocommerce-BlankState a.button-primary:focus,.woocommerce-BlankState a.button-primary:hover,.woocommerce-BlankState button.button-primary:active,.woocommerce-BlankState button.button-primary:focus,.woocommerce-BlankState button.button-primary:hover,.woocommerce-message a.button-primary:active,.woocommerce-message a.button-primary:focus,.woocommerce-message a.button-primary:hover,.woocommerce-message button.button-primary:active,.woocommerce-message button.button-primary:focus,.woocommerce-message button.button-primary:hover{background:#a36597;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597}.woocommerce-message{position:relative;overflow:hidden}.woocommerce-message.updated{border-right-color:#cc99c2!important}.woocommerce-message a.docs,.woocommerce-message a.skip{text-decoration:none!important}.woocommerce-message a.woocommerce-message-close{position:static;float:left;padding:0 28px 10px 15px;margin-top:-10px;font-size:13px;line-height:1.23076923;text-decoration:none}.woocommerce-message a.woocommerce-message-close::before{position:relative;top:18px;right:-20px;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out}.woocommerce-message .twitter-share-button{margin-top:-3px;margin-right:3px;vertical-align:middle}#variable_product_options #message,#variable_product_options .notice{margin:10px}#variable_product_options .form-row select{max-width:100%}#variable_product_options .toolbar-top .button{margin:1px}#product_attributes .toolbar-top .button{margin:1px}.clear{clear:both}.wrap.woocommerce div.error,.wrap.woocommerce div.updated{margin-top:10px}mark.amount{background:transparent none;color:inherit}.woocommerce-help-tip{color:#666;display:inline-block;font-size:1.1em;font-style:normal;height:16px;line-height:16px;position:relative;vertical-align:middle;width:16px}.woocommerce-help-tip::after{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";cursor:help}.wc-wp-version-gte-53 .woocommerce-help-tip{font-size:1.2em;cursor:help}h2 .woocommerce-help-tip{margin-top:-5px;margin-right:.25em}table.wc_status_table{margin-bottom:1em}table.wc_status_table h2{font-size:14px;margin:0}table.wc_status_table tr:nth-child(2n) td,table.wc_status_table tr:nth-child(2n) th{background:#fcfcfc}table.wc_status_table th{font-weight:700;padding:9px}table.wc_status_table td:first-child{width:33%}table.wc_status_table td.help{width:1em}table.wc_status_table td,table.wc_status_table th{font-size:1.1em;font-weight:400}table.wc_status_table td.run-tool,table.wc_status_table th.run-tool{text-align:left}table.wc_status_table td strong.name,table.wc_status_table th strong.name{display:block;margin-bottom:.5em}table.wc_status_table td mark,table.wc_status_table th mark{background:transparent none}table.wc_status_table td mark.yes,table.wc_status_table th mark.yes{color:#7ad03a}table.wc_status_table td mark.no,table.wc_status_table th mark.no{color:#999}table.wc_status_table td .red,table.wc_status_table td mark.error,table.wc_status_table th .red,table.wc_status_table th mark.error{color:#a00}table.wc_status_table td ul,table.wc_status_table th ul{margin:0}table.wc_status_table .help_tip{cursor:help}table.wc_status_table--tools td,table.wc_status_table--tools th{padding:2em}.taxonomy-product_cat .check-column .woocommerce-help-tip{font-size:1.5em;margin:-3px 5px 0 0;display:block;position:absolute}#debug-report{display:none;margin:10px 0;padding:0;position:relative}#debug-report textarea{font-family:monospace;width:100%;margin:0;height:300px;padding:20px;border-radius:0;resize:none;font-size:12px;line-height:20px;outline:0}.wp-list-table.logs .log-level{display:inline;padding:.2em .6em .3em;font-size:80%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.2em}.wp-list-table.logs .log-level:empty{display:none}.wp-list-table.logs .log-level--alert,.wp-list-table.logs .log-level--emergency{background-color:#ff4136}.wp-list-table.logs .log-level--critical,.wp-list-table.logs .log-level--error{background-color:#ff851b}.wp-list-table.logs .log-level--notice,.wp-list-table.logs .log-level--warning{color:#222;background-color:#ffdc00}.wp-list-table.logs .log-level--info{background-color:#0074d9}.wp-list-table.logs .log-level--debug{background-color:#3d9970}@media screen and (min-width:783px){.wp-list-table.logs .column-timestamp{width:18%}.wp-list-table.logs .column-level{width:14%}.wp-list-table.logs .column-source{width:15%}}#log-viewer-select{padding:10px 0 8px;line-height:28px}#log-viewer-select h2 a{vertical-align:middle}#log-viewer{background:#fff;border:1px solid #e5e5e5;box-shadow:0 1px 1px rgba(0,0,0,.04);padding:5px 20px}#log-viewer pre{font-family:monospace;white-space:pre-wrap;word-wrap:break-word}.inline-edit-product.quick-edit-row .inline-edit-col-center,.inline-edit-product.quick-edit-row .inline-edit-col-right{float:left!important}#woocommerce-fields.inline-edit-col{clear:right}#woocommerce-fields.inline-edit-col label.featured,#woocommerce-fields.inline-edit-col label.manage_stock{margin-right:10px}#woocommerce-fields.inline-edit-col label.stock_status_field{clear:both;float:right}#woocommerce-fields.inline-edit-col .dimensions div{display:block;margin:.2em 0}#woocommerce-fields.inline-edit-col .dimensions div span.title{display:block;float:right;width:5em}#woocommerce-fields.inline-edit-col .dimensions div span.input-text-wrap{display:block;margin-right:5em}#woocommerce-fields.inline-edit-col .text{box-sizing:border-box;width:99%;float:right;margin:1px 1px 1px 1%}#woocommerce-fields.inline-edit-col .height,#woocommerce-fields.inline-edit-col .length,#woocommerce-fields.inline-edit-col .width{width:32.33%}#woocommerce-fields.inline-edit-col .height{margin-left:0}#woocommerce-fields-bulk.inline-edit-col label{clear:right}#woocommerce-fields-bulk.inline-edit-col .inline-edit-group label{clear:none;width:49%;margin:.2em 0}#woocommerce-fields-bulk.inline-edit-col .inline-edit-group.dimensions label{width:75%;max-width:75%}#woocommerce-fields-bulk.inline-edit-col .length,#woocommerce-fields-bulk.inline-edit-col .regular_price,#woocommerce-fields-bulk.inline-edit-col .sale_price,#woocommerce-fields-bulk.inline-edit-col .stock,#woocommerce-fields-bulk.inline-edit-col .weight{box-sizing:border-box;width:100%;margin-right:4.4em}#woocommerce-fields-bulk.inline-edit-col .height,#woocommerce-fields-bulk.inline-edit-col .length,#woocommerce-fields-bulk.inline-edit-col .width{box-sizing:border-box;width:25%}.column-coupon_code{line-height:2.25em}.column-coupon_code,ul.wc_coupon_list{margin:0;overflow:hidden;zoom:1;clear:both}ul.wc_coupon_list{padding-bottom:5px}ul.wc_coupon_list li{margin:0}ul.wc_coupon_list li.code{display:inline-block;position:relative;padding:0 .5em;background-color:#fff;border:1px solid #aaa;box-shadow:0 1px 0 #dfdfdf;border-radius:4px;margin-left:5px;margin-top:5px}ul.wc_coupon_list li.code.editable{padding-left:2em}ul.wc_coupon_list li.code .tips{cursor:pointer}ul.wc_coupon_list li.code .tips span{color:#888}ul.wc_coupon_list li.code .tips span:hover{color:#000}ul.wc_coupon_list li.code .remove-coupon{text-decoration:none;color:#888;position:absolute;top:7px;left:20px;left:7px}ul.wc_coupon_list li.code .remove-coupon::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:""}ul.wc_coupon_list li.code .remove-coupon:hover::before{color:#a00}ul.wc_coupon_list_block{margin:0;padding-bottom:2px}ul.wc_coupon_list_block li{border-top:1px solid #fff;border-bottom:1px solid #ccc;line-height:2.5em;margin:0;padding:.5em 0}ul.wc_coupon_list_block li:first-child{border-top:0;padding-top:0}ul.wc_coupon_list_block li:last-child{border-bottom:0;padding-bottom:0}.button.wc-reload{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;padding:0;height:28px;width:28px!important;display:inline-block}.button.wc-reload::after{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";line-height:28px}#woocommerce-order-data .handlediv,#woocommerce-order-data .hndle,#woocommerce-order-data .postbox-header{display:none}#woocommerce-order-data .inside{display:block!important}#order_data{padding:23px 24px 12px}#order_data h2{margin:0;font-family:HelveticaNeue-Light,"Helvetica Neue Light","Helvetica Neue",sans-serif;font-size:21px;font-weight:400;line-height:1.2;text-shadow:-1px 1px 1px #fff;padding:0}#order_data h3{font-size:14px}#order_data h3,#order_data h4{color:#333;margin:1.33em 0 0}#order_data p{color:#777}#order_data p.order_number{margin:0;font-family:HelveticaNeue-Light,"Helvetica Neue Light","Helvetica Neue",sans-serif;font-weight:400;line-height:1.6em;font-size:16px}#order_data .order_data_column_container{clear:both}#order_data .order_data_column_container p._billing_email_field{margin-top:13px}#order_data .order_data_column{width:32%;padding:0 0 0 2%;float:right}#order_data .order_data_column>h3 span{display:block}#order_data .order_data_column:last-child{padding-left:0}#order_data .order_data_column p{padding:0!important}#order_data .order_data_column .address strong{display:block}#order_data .order_data_column .form-field{float:right;clear:right;width:48%;padding:0;margin:9px 0 0}#order_data .order_data_column .form-field label{display:block;padding:0 0 3px}#order_data .order_data_column .form-field input,#order_data .order_data_column .form-field textarea{width:100%}#order_data .order_data_column .form-field select{width:100%;max-width:100%}#order_data .order_data_column .form-field .select2-container{width:100%!important}#order_data .order_data_column .form-field .date-picker{width:50%}#order_data .order_data_column .form-field .hour,#order_data .order_data_column .form-field .minute{width:3.5em}#order_data .order_data_column .form-field small{display:block;margin:5px 0 0;color:#999}#order_data .order_data_column ._billing_address_2_field,#order_data .order_data_column ._billing_last_name_field,#order_data .order_data_column ._billing_phone_field,#order_data .order_data_column ._billing_postcode_field,#order_data .order_data_column ._billing_state_field,#order_data .order_data_column ._shipping_address_2_field,#order_data .order_data_column ._shipping_last_name_field,#order_data .order_data_column ._shipping_postcode_field,#order_data .order_data_column ._shipping_state_field,#order_data .order_data_column .form-field.last{float:left;clear:left}#order_data .order_data_column ._billing_company_field,#order_data .order_data_column ._shipping_company_field,#order_data .order_data_column ._transaction_id_field,#order_data .order_data_column .form-field-wide{width:100%;clear:both}#order_data .order_data_column ._billing_company_field .wc-category-search,#order_data .order_data_column ._billing_company_field .wc-customer-search,#order_data .order_data_column ._billing_company_field .wc-enhanced-select,#order_data .order_data_column ._billing_company_field input,#order_data .order_data_column ._billing_company_field select,#order_data .order_data_column ._billing_company_field textarea,#order_data .order_data_column ._shipping_company_field .wc-category-search,#order_data .order_data_column ._shipping_company_field .wc-customer-search,#order_data .order_data_column ._shipping_company_field .wc-enhanced-select,#order_data .order_data_column ._shipping_company_field input,#order_data .order_data_column ._shipping_company_field select,#order_data .order_data_column ._shipping_company_field textarea,#order_data .order_data_column ._transaction_id_field .wc-category-search,#order_data .order_data_column ._transaction_id_field .wc-customer-search,#order_data .order_data_column ._transaction_id_field .wc-enhanced-select,#order_data .order_data_column ._transaction_id_field input,#order_data .order_data_column ._transaction_id_field select,#order_data .order_data_column ._transaction_id_field textarea,#order_data .order_data_column .form-field-wide .wc-category-search,#order_data .order_data_column .form-field-wide .wc-customer-search,#order_data .order_data_column .form-field-wide .wc-enhanced-select,#order_data .order_data_column .form-field-wide input,#order_data .order_data_column .form-field-wide select,#order_data .order_data_column .form-field-wide textarea{width:100%}#order_data .order_data_column p.none_set{color:#999}#order_data .order_data_column div.edit_address{display:none;zoom:1;padding-left:1px}#order_data .order_data_column div.edit_address .select2-container .select2-selection--single{height:32px}#order_data .order_data_column div.edit_address .select2-container .select2-selection--single .select2-selection__rendered{line-height:32px}#order_data .order_data_column .wc-customer-user label a,#order_data .order_data_column .wc-order-status label a{float:left;margin-right:8px}#order_data .order_data_column a.edit_address{width:14px;height:0;padding:14px 0 0;margin:0 6px 0 0;overflow:hidden;position:relative;color:#999;border:0;float:left}#order_data .order_data_column a.edit_address:focus,#order_data .order_data_column a.edit_address:hover{color:#000}#order_data .order_data_column a.edit_address::after{font-family:WooCommerce;position:absolute;top:0;right:0;text-align:center;vertical-align:top;line-height:14px;font-size:14px;font-weight:400}#order_data .order_data_column a.edit_address::after{font-family:Dashicons;content:"\f464"}#order_data .order_data_column .billing-same-as-shipping,#order_data .order_data_column .load_customer_billing,#order_data .order_data_column .load_customer_shipping{font-size:13px;display:inline-block;font-weight:400}#order_data .order_data_column .load_customer_shipping{margin-left:.3em}.order_actions{margin:0;overflow:hidden;zoom:1}.order_actions li{border-top:1px solid #fff;border-bottom:1px solid #ddd;padding:6px 0;margin:0;line-height:1.6em;float:right;width:50%;text-align:center}.order_actions li a{float:none;text-align:center;text-decoration:underline}.order_actions li.wide{width:auto;float:none;clear:both;padding:6px;text-align:right;overflow:hidden}.order_actions li #delete-action{line-height:25px;vertical-align:middle;text-align:right;float:right}.order_actions li .save_order{float:left}.order_actions li#actions{overflow:hidden}.order_actions li#actions .button{width:24px;box-sizing:border-box;float:left}.order_actions li#actions select{width:225px;box-sizing:border-box;float:right}#woocommerce-order-items .inside{margin:0;padding:0;background:#fefefe}#woocommerce-order-items .wc-order-data-row{border-bottom:1px solid #dfdfdf;padding:1.5em 2em;background:#f8f8f8;line-height:2em;text-align:left}#woocommerce-order-items .wc-order-data-row::after,#woocommerce-order-items .wc-order-data-row::before{content:" ";display:table}#woocommerce-order-items .wc-order-data-row::after{clear:both}#woocommerce-order-items .wc-order-data-row p{margin:0;line-height:2em}#woocommerce-order-items .wc-order-data-row .wc-used-coupons{text-align:right}#woocommerce-order-items .wc-order-data-row .wc-used-coupons .tips{display:inline-block}#woocommerce-order-items .wc-used-coupons{float:right;width:50%}#woocommerce-order-items .wc-order-totals{float:left;width:50%;margin:0;padding:0;text-align:left}#woocommerce-order-items .wc-order-totals .amount{font-weight:700}#woocommerce-order-items .wc-order-totals .label{vertical-align:top}#woocommerce-order-items .wc-order-totals .total{font-size:1em!important;width:10em;margin:0 .5em 0 0;box-sizing:border-box}#woocommerce-order-items .wc-order-totals .total input[type=text]{width:96%;float:left}#woocommerce-order-items .wc-order-totals .refunded-total{color:#a00}#woocommerce-order-items .wc-order-totals .label-highlight{font-weight:700}#woocommerce-order-items .refund-actions{margin-top:5px;padding-top:12px;border-top:1px solid #dfdfdf}#woocommerce-order-items .refund-actions .button{float:left;margin-right:4px}#woocommerce-order-items .refund-actions .cancel-action{float:right;margin-right:0}#woocommerce-order-items .add_meta{margin-right:0!important}#woocommerce-order-items h3 small{color:#999}#woocommerce-order-items .amount{white-space:nowrap}#woocommerce-order-items .add-items .description{margin-left:10px}#woocommerce-order-items .add-items .button{float:right;margin-left:.25em}#woocommerce-order-items .add-items .button-primary{float:none;margin-left:0}#woocommerce-order-items .inside{display:block!important}#woocommerce-order-items .handlediv,#woocommerce-order-items .hndle,#woocommerce-order-items .postbox-header{display:none}#woocommerce-order-items .woocommerce_order_items_wrapper{margin:0;overflow-x:auto}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items{width:100%;background:#fff}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th{text-align:right;padding:1em;font-weight:400;color:#999;background:#f8f8f8;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th.sortable{cursor:pointer}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th:last-child{padding-left:2em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th:first-child{padding-right:2em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th .wc-arrow{float:left;position:relative;margin-left:-1em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td{padding:1.5em 1em 1em;text-align:right;line-height:1.5em;vertical-align:top;border-bottom:1px solid #f8f8f8}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th textarea,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td textarea{width:100%}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td select{width:50%}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th textarea,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td textarea{font-size:14px;padding:4px;color:#555}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th:last-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td:last-child{padding-left:2em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td:first-child{padding-right:2em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody tr:last-child td{border-bottom:1px solid #dfdfdf}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody tr:first-child td{border-top:8px solid #f8f8f8}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody#order_line_items tr:first-child td{border-top:none}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.thumb{text-align:right;width:38px;padding-bottom:1.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.thumb .wc-order-item-thumbnail{width:38px;height:38px;border:2px solid #e8e8e8;background:#f8f8f8;color:#ccc;position:relative;font-size:21px;display:block;text-align:center}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.thumb .wc-order-item-thumbnail::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";width:38px;line-height:38px;display:block}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.thumb .wc-order-item-thumbnail img{width:100%;height:100%;margin:0;padding:0;position:relative}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.name .wc-order-item-sku,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.name .wc-order-item-variation{display:block;margin-top:.5em;font-size:.92em!important;color:#888}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item{min-width:200px}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .center,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .variation-id{text-align:center}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class{text-align:left}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class label{white-space:nowrap;color:#999;font-size:.833em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class label input{display:inline}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class input{width:70px;vertical-align:middle;text-align:left}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class select{width:85px;height:26px;vertical-align:middle;font-size:1em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input{display:inline-block;background:#fff;border:1px solid #ddd;box-shadow:inset 0 1px 2px rgba(0,0,0,.07);margin:1px 0;min-width:80px;overflow:hidden;line-height:1em;text-align:left}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input{width:100%;box-sizing:border-box}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input label{font-size:.75em;padding:4px 6px 0;color:#555;display:block}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input input{width:100%;box-sizing:border-box;border:0;box-shadow:none;margin:0;padding:0 6px 4px;color:#555;background:0 0}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input input::-webkit-input-placeholder{color:#ddd}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input:first-child{border-bottom:1px dashed #ddd;background:#fff}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input:first-child label{color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input:first-child input{color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .view{white-space:nowrap}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .edit{text-align:right}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class small.times{font-size:.92em!important;color:#888}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-taxes{margin:0}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-taxes label{display:block}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-discount{display:block;margin-top:.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class small.times{margin-left:.25em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity{text-align:center}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity input{text-align:center;width:50px}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items span.subtotal{opacity:.5}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.tax_class,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.tax_class{text-align:right}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .calculated{border-color:#ae8ca2;border-style:dotted}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta{width:100%}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta{margin:.5em 0 0;font-size:.92em!important;color:#888}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr th,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr th{border:0;padding:0 0 .5em 4px;line-height:1.5em;width:20%}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td{padding:0 0 .5em 4px;border:0;line-height:1.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td input{width:100%;margin:0;position:relative;border-bottom:0;box-shadow:none}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td textarea,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td textarea{width:100%;height:4em;margin:0;box-shadow:none}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td input:focus+textarea,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td input:focus+textarea{border-top-color:#999}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td p,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td p{margin:0 0 .5em;line-height:1.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td p:last-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td p:last-child{margin:0}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .refund_by{border-bottom:1px dotted #999}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.fee .thumb div{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.5em;line-height:1em;vertical-align:middle;margin:0 auto}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.fee .thumb div::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.refund .thumb div{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.5em;line-height:1em;vertical-align:middle;margin:0 auto}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.refund .thumb div::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.shipping .thumb div{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.5em;line-height:1em;vertical-align:middle;margin:0 auto}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.shipping .thumb div::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.shipping .shipping_method,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.shipping .shipping_method_name{width:100%;margin:0 0 .5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax{white-space:nowrap}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.line_tax .delete-order-tax,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax .delete-order-tax{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;float:left;font-size:14px;visibility:hidden;margin:3px 0 0 -18px}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.line_tax .delete-order-tax::before,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax .delete-order-tax::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#999}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.line_tax .delete-order-tax:hover::before,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax .delete-order-tax:hover::before{color:#a00}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.line_tax:hover .delete-order-tax,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax:hover .delete-order-tax{visibility:visible}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items small.refunded{display:block;color:#a00;white-space:nowrap;margin-top:.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items small.refunded::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";position:relative;top:auto;right:auto;margin:-1px 0 0 4px;vertical-align:middle;line-height:1em}#woocommerce-order-items .wc-order-edit-line-item{padding-right:0}#woocommerce-order-items .wc-order-edit-line-item-actions{width:44px;text-align:left;padding-right:0;vertical-align:middle}#woocommerce-order-items .wc-order-edit-line-item-actions a{color:#ccc;display:inline-block;cursor:pointer;padding:0 0 .5em;margin:0 12px 0 0;vertical-align:middle;text-decoration:none;line-height:16px;width:16px;overflow:hidden}#woocommerce-order-items .wc-order-edit-line-item-actions a::before{margin:0;padding:0;font-size:16px;width:16px;height:16px}#woocommerce-order-items .wc-order-edit-line-item-actions a:hover::before{color:#999}#woocommerce-order-items .wc-order-edit-line-item-actions a:first-child{margin-right:0}#woocommerce-order-items .wc-order-edit-line-item-actions .edit-order-item::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";position:relative}#woocommerce-order-items .wc-order-edit-line-item-actions .delete-order-item::before,#woocommerce-order-items .wc-order-edit-line-item-actions .delete_refund::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";position:relative}#woocommerce-order-items .wc-order-edit-line-item-actions .delete-order-item:hover::before,#woocommerce-order-items .wc-order-edit-line-item-actions .delete_refund:hover::before{color:#a00}#woocommerce-order-items tbody tr .wc-order-edit-line-item-actions{visibility:hidden}#woocommerce-order-items tbody tr:hover .wc-order-edit-line-item-actions{visibility:visible}#woocommerce-order-items .wc-order-totals .wc-order-edit-line-item-actions{width:1.5em;visibility:visible!important}#woocommerce-order-items .wc-order-totals .wc-order-edit-line-item-actions a{padding:0}#woocommerce-order-downloads .buttons{float:right;padding:0;margin:0;vertical-align:top}#woocommerce-order-downloads .buttons .add_item_id,#woocommerce-order-downloads .buttons .select2-container{width:400px!important;margin-left:9px;vertical-align:top;float:right}#woocommerce-order-downloads .buttons button{margin:2px 0 0}#woocommerce-order-downloads h3 small{color:#999}#poststuff #woocommerce-order-actions .inside{margin:0;padding:0}#poststuff #woocommerce-order-actions .inside ul.order_actions li{padding:6px 10px;box-sizing:border-box}#poststuff #woocommerce-order-actions .inside ul.order_actions li:last-child{border-bottom:0}#poststuff #woocommerce-order-actions .inside button{margin:1px}#poststuff #woocommerce-order-notes .inside{margin:0;padding:0}#poststuff #woocommerce-order-notes .inside ul.order_notes li{padding:0 10px}#poststuff #woocommerce-order-notes .inside button{margin:1px;vertical-align:top}#woocommerce_customers p.search-box{margin:6px 0 4px;float:right}#woocommerce_customers .tablenav{float:left;clear:none}.widefat.customers td{vertical-align:middle;padding:4px 7px}.widefat .column-order_title{width:15%}.widefat .column-order_title time{display:block;color:#999;margin:3px 0}.widefat .column-orders,.widefat .column-paying,.widefat .column-spent{text-align:center;width:8%}.widefat .column-last_order{width:11%}.widefat .column-wc_actions{width:110px}.widefat .column-wc_actions a.button{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;display:inline-block;margin:2px 0 2px 4px;padding:0!important;height:2em!important;width:2em;overflow:hidden;vertical-align:middle}.widefat .column-wc_actions a.button::after{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;line-height:1.85}.widefat .column-wc_actions a.button img{display:block;width:12px;height:auto}.widefat .column-wc_actions a.edit::after{content:"\f464"}.widefat .column-wc_actions a.link::after{font-family:WooCommerce;content:"\e00d"}.widefat .column-wc_actions a.view::after{content:"\f177"}.widefat .column-wc_actions a.refresh::after{font-family:WooCommerce;content:"\e031"}.widefat .column-wc_actions a.processing::after{font-family:WooCommerce;content:"\e00f"}.widefat .column-wc_actions a.complete::after{content:"\f147"}.widefat small.meta{display:block;color:#999;font-size:inherit;margin:3px 0}.wc-wp-version-gte-53 .widefat .column-wc_actions a.button::after{margin-top:2px}.post-type-shop_order .tablenav .one-page .displaying-num{display:none}.post-type-shop_order .tablenav .select2-selection--single{height:32px}.post-type-shop_order .tablenav .select2-selection--single .select2-selection__rendered{line-height:29px}.post-type-shop_order .tablenav .select2-selection--single .select2-selection__arrow{height:30px}.post-type-shop_order .wp-list-table{margin-top:1em}.post-type-shop_order .wp-list-table tfoot th,.post-type-shop_order .wp-list-table thead th{padding:.75em 1em}.post-type-shop_order .wp-list-table tfoot th.sortable a,.post-type-shop_order .wp-list-table tfoot th.sorted a,.post-type-shop_order .wp-list-table thead th.sortable a,.post-type-shop_order .wp-list-table thead th.sorted a{padding:0}.post-type-shop_order .wp-list-table tfoot th:first-child,.post-type-shop_order .wp-list-table thead th:first-child{padding-right:2em}.post-type-shop_order .wp-list-table tfoot th:last-child,.post-type-shop_order .wp-list-table thead th:last-child{padding-left:2em}.post-type-shop_order .wp-list-table tbody td,.post-type-shop_order .wp-list-table tbody th{padding:1em;line-height:26px}.post-type-shop_order .wp-list-table tbody td:first-child{padding-right:2em}.post-type-shop_order .wp-list-table tbody td:last-child{padding-left:2em}.post-type-shop_order .wp-list-table tbody tr{border-top:1px solid #f5f5f5}.post-type-shop_order .wp-list-table tbody tr:hover:not(.status-trash):not(.no-link) td{cursor:pointer}.post-type-shop_order .wp-list-table .no-link{cursor:default!important}.post-type-shop_order .wp-list-table td,.post-type-shop_order .wp-list-table th{width:12ch;vertical-align:middle}.post-type-shop_order .wp-list-table td p,.post-type-shop_order .wp-list-table th p{margin:0}.post-type-shop_order .wp-list-table .check-column{width:1px;white-space:nowrap;padding:1em 1em 1em 1em!important;vertical-align:middle}.post-type-shop_order .wp-list-table .check-column input{vertical-align:text-top;margin:1px 0}.post-type-shop_order .wp-list-table .column-order_number{width:20ch}.post-type-shop_order .wp-list-table .column-order_total{width:8ch;text-align:left}.post-type-shop_order .wp-list-table .column-order_total a span{float:left}.post-type-shop_order .wp-list-table .column-order_date,.post-type-shop_order .wp-list-table .column-order_status{width:10ch}.post-type-shop_order .wp-list-table .column-order_status{width:14ch}.post-type-shop_order .wp-list-table .column-billing_address,.post-type-shop_order .wp-list-table .column-shipping_address{width:20ch;line-height:1.5em}.post-type-shop_order .wp-list-table .column-billing_address .description,.post-type-shop_order .wp-list-table .column-shipping_address .description{display:block;color:#999}.post-type-shop_order .wp-list-table .column-wc_actions{text-align:left}.post-type-shop_order .wp-list-table .column-wc_actions a.button{text-indent:9999px;margin:2px 4px 2px 0}.post-type-shop_order .wp-list-table .order-preview{float:left;width:16px;padding:20px 4px 4px 4px;height:0;overflow:hidden;position:relative;border:2px solid transparent;border-radius:4px}.post-type-shop_order .wp-list-table .order-preview::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";line-height:16px;font-size:14px;vertical-align:middle;top:4px}.post-type-shop_order .wp-list-table .order-preview:hover{border:2px solid #00a0d2}.post-type-shop_order .wp-list-table .order-preview.disabled::before{content:"";background:url(../images/wpspin-2x.gif) no-repeat center top;background-size:71%}.order-status{display:-webkit-inline-box;display:inline-flex;line-height:2.5em;color:#777;background:#e5e5e5;border-radius:4px;border-bottom:1px solid rgba(0,0,0,.05);margin:-.25em 0;cursor:inherit!important;white-space:nowrap;max-width:100%}.order-status.status-completed{background:#c8d7e1;color:#2e4453}.order-status.status-on-hold{background:#f8dda7;color:#94660c}.order-status.status-failed{background:#eba3a3;color:#761919}.order-status.status-processing{background:#c6e1c6;color:#5b841b}.order-status.status-trash{background:#eba3a3;color:#761919}.order-status>span{margin:0 1em;overflow:hidden;text-overflow:ellipsis}.wc-order-preview .order-status{float:left;margin-left:54px}.wc-order-preview article{padding:0!important}.wc-order-preview .modal-close{border-radius:0}.wc-order-preview .wc-order-preview-table{width:100%;margin:0}.wc-order-preview .wc-order-preview-table td,.wc-order-preview .wc-order-preview-table th{padding:1em 1.5em;text-align:right;border:0;border-bottom:1px solid #eee;margin:0;background:0 0;box-shadow:none;text-align:left;vertical-align:top}.wc-order-preview .wc-order-preview-table td:first-child,.wc-order-preview .wc-order-preview-table th:first-child{text-align:right}.wc-order-preview .wc-order-preview-table th{border-color:#ccc}.wc-order-preview .wc-order-preview-table tr:last-child td{border:0}.wc-order-preview .wc-order-preview-table .wc-order-item-sku{margin-top:.5em}.wc-order-preview .wc-order-preview-table .wc-order-item-meta{margin-top:.5em}.wc-order-preview .wc-order-preview-table .wc-order-item-meta td,.wc-order-preview .wc-order-preview-table .wc-order-item-meta th{padding:0;border:0;text-align:right;vertical-align:top}.wc-order-preview .wc-order-preview-table .wc-order-item-meta td:last-child{padding-right:.5em}.wc-order-preview .wc-order-preview-addresses{overflow:hidden;padding-bottom:1.5em}.wc-order-preview .wc-order-preview-addresses .wc-order-preview-address,.wc-order-preview .wc-order-preview-addresses .wc-order-preview-note{width:50%;float:right;padding:1.5em 1.5em 0;box-sizing:border-box;word-wrap:break-word}.wc-order-preview .wc-order-preview-addresses .wc-order-preview-address h2,.wc-order-preview .wc-order-preview-addresses .wc-order-preview-note h2{margin-top:0}.wc-order-preview .wc-order-preview-addresses .wc-order-preview-address strong,.wc-order-preview .wc-order-preview-addresses .wc-order-preview-note strong{display:block;margin-top:1.5em}.wc-order-preview .wc-order-preview-addresses .wc-order-preview-address strong:first-child,.wc-order-preview .wc-order-preview-addresses .wc-order-preview-note strong:first-child{margin-top:0}.wc-order-preview footer .wc-action-button-group{display:inline-block;float:right}.wc-order-preview footer .button.button-large{margin-right:10px;padding:0 10px!important;line-height:28px;height:auto;display:inline-block}.wc-order-preview .wc-action-button-group label{display:none}.wc-action-button-group{vertical-align:middle;line-height:26px;text-align:right}.wc-action-button-group label{margin-left:6px;cursor:default;font-weight:700;line-height:28px}.wc-action-button-group .wc-action-button-group__items{display:-webkit-inline-box;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;align-content:flex-start;-webkit-box-pack:start;justify-content:flex-start}.wc-action-button-group .wc-action-button{margin:0 -1px 0 0!important;border:1px solid #ccc;padding:0 10px!important;border-radius:0!important;float:none;line-height:28px;height:auto;z-index:1;position:relative;overflow:hidden;text-overflow:ellipsis;-webkit-box-flex:1;flex:1 0 auto;box-sizing:border-box;text-align:center;white-space:nowrap}.wc-action-button-group .wc-action-button:focus,.wc-action-button-group .wc-action-button:hover{border:1px solid #999;z-index:2}.wc-action-button-group .wc-action-button:first-child{margin-right:0!important;border-top-right-radius:3px!important;border-bottom-right-radius:3px!important}.wc-action-button-group .wc-action-button:last-child{border-top-left-radius:3px!important;border-bottom-left-radius:3px!important}@media screen and (max-width:782px){.wc-order-preview footer .wc-action-button-group .wc-action-button-group__items{display:-webkit-box;display:flex}.wc-order-preview footer .wc-action-button-group{float:none;display:block;margin-bottom:4px}.wc-order-preview footer .button.button-large{width:100%;float:none;text-align:center;margin:0;display:block}.post-type-shop_order .wp-list-table td.check-column{width:1em}.post-type-shop_order .wp-list-table td.column-order_number{padding-right:0;padding-bottom:.5em}.post-type-shop_order .wp-list-table td.column-order_date,.post-type-shop_order .wp-list-table td.column-order_status{display:inline-block!important;padding:0 1em 1em 1em!important}.post-type-shop_order .wp-list-table td.column-order_date::before,.post-type-shop_order .wp-list-table td.column-order_status::before{display:none!important}.post-type-shop_order .wp-list-table td.column-order_date{padding-right:0!important}.post-type-shop_order .wp-list-table td.column-order_status{float:left}}.column-customer_message .note-on{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;margin:0 auto;color:#999}.column-customer_message .note-on::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";line-height:16px}.column-order_notes .note-on{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;margin:0 auto;color:#999}.column-order_notes .note-on::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";line-height:16px}.attributes-table td,.attributes-table th{width:15%;vertical-align:top}.attributes-table .attribute-terms{width:32%}.attributes-table .attribute-actions{width:2em}.attributes-table .attribute-actions .configure-terms{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;padding:0!important;height:2em!important;width:2em}.attributes-table .attribute-actions .configure-terms::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";font-family:Dashicons;line-height:1.85}ul.order_notes{padding:2px 0 0}ul.order_notes li .note_content{padding:10px;background:#efefef;position:relative}ul.order_notes li .note_content p{margin:0;padding:0;word-wrap:break-word}ul.order_notes li p.meta{padding:10px;color:#999;margin:0;font-size:11px}ul.order_notes li p.meta .exact-date{border-bottom:1px dotted #999}ul.order_notes li a.delete_note{color:#a00}ul.order_notes li .note_content::after{content:"";display:block;position:absolute;bottom:-10px;right:20px;width:0;height:0;border-width:10px 0 0 10px;border-style:solid;border-color:#efefef transparent}ul.order_notes li.system-note .note_content{background:#d7cad2}ul.order_notes li.system-note .note_content::after{border-color:#d7cad2 transparent}ul.order_notes li.customer-note .note_content{background:#a7cedc}ul.order_notes li.customer-note .note_content::after{border-color:#a7cedc transparent}.add_note{border-top:1px solid #ddd;padding:10px 10px 0}.add_note h4{margin-top:5px!important}.add_note #add_order_note{width:100%;height:50px}table.wp-list-table .column-thumb{width:52px;text-align:center;white-space:nowrap}table.wp-list-table .column-handle{width:17px;display:none}table.wp-list-table tbody td.column-handle{cursor:move;width:17px;text-align:center;vertical-align:text-top}table.wp-list-table tbody td.column-handle::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;height:100%;margin:4px 0 0 0}table.wp-list-table .column-name{width:22%}table.wp-list-table .column-product_cat,table.wp-list-table .column-product_tag{width:11%!important}table.wp-list-table .column-featured,table.wp-list-table .column-product_type{width:48px;text-align:right!important}table.wp-list-table .column-customer_message,table.wp-list-table .column-order_notes{width:48px;text-align:center}table.wp-list-table .column-customer_message img,table.wp-list-table .column-order_notes img{margin:0 auto;padding-top:0!important}table.wp-list-table .manage-column.column-featured img,table.wp-list-table .manage-column.column-product_type img{padding-right:2px}table.wp-list-table .column-price .woocommerce-price-suffix{display:none}table.wp-list-table img{margin:1px 2px}table.wp-list-table .row-actions{color:#999}table.wp-list-table .row-actions span.id{padding-top:8px}table.wp-list-table td.column-thumb img{margin:0;width:auto;height:auto;max-width:40px;max-height:40px;vertical-align:middle}table.wp-list-table span.na{color:#999}table.wp-list-table .column-sku{width:10%}table.wp-list-table .column-price{width:10ch}table.wp-list-table .column-is_in_stock{text-align:right!important;width:12ch}table.wp-list-table span.wc-featured,table.wp-list-table span.wc-image{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;margin:0 auto}table.wp-list-table span.wc-featured::before,table.wp-list-table span.wc-image::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:""}table.wp-list-table span.wc-featured::before{content:"\f155"}table.wp-list-table span.wc-featured.not-featured::before{content:"\f154"}table.wp-list-table td.column-featured span.wc-featured{font-size:1.6em;cursor:pointer}table.wp-list-table mark.instock,table.wp-list-table mark.onbackorder,table.wp-list-table mark.outofstock{font-weight:700;background:transparent none;line-height:1}table.wp-list-table mark.instock{color:#7ad03a}table.wp-list-table mark.outofstock{color:#a44}table.wp-list-table mark.onbackorder{color:#eaa600}table.wp-list-table .notes_head,table.wp-list-table .order-notes_head,table.wp-list-table .status_head{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;margin:0 auto}table.wp-list-table .notes_head::after,table.wp-list-table .order-notes_head::after,table.wp-list-table .status_head::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:""}table.wp-list-table .order-notes_head::after{content:"\e028"}table.wp-list-table .notes_head::after{content:"\e026"}table.wp-list-table .status_head::after{content:"\e011"}table.wp-list-table .column-order_items{width:12%}table.wp-list-table .column-order_items table.order_items{width:100%;margin:3px 0 0;padding:0;display:none}table.wp-list-table .column-order_items table.order_items td{border:0;margin:0;padding:0 0 3px}table.wp-list-table .column-order_items table.order_items td.qty{color:#999;padding-left:6px;text-align:right}mark.notice{background:#fff;color:#a00;margin:0 10px 0 0}a.export_rates,a.import_rates{float:left;margin-right:9px;margin-top:-2px;margin-bottom:0}#rates-search{float:left}#rates-search input.wc-tax-rates-search-field{padding:4px 8px;font-size:1.2em}#rates-pagination{float:left;margin-left:.5em}#rates-pagination .tablenav{margin:0}.wc_input_table_wrapper{overflow-x:auto;display:block}table.wc_input_table,table.wc_tax_rates{width:100%}table.wc_input_table td,table.wc_input_table th,table.wc_tax_rates td,table.wc_tax_rates th{display:table-cell!important}table.wc_input_table span.tips,table.wc_tax_rates span.tips{color:#2ea2cc}table.wc_input_table th,table.wc_tax_rates th{white-space:nowrap;padding:10px}table.wc_input_table td,table.wc_tax_rates td{padding:0;border-left:1px solid #dfdfdf;border-bottom:1px solid #dfdfdf;border-top:0;background:#fff;cursor:default}table.wc_input_table td input[type=number],table.wc_input_table td input[type=text],table.wc_tax_rates td input[type=number],table.wc_tax_rates td input[type=text]{width:100%!important;min-width:100px;padding:8px 10px;margin:0;border:0;outline:0;background:transparent none}table.wc_input_table td input[type=number]:focus,table.wc_input_table td input[type=text]:focus,table.wc_tax_rates td input[type=number]:focus,table.wc_tax_rates td input[type=text]:focus{outline:0;box-shadow:none}table.wc_input_table td.apply_to_shipping,table.wc_input_table td.compound,table.wc_tax_rates td.apply_to_shipping,table.wc_tax_rates td.compound{padding:5px 7px;vertical-align:middle}table.wc_input_table td.apply_to_shipping input,table.wc_input_table td.compound input,table.wc_tax_rates td.apply_to_shipping input,table.wc_tax_rates td.compound input{padding:0}table.wc_input_table td:last-child,table.wc_tax_rates td:last-child{border-left:0}table.wc_input_table tr.current td,table.wc_tax_rates tr.current td{background-color:#fefbcc}table.wc_input_table .cost,table.wc_input_table .item_cost,table.wc_tax_rates .cost,table.wc_tax_rates .item_cost{text-align:left}table.wc_input_table .cost input,table.wc_input_table .item_cost input,table.wc_tax_rates .cost input,table.wc_tax_rates .item_cost input{text-align:left}table.wc_input_table th.sort,table.wc_tax_rates th.sort{width:17px;padding:0 4px}table.wc_input_table td.sort,table.wc_tax_rates td.sort{padding:0 4px}table.wc_input_table .ui-sortable:not(.ui-sortable-disabled) td.sort,table.wc_tax_rates .ui-sortable:not(.ui-sortable-disabled) td.sort{cursor:move;font-size:15px;background:#f9f9f9;text-align:center;vertical-align:middle}table.wc_input_table .ui-sortable:not(.ui-sortable-disabled) td.sort::before,table.wc_tax_rates .ui-sortable:not(.ui-sortable-disabled) td.sort::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;float:right;height:100%}table.wc_input_table .ui-sortable:not(.ui-sortable-disabled) td.sort:hover::before,table.wc_tax_rates .ui-sortable:not(.ui-sortable-disabled) td.sort:hover::before{color:#333}table.wc_input_table .button,table.wc_tax_rates .button{float:right;margin-left:5px}table.wc_input_table .export,table.wc_input_table .import,table.wc_tax_rates .export,table.wc_tax_rates .import{float:left;margin-left:0;margin-right:5px}table.wc_input_table span.tips,table.wc_tax_rates span.tips{padding:0 3px}table.wc_input_table .pagination,table.wc_tax_rates .pagination{float:left}table.wc_input_table .pagination .button,table.wc_tax_rates .pagination .button{margin-right:5px;margin-left:0}table.wc_input_table .pagination .current,table.wc_tax_rates .pagination .current{background:#bbb;text-shadow:none}table.wc_input_table tr:last-child td,table.wc_tax_rates tr:last-child td{border-bottom:0}table.wc_tax_rates td.country{position:relative}table.wc_emails,table.wc_gateways,table.wc_shipping{position:relative}table.wc_emails td,table.wc_emails th,table.wc_gateways td,table.wc_gateways th,table.wc_shipping td,table.wc_shipping th{display:table-cell!important;padding:1em!important;vertical-align:top;line-height:1.75em}table.wc_emails.wc_emails td,table.wc_gateways.wc_emails td,table.wc_shipping.wc_emails td{vertical-align:middle}table.wc_emails tr:nth-child(odd) td,table.wc_gateways tr:nth-child(odd) td,table.wc_shipping tr:nth-child(odd) td{background:#f9f9f9}table.wc_emails td.name,table.wc_gateways td.name,table.wc_shipping td.name{font-weight:700}table.wc_emails .settings,table.wc_gateways .settings,table.wc_shipping .settings{text-align:left}table.wc_emails .default,table.wc_emails .radio,table.wc_emails .status,table.wc_gateways .default,table.wc_gateways .radio,table.wc_gateways .status,table.wc_shipping .default,table.wc_shipping .radio,table.wc_shipping .status{text-align:center}table.wc_emails .default .tips,table.wc_emails .radio .tips,table.wc_emails .status .tips,table.wc_gateways .default .tips,table.wc_gateways .radio .tips,table.wc_gateways .status .tips,table.wc_shipping .default .tips,table.wc_shipping .radio .tips,table.wc_shipping .status .tips{margin:0 auto}table.wc_emails .default input,table.wc_emails .radio input,table.wc_emails .status input,table.wc_gateways .default input,table.wc_gateways .radio input,table.wc_gateways .status input,table.wc_shipping .default input,table.wc_shipping .radio input,table.wc_shipping .status input{margin:0}table.wc_emails td.sort,table.wc_gateways td.sort,table.wc_shipping td.sort{font-size:15px;text-align:center}table.wc_emails td.sort .wc-item-reorder-nav,table.wc_gateways td.sort .wc-item-reorder-nav,table.wc_shipping td.sort .wc-item-reorder-nav{white-space:nowrap;width:72px}table.wc_emails td.sort .wc-item-reorder-nav::before,table.wc_gateways td.sort .wc-item-reorder-nav::before,table.wc_shipping td.sort .wc-item-reorder-nav::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:24px;float:right;height:100%;line-height:24px;cursor:move}table.wc_emails td.sort .wc-item-reorder-nav button,table.wc_gateways td.sort .wc-item-reorder-nav button,table.wc_shipping td.sort .wc-item-reorder-nav button{position:relative;overflow:hidden;float:right;display:block;width:24px;height:24px;margin:0;background:0 0;border:none;box-shadow:none;color:#82878c;text-indent:-9999px;cursor:pointer;outline:0}table.wc_emails td.sort .wc-item-reorder-nav button::before,table.wc_gateways td.sort .wc-item-reorder-nav button::before,table.wc_shipping td.sort .wc-item-reorder-nav button::before{display:inline-block;position:absolute;top:0;left:0;width:100%;height:100%;font:normal 20px/23px dashicons;text-align:center;text-indent:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}table.wc_emails td.sort .wc-item-reorder-nav button:focus,table.wc_emails td.sort .wc-item-reorder-nav button:hover,table.wc_gateways td.sort .wc-item-reorder-nav button:focus,table.wc_gateways td.sort .wc-item-reorder-nav button:hover,table.wc_shipping td.sort .wc-item-reorder-nav button:focus,table.wc_shipping td.sort .wc-item-reorder-nav button:hover{color:#191e23}table.wc_emails td.sort .wc-item-reorder-nav .wc-move-down::before,table.wc_gateways td.sort .wc-item-reorder-nav .wc-move-down::before,table.wc_shipping td.sort .wc-item-reorder-nav .wc-move-down::before{content:"\f347"}table.wc_emails td.sort .wc-item-reorder-nav .wc-move-up::before,table.wc_gateways td.sort .wc-item-reorder-nav .wc-move-up::before,table.wc_shipping td.sort .wc-item-reorder-nav .wc-move-up::before{content:"\f343"}table.wc_emails td.sort .wc-item-reorder-nav .wc-move-disabled,table.wc_gateways td.sort .wc-item-reorder-nav .wc-move-disabled,table.wc_shipping td.sort .wc-item-reorder-nav .wc-move-disabled{color:#d5d5d5!important;cursor:default;pointer-events:none}table.wc_emails .wc-payment-gateway-method-name,table.wc_gateways .wc-payment-gateway-method-name,table.wc_shipping .wc-payment-gateway-method-name{font-weight:400}table.wc_emails .wc-email-settings-table-name,table.wc_gateways .wc-email-settings-table-name,table.wc_shipping .wc-email-settings-table-name{font-weight:700}table.wc_emails .wc-email-settings-table-name span,table.wc_gateways .wc-email-settings-table-name span,table.wc_shipping .wc-email-settings-table-name span{font-weight:400;color:#999;margin:0 4px 0 0!important}table.wc_emails .wc-payment-gateway-method-toggle-disabled,table.wc_emails .wc-payment-gateway-method-toggle-enabled,table.wc_gateways .wc-payment-gateway-method-toggle-disabled,table.wc_gateways .wc-payment-gateway-method-toggle-enabled,table.wc_shipping .wc-payment-gateway-method-toggle-disabled,table.wc_shipping .wc-payment-gateway-method-toggle-enabled{padding-top:1px;display:block;outline:0;box-shadow:none}table.wc_emails .wc-email-settings-table-status,table.wc_gateways .wc-email-settings-table-status,table.wc_shipping .wc-email-settings-table-status{text-align:center;width:1em}table.wc_emails .wc-email-settings-table-status .tips,table.wc_gateways .wc-email-settings-table-status .tips,table.wc_shipping .wc-email-settings-table-status .tips{margin:0 auto}.wc-shipping-zone-settings th{padding:24px 0 24px 24px}.wc-shipping-zone-settings td.forminp input,.wc-shipping-zone-settings td.forminp textarea{padding:8px;max-width:100%!important}.wc-shipping-zone-settings td.forminp .wc-shipping-zone-region-select{width:448px;max-width:100%!important}.wc-shipping-zone-settings td.forminp .wc-shipping-zone-region-select .select2-choices{padding:8px 8px 4px;border-color:#ddd;min-height:0;line-height:1}.wc-shipping-zone-settings td.forminp .wc-shipping-zone-region-select .select2-choices input{padding:0}.wc-shipping-zone-settings td.forminp .wc-shipping-zone-region-select .select2-choices li{margin:0 0 4px 4px}.wc-shipping-zone-settings .wc-shipping-zone-postcodes-toggle{margin:.5em 0 0;font-size:.9em;text-decoration:underline;display:block}.wc-shipping-zone-settings .wc-shipping-zone-postcodes-toggle+.wc-shipping-zone-postcodes{display:none}.wc-shipping-zone-settings .wc-shipping-zone-postcodes textarea{margin:10px 0}.wc-shipping-zone-settings .wc-shipping-zone-postcodes .description{font-size:.9em;color:#999}.wc-shipping-zone-settings+p.submit{margin-top:0}.wc-shipping-zone-settings tbody{display:table-row-group}table tr table.wc-shipping-zone-methods tr .row-actions,table tr:hover table.wc-shipping-zone-methods tr .row-actions{position:relative}table tr table.wc-shipping-zone-methods tr:hover .row-actions,table tr:hover table.wc-shipping-zone-methods tr:hover .row-actions{position:static}.wc-shipping-zones-heading .page-title-action{display:inline-block}table.wc-shipping-classes td,table.wc-shipping-classes th,table.wc-shipping-zone-methods td,table.wc-shipping-zone-methods th,table.wc-shipping-zones td,table.wc-shipping-zones th{vertical-align:top;line-height:24px;padding:1em!important;font-size:14px;background:#fff;display:table-cell!important}table.wc-shipping-classes td li,table.wc-shipping-classes th li,table.wc-shipping-zone-methods td li,table.wc-shipping-zone-methods th li,table.wc-shipping-zones td li,table.wc-shipping-zones th li{line-height:24px;font-size:14px}table.wc-shipping-classes td .woocommerce-help-tip,table.wc-shipping-classes th .woocommerce-help-tip,table.wc-shipping-zone-methods td .woocommerce-help-tip,table.wc-shipping-zone-methods th .woocommerce-help-tip,table.wc-shipping-zones td .woocommerce-help-tip,table.wc-shipping-zones th .woocommerce-help-tip{margin:0!important}table.wc-shipping-classes thead th,table.wc-shipping-zone-methods thead th,table.wc-shipping-zones thead th{vertical-align:middle}table.wc-shipping-classes thead .wc-shipping-zone-sort,table.wc-shipping-zone-methods thead .wc-shipping-zone-sort,table.wc-shipping-zones thead .wc-shipping-zone-sort{text-align:center}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state,table.wc-shipping-classes td.wc-shipping-zones-blank-state,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state,table.wc-shipping-zones td.wc-shipping-zones-blank-state{background:#f7f1f6!important;overflow:hidden;position:relative;padding:7.5em 7.5%!important;border-bottom:2px solid #eee2ec}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-classes td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-zones td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state{padding:2em!important}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-classes td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-zones td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state p{margin-bottom:0}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state li,table.wc-shipping-classes td.wc-shipping-zone-method-blank-state p,table.wc-shipping-classes td.wc-shipping-zones-blank-state li,table.wc-shipping-classes td.wc-shipping-zones-blank-state p,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state li,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state p,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state li,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state p,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state li,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state p,table.wc-shipping-zones td.wc-shipping-zones-blank-state li,table.wc-shipping-zones td.wc-shipping-zones-blank-state p{color:#a46497;font-size:1.5em;line-height:1.5em;margin:0 0 1em;position:relative;z-index:1;text-shadow:-1px 1px 1px #fff}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state li.main,table.wc-shipping-classes td.wc-shipping-zone-method-blank-state p.main,table.wc-shipping-classes td.wc-shipping-zones-blank-state li.main,table.wc-shipping-classes td.wc-shipping-zones-blank-state p.main,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state li.main,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state p.main,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state li.main,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state p.main,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state li.main,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state p.main,table.wc-shipping-zones td.wc-shipping-zones-blank-state li.main,table.wc-shipping-zones td.wc-shipping-zones-blank-state p.main{font-size:2em}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state li,table.wc-shipping-classes td.wc-shipping-zones-blank-state li,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state li,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state li,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state li,table.wc-shipping-zones td.wc-shipping-zones-blank-state li{margin-right:1em;list-style:circle inside}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state::before,table.wc-shipping-classes td.wc-shipping-zones-blank-state::before,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state::before,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state::before,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state::before,table.wc-shipping-zones td.wc-shipping-zones-blank-state::before{content:"\e01b";font-family:WooCommerce;text-align:center;line-height:1;color:#eee2ec;display:block;width:1em;font-size:40em;top:50%;left:-3.75%;margin-top:-.1875em;position:absolute}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state .button-primary,table.wc-shipping-classes td.wc-shipping-zones-blank-state .button-primary,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state .button-primary,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state .button-primary,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state .button-primary,table.wc-shipping-zones td.wc-shipping-zones-blank-state .button-primary{background-color:#804877;border-color:#804877;box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);margin:0;opacity:1;text-shadow:0 -1px 1px #8a4f7f,-1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,1px 0 1px #8a4f7f;font-size:1.5em;padding:.75em 1em;height:auto;position:relative;z-index:1}table.wc-shipping-classes .wc-shipping-zone-method-rows tr:nth-child(even) td,table.wc-shipping-zone-methods .wc-shipping-zone-method-rows tr:nth-child(even) td,table.wc-shipping-zones .wc-shipping-zone-method-rows tr:nth-child(even) td{background:#f9f9f9}table.wc-shipping-classes .wc-shipping-class-rows tr:nth-child(odd) td,table.wc-shipping-classes tr.odd td,table.wc-shipping-zone-methods .wc-shipping-class-rows tr:nth-child(odd) td,table.wc-shipping-zone-methods tr.odd td,table.wc-shipping-zones .wc-shipping-class-rows tr:nth-child(odd) td,table.wc-shipping-zones tr.odd td{background:#f9f9f9}table.wc-shipping-classes tbody.wc-shipping-zone-rows td,table.wc-shipping-zone-methods tbody.wc-shipping-zone-rows td,table.wc-shipping-zones tbody.wc-shipping-zone-rows td{border-top:2px solid #f9f9f9}table.wc-shipping-classes tbody.wc-shipping-zone-rows tr:first-child td,table.wc-shipping-zone-methods tbody.wc-shipping-zone-rows tr:first-child td,table.wc-shipping-zones tbody.wc-shipping-zone-rows tr:first-child td{border-top:0}table.wc-shipping-classes tr.wc-shipping-zone-worldwide td,table.wc-shipping-zone-methods tr.wc-shipping-zone-worldwide td,table.wc-shipping-zones tr.wc-shipping-zone-worldwide td{background:#f9f9f9;border-top:2px solid #e1e1e1}table.wc-shipping-classes p,table.wc-shipping-classes ul,table.wc-shipping-zone-methods p,table.wc-shipping-zone-methods ul,table.wc-shipping-zones p,table.wc-shipping-zones ul{margin:0}table.wc-shipping-classes td.wc-shipping-zone-method-sort,table.wc-shipping-classes td.wc-shipping-zone-sort,table.wc-shipping-zone-methods td.wc-shipping-zone-method-sort,table.wc-shipping-zone-methods td.wc-shipping-zone-sort,table.wc-shipping-zones td.wc-shipping-zone-method-sort,table.wc-shipping-zones td.wc-shipping-zone-sort{cursor:move;font-size:15px;text-align:center}table.wc-shipping-classes td.wc-shipping-zone-method-sort::before,table.wc-shipping-classes td.wc-shipping-zone-sort::before,table.wc-shipping-zone-methods td.wc-shipping-zone-method-sort::before,table.wc-shipping-zone-methods td.wc-shipping-zone-sort::before,table.wc-shipping-zones td.wc-shipping-zone-method-sort::before,table.wc-shipping-zones td.wc-shipping-zone-sort::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;float:right;height:100%;line-height:24px}table.wc-shipping-classes td.wc-shipping-zone-method-sort:hover::before,table.wc-shipping-classes td.wc-shipping-zone-sort:hover::before,table.wc-shipping-zone-methods td.wc-shipping-zone-method-sort:hover::before,table.wc-shipping-zone-methods td.wc-shipping-zone-sort:hover::before,table.wc-shipping-zones td.wc-shipping-zone-method-sort:hover::before,table.wc-shipping-zones td.wc-shipping-zone-sort:hover::before{color:#333}table.wc-shipping-classes td.wc-shipping-zone-worldwide,table.wc-shipping-zone-methods td.wc-shipping-zone-worldwide,table.wc-shipping-zones td.wc-shipping-zone-worldwide{text-align:center}table.wc-shipping-classes td.wc-shipping-zone-worldwide::before,table.wc-shipping-zone-methods td.wc-shipping-zone-worldwide::before,table.wc-shipping-zones td.wc-shipping-zone-worldwide::before{content:"\f319";font-family:dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;float:right;height:100%;line-height:24px}table.wc-shipping-classes .wc-shipping-zone-methods,table.wc-shipping-classes .wc-shipping-zone-name,table.wc-shipping-zone-methods .wc-shipping-zone-methods,table.wc-shipping-zone-methods .wc-shipping-zone-name,table.wc-shipping-zones .wc-shipping-zone-methods,table.wc-shipping-zones .wc-shipping-zone-name{width:25%}table.wc-shipping-classes .wc-shipping-class-description input,table.wc-shipping-classes .wc-shipping-class-description select,table.wc-shipping-classes .wc-shipping-class-description textarea,table.wc-shipping-classes .wc-shipping-class-name input,table.wc-shipping-classes .wc-shipping-class-name select,table.wc-shipping-classes .wc-shipping-class-name textarea,table.wc-shipping-classes .wc-shipping-class-slug input,table.wc-shipping-classes .wc-shipping-class-slug select,table.wc-shipping-classes .wc-shipping-class-slug textarea,table.wc-shipping-classes .wc-shipping-zone-name input,table.wc-shipping-classes .wc-shipping-zone-name select,table.wc-shipping-classes .wc-shipping-zone-name textarea,table.wc-shipping-classes .wc-shipping-zone-region input,table.wc-shipping-classes .wc-shipping-zone-region select,table.wc-shipping-classes .wc-shipping-zone-region textarea,table.wc-shipping-zone-methods .wc-shipping-class-description input,table.wc-shipping-zone-methods .wc-shipping-class-description select,table.wc-shipping-zone-methods .wc-shipping-class-description textarea,table.wc-shipping-zone-methods .wc-shipping-class-name input,table.wc-shipping-zone-methods .wc-shipping-class-name select,table.wc-shipping-zone-methods .wc-shipping-class-name textarea,table.wc-shipping-zone-methods .wc-shipping-class-slug input,table.wc-shipping-zone-methods .wc-shipping-class-slug select,table.wc-shipping-zone-methods .wc-shipping-class-slug textarea,table.wc-shipping-zone-methods .wc-shipping-zone-name input,table.wc-shipping-zone-methods .wc-shipping-zone-name select,table.wc-shipping-zone-methods .wc-shipping-zone-name textarea,table.wc-shipping-zone-methods .wc-shipping-zone-region input,table.wc-shipping-zone-methods .wc-shipping-zone-region select,table.wc-shipping-zone-methods .wc-shipping-zone-region textarea,table.wc-shipping-zones .wc-shipping-class-description input,table.wc-shipping-zones .wc-shipping-class-description select,table.wc-shipping-zones .wc-shipping-class-description textarea,table.wc-shipping-zones .wc-shipping-class-name input,table.wc-shipping-zones .wc-shipping-class-name select,table.wc-shipping-zones .wc-shipping-class-name textarea,table.wc-shipping-zones .wc-shipping-class-slug input,table.wc-shipping-zones .wc-shipping-class-slug select,table.wc-shipping-zones .wc-shipping-class-slug textarea,table.wc-shipping-zones .wc-shipping-zone-name input,table.wc-shipping-zones .wc-shipping-zone-name select,table.wc-shipping-zones .wc-shipping-zone-name textarea,table.wc-shipping-zones .wc-shipping-zone-region input,table.wc-shipping-zones .wc-shipping-zone-region select,table.wc-shipping-zones .wc-shipping-zone-region textarea{width:100%}table.wc-shipping-classes .wc-shipping-class-description a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-class-description a.wc-shipping-zone-delete,table.wc-shipping-classes .wc-shipping-class-name a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-class-name a.wc-shipping-zone-delete,table.wc-shipping-classes .wc-shipping-class-slug a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-class-slug a.wc-shipping-zone-delete,table.wc-shipping-classes .wc-shipping-zone-name a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-zone-name a.wc-shipping-zone-delete,table.wc-shipping-classes .wc-shipping-zone-region a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-zone-region a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-class-description a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-class-description a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-class-name a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-class-name a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-class-slug a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-class-slug a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-zone-name a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-zone-name a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-zone-region a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-zone-region a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-class-description a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-class-description a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-class-name a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-class-name a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-class-slug a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-class-slug a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-zone-name a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-zone-name a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-zone-region a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-zone-region a.wc-shipping-zone-delete{color:#a00}table.wc-shipping-classes .wc-shipping-class-description a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-class-description a.wc-shipping-zone-delete:hover,table.wc-shipping-classes .wc-shipping-class-name a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-class-name a.wc-shipping-zone-delete:hover,table.wc-shipping-classes .wc-shipping-class-slug a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-class-slug a.wc-shipping-zone-delete:hover,table.wc-shipping-classes .wc-shipping-zone-name a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-zone-name a.wc-shipping-zone-delete:hover,table.wc-shipping-classes .wc-shipping-zone-region a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-zone-region a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-description a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-description a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-name a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-name a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-slug a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-slug a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-zone-name a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-zone-name a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-zone-region a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-zone-region a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-class-description a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-class-description a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-class-name a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-class-name a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-class-slug a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-class-slug a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-zone-name a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-zone-name a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-zone-region a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-zone-region a.wc-shipping-zone-delete:hover{color:red}table.wc-shipping-classes .wc-shipping-class-count,table.wc-shipping-zone-methods .wc-shipping-class-count,table.wc-shipping-zones .wc-shipping-class-count{text-align:center}table.wc-shipping-classes td.wc-shipping-zone-methods,table.wc-shipping-zone-methods td.wc-shipping-zone-methods,table.wc-shipping-zones td.wc-shipping-zone-methods{color:#555}table.wc-shipping-classes td.wc-shipping-zone-methods .method_disabled,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .method_disabled,table.wc-shipping-zones td.wc-shipping-zone-methods .method_disabled{text-decoration:line-through}table.wc-shipping-classes td.wc-shipping-zone-methods ul,table.wc-shipping-zone-methods td.wc-shipping-zone-methods ul,table.wc-shipping-zones td.wc-shipping-zone-methods ul{position:relative;padding-left:32px}table.wc-shipping-classes td.wc-shipping-zone-methods ul li,table.wc-shipping-zone-methods td.wc-shipping-zone-methods ul li,table.wc-shipping-zones td.wc-shipping-zone-methods ul li{color:#555;display:inline;margin:0}table.wc-shipping-classes td.wc-shipping-zone-methods ul li::before,table.wc-shipping-zone-methods td.wc-shipping-zone-methods ul li::before,table.wc-shipping-zones td.wc-shipping-zone-methods ul li::before{content:", "}table.wc-shipping-classes td.wc-shipping-zone-methods ul li:first-child::before,table.wc-shipping-zone-methods td.wc-shipping-zone-methods ul li:first-child::before,table.wc-shipping-zones td.wc-shipping-zone-methods ul li:first-child::before{content:""}table.wc-shipping-classes td.wc-shipping-zone-methods .add_shipping_method,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .add_shipping_method,table.wc-shipping-zones td.wc-shipping-zone-methods .add_shipping_method{display:block;width:24px;padding:24px 0 0;height:0;overflow:hidden;cursor:pointer}table.wc-shipping-classes td.wc-shipping-zone-methods .add_shipping_method::before,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .add_shipping_method::before,table.wc-shipping-zones td.wc-shipping-zone-methods .add_shipping_method::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";font-family:Dashicons;content:"\f502";color:#999;vertical-align:middle;line-height:24px;font-size:16px;margin:0}table.wc-shipping-classes td.wc-shipping-zone-methods .add_shipping_method.disabled,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .add_shipping_method.disabled,table.wc-shipping-zones td.wc-shipping-zone-methods .add_shipping_method.disabled{cursor:not-allowed}table.wc-shipping-classes td.wc-shipping-zone-methods .add_shipping_method.disabled::before,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .add_shipping_method.disabled::before,table.wc-shipping-zones td.wc-shipping-zone-methods .add_shipping_method.disabled::before{color:#ccc}table.wc-shipping-classes .wc-shipping-zone-method-title,table.wc-shipping-zone-methods .wc-shipping-zone-method-title,table.wc-shipping-zones .wc-shipping-zone-method-title{width:25%}table.wc-shipping-classes .wc-shipping-zone-method-title .wc-shipping-zone-method-delete,table.wc-shipping-zone-methods .wc-shipping-zone-method-title .wc-shipping-zone-method-delete,table.wc-shipping-zones .wc-shipping-zone-method-title .wc-shipping-zone-method-delete{color:red}table.wc-shipping-classes .wc-shipping-zone-method-enabled,table.wc-shipping-zone-methods .wc-shipping-zone-method-enabled,table.wc-shipping-zones .wc-shipping-zone-method-enabled{text-align:center}table.wc-shipping-classes .wc-shipping-zone-method-enabled a,table.wc-shipping-zone-methods .wc-shipping-zone-method-enabled a,table.wc-shipping-zones .wc-shipping-zone-method-enabled a{display:inline-block}table.wc-shipping-classes .wc-shipping-zone-method-enabled .woocommerce-input-toggle,table.wc-shipping-zone-methods .wc-shipping-zone-method-enabled .woocommerce-input-toggle,table.wc-shipping-zones .wc-shipping-zone-method-enabled .woocommerce-input-toggle{margin-top:3px}table.wc-shipping-classes .wc-shipping-zone-method-type,table.wc-shipping-zone-methods .wc-shipping-zone-method-type,table.wc-shipping-zones .wc-shipping-zone-method-type{display:block}table.wc-shipping-classes tfoot input,table.wc-shipping-classes tfoot select,table.wc-shipping-zone-methods tfoot input,table.wc-shipping-zone-methods tfoot select,table.wc-shipping-zones tfoot input,table.wc-shipping-zones tfoot select{vertical-align:middle!important}table.wc-shipping-classes tfoot .button-secondary,table.wc-shipping-zone-methods tfoot .button-secondary,table.wc-shipping-zones tfoot .button-secondary{float:left}table.wc-shipping-classes .editing .wc-shipping-zone-edit,table.wc-shipping-classes .editing .wc-shipping-zone-view,table.wc-shipping-zone-methods .editing .wc-shipping-zone-edit,table.wc-shipping-zone-methods .editing .wc-shipping-zone-view,table.wc-shipping-zones .editing .wc-shipping-zone-edit,table.wc-shipping-zones .editing .wc-shipping-zone-view{display:none}.woocommerce-input-toggle{height:16px;width:32px;border:2px solid #935687;background-color:#935687;display:inline-block;text-indent:-9999px;border-radius:10em;position:relative;margin-top:-1px;vertical-align:text-top}.woocommerce-input-toggle::before{content:"";display:block;width:16px;height:16px;background:#fff;position:absolute;top:0;left:0;border-radius:100%}.woocommerce-input-toggle.woocommerce-input-toggle--disabled{border-color:#999;background-color:#999}.woocommerce-input-toggle.woocommerce-input-toggle--disabled::before{left:auto;right:0}.woocommerce-input-toggle.woocommerce-input-toggle--loading{opacity:.5}.wc-modal-shipping-method-settings{background:#f8f8f8;padding:1em!important}.wc-modal-shipping-method-settings form .form-table{width:100%;background:#fff;margin:0 0 1.5em}.wc-modal-shipping-method-settings form .form-table tr th{width:30%;position:relative}.wc-modal-shipping-method-settings form .form-table tr th .woocommerce-help-tip{float:left;margin:-8px 0 0 -.5em;vertical-align:middle;left:0;top:50%;position:absolute}.wc-modal-shipping-method-settings form .form-table tr td input,.wc-modal-shipping-method-settings form .form-table tr td select,.wc-modal-shipping-method-settings form .form-table tr td textarea{width:50%;min-width:250px}.wc-modal-shipping-method-settings form .form-table tr td input[type=checkbox]{width:auto;min-width:16px}.wc-modal-shipping-method-settings form .form-table tr td,.wc-modal-shipping-method-settings form .form-table tr th{vertical-align:middle;margin:0;line-height:24px;padding:1em;border-bottom:1px solid #f8f8f8}.wc-modal-shipping-method-settings form .form-table:last-of-type{margin-bottom:0}.wc-backbone-modal .wc-shipping-zone-method-selector p{margin-top:0}.wc-backbone-modal .wc-shipping-zone-method-selector .wc-shipping-zone-method-description{margin:.75em 1px 0;line-height:1.5em;color:#999;font-style:italic}.wc-backbone-modal .wc-shipping-zone-method-selector select{width:100%;cursor:pointer}img.help_tip{margin:0 9px 0 0;vertical-align:middle}.postbox img.help_tip{margin-top:0}.postbox .woocommerce-help-tip{margin:0 9px 0 0}.status-disabled,.status-enabled,.status-manual{font-size:1.4em;display:block;text-indent:-9999px;position:relative;height:1em;width:1em}.status-manual::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#999}.status-enabled::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#a46497}.status-disabled::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#ccc}.woocommerce h2.woo-nav-tab-wrapper{margin-bottom:1em}.woocommerce nav.woo-nav-tab-wrapper{margin:1.5em 0 1em}.woocommerce .subsubsub{margin:-8px 0 0}.woocommerce .wc-admin-breadcrumb{margin-right:.5em}.woocommerce .wc-admin-breadcrumb a{color:#a46497}.woocommerce #template div{margin:0}.woocommerce #template div p .button{float:left;margin-right:10px;margin-top:-4px}.woocommerce #template div .editor textarea{margin-bottom:8px}.woocommerce textarea[disabled=disabled]{background:#dfdfdf!important}.woocommerce table.form-table{margin:0;position:relative;table-layout:fixed}.woocommerce table.form-table .forminp-radio ul{margin:0}.woocommerce table.form-table .forminp-radio ul li{line-height:1.4em}.woocommerce table.form-table input[type=email],.woocommerce table.form-table input[type=number],.woocommerce table.form-table input[type=text]{height:auto}.woocommerce table.form-table textarea.input-text{height:100%;min-width:150px;display:block}.woocommerce table.form-table input.regular-input,.woocommerce table.form-table input[type=date],.woocommerce table.form-table input[type=datetime-local],.woocommerce table.form-table input[type=datetime],.woocommerce table.form-table input[type=email],.woocommerce table.form-table input[type=number],.woocommerce table.form-table input[type=password],.woocommerce table.form-table input[type=tel],.woocommerce table.form-table input[type=text],.woocommerce table.form-table input[type=time],.woocommerce table.form-table input[type=url],.woocommerce table.form-table input[type=week],.woocommerce table.form-table textarea{width:400px;margin:0;padding:6px;box-sizing:border-box;vertical-align:top}.woocommerce table.form-table input[type=date],.woocommerce table.form-table input[type=datetime-local],.woocommerce table.form-table input[type=tel],.woocommerce table.form-table input[type=time],.woocommerce table.form-table input[type=week]{width:200px}.woocommerce table.form-table select{width:400px;margin:0;box-sizing:border-box;line-height:32px;vertical-align:top}.woocommerce table.form-table input[size]{width:auto!important}.woocommerce table.form-table table input.regular-input,.woocommerce table.form-table table input[type=email],.woocommerce table.form-table table input[type=number],.woocommerce table.form-table table input[type=text],.woocommerce table.form-table table select,.woocommerce table.form-table table textarea{width:auto}.woocommerce table.form-table textarea.wide-input{width:100%}.woocommerce table.form-table .woocommerce-help-tip,.woocommerce table.form-table img.help_tip{padding:0;margin:-4px 5px 0 0;vertical-align:middle;cursor:help;line-height:1}.woocommerce table.form-table span.help_tip{cursor:help;color:#2ea2cc}.woocommerce table.form-table th{position:relative;padding-left:24px}.woocommerce table.form-table th label{position:relative;display:block}.woocommerce table.form-table th label .woocommerce-help-tip,.woocommerce table.form-table th label img.help_tip{margin:-8px 0 0 -24px;position:absolute;left:0;top:50%}.woocommerce table.form-table th label+.woocommerce-help-tip{margin:0;position:absolute;left:0;top:20px}.woocommerce table.form-table .select2-container{vertical-align:top;margin-bottom:3px}.woocommerce table.form-table .select2-container+span.description{display:block;margin-top:8px}.woocommerce table.form-table table.widefat th{padding-left:inherit}.woocommerce table.form-table .wp-list-table .woocommerce-help-tip{float:none}.woocommerce table.form-table fieldset{margin-top:4px}.woocommerce table.form-table fieldset .woocommerce-help-tip,.woocommerce table.form-table fieldset img.help_tip{margin:-3px 5px 0 0}.woocommerce table.form-table fieldset p.description{margin-bottom:8px}.woocommerce table.form-table fieldset:first-child{margin-top:0}.woocommerce table.form-table .iris-picker{z-index:100;display:none;position:absolute;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.2)}.woocommerce table.form-table .iris-picker .ui-slider{border:0!important;margin:0!important;width:auto!important;height:auto!important;background:none transparent!important}.woocommerce table.form-table .iris-picker .ui-slider .ui-slider-handle{margin-bottom:0!important}.woocommerce table.form-table .iris-error{background-color:#ffafaf}.woocommerce table.form-table .colorpickpreview{padding:7px 0;line-height:1em;display:inline-block;width:26px;border:1px solid #ddd;font-size:14px}.woocommerce table.form-table .image_width_settings{vertical-align:middle}.woocommerce table.form-table .image_width_settings label{margin-right:10px}.woocommerce table.form-table .image_width_settings input{width:auto}.woocommerce table.form-table .wc_emails_wrapper,.woocommerce table.form-table .wc_payment_gateways_wrapper{padding:0 0 10px 15px}.woocommerce .wc-shipping-zone-settings td.forminp input,.woocommerce .wc-shipping-zone-settings td.forminp textarea{width:448px;padding:6px 11px}.woocommerce .wc-shipping-zone-settings td.forminp .select2-search input{padding:6px}.wc-wp-version-gte-53 .woocommerce h2.wc-table-list-header{margin:1em 0 .35em 0}.wc-wp-version-gte-53 .woocommerce input+.subsubsub{margin:8px 0 0}.wc-wp-version-gte-53 .woocommerce table.form-table input.regular-input,.wc-wp-version-gte-53 .woocommerce table.form-table input[type=date],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=datetime-local],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=datetime],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=email],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=number],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=password],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=tel],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=text],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=time],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=url],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=week],.wc-wp-version-gte-53 .woocommerce table.form-table textarea{padding:0 8px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table input.regular-input,.wc-wp-version-gte-53 .woocommerce table.form-table input[type=date],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=datetime-local],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=datetime],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=email],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=number],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=password],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=tel],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=text],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=time],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=url],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=week],.wc-wp-version-gte-53 .woocommerce table.form-table textarea{width:100%}}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table select{width:100%}}.wc-wp-version-gte-53 .woocommerce table.form-table th label .woocommerce-help-tip,.wc-wp-version-gte-53 .woocommerce table.form-table th label img.help_tip{margin:-7px 0 0 -24px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table th label .woocommerce-help-tip,.wc-wp-version-gte-53 .woocommerce table.form-table th label img.help_tip{left:auto;margin-right:5px}}.wc-wp-version-gte-53 .woocommerce table.form-table .forminp-color{font-size:0}.wc-wp-version-gte-53 .woocommerce table.form-table .colorpickpreview{padding:0;width:30px;height:30px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);font-size:16px;border-radius:4px;margin-left:3px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table .colorpickpreview{float:right;width:40px;height:40px}}.woocommerce #tabs-wrap table a.remove{margin-right:4px}.woocommerce #tabs-wrap table p{margin:0 0 4px!important;overflow:hidden;zoom:1}.woocommerce #tabs-wrap table p a.add{float:right}#wp-excerpt-editor-container{background:#fff}#product_variation-parent #parent_id{width:100%}#postimagediv img{border:1px solid #d5d5d5;max-width:100%}#woocommerce-product-images .inside{margin:0;padding:0}#woocommerce-product-images .inside .add_product_images{padding:0 12px 12px}#woocommerce-product-images .inside #product_images_container{padding:0 9px 0 0}#woocommerce-product-images .inside #product_images_container ul{margin:0;padding:0}#woocommerce-product-images .inside #product_images_container ul::after,#woocommerce-product-images .inside #product_images_container ul::before{content:" ";display:table}#woocommerce-product-images .inside #product_images_container ul::after{clear:both}#woocommerce-product-images .inside #product_images_container ul li.add,#woocommerce-product-images .inside #product_images_container ul li.image,#woocommerce-product-images .inside #product_images_container ul li.wc-metabox-sortable-placeholder{width:80px;float:right;cursor:move;border:1px solid #d5d5d5;margin:9px 0 0 9px;background:#f7f7f7;border-radius:2px;position:relative;box-sizing:border-box}#woocommerce-product-images .inside #product_images_container ul li.add img,#woocommerce-product-images .inside #product_images_container ul li.image img,#woocommerce-product-images .inside #product_images_container ul li.wc-metabox-sortable-placeholder img{width:100%;height:auto;display:block}#woocommerce-product-images .inside #product_images_container ul li.wc-metabox-sortable-placeholder{border:3px dashed #ddd;position:relative}#woocommerce-product-images .inside #product_images_container ul li.wc-metabox-sortable-placeholder::after{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";font-size:2.618em;line-height:72px;color:#ddd}#woocommerce-product-images .inside #product_images_container ul ul.actions{position:absolute;top:-8px;left:-8px;padding:2px;display:none}@media (max-width:768px){#woocommerce-product-images .inside #product_images_container ul ul.actions{display:block}}#woocommerce-product-images .inside #product_images_container ul ul.actions li{float:left;margin:0 2px 0 0}#woocommerce-product-images .inside #product_images_container ul ul.actions li a{width:1em;height:1em;margin:0;height:0;display:block;overflow:hidden}#woocommerce-product-images .inside #product_images_container ul ul.actions li a.tips{cursor:pointer}#woocommerce-product-images .inside #product_images_container ul ul.actions li a.delete{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.4em}#woocommerce-product-images .inside #product_images_container ul ul.actions li a.delete::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#999;background:#fff;border-radius:50%;height:1em;width:1em;line-height:1em}#woocommerce-product-images .inside #product_images_container ul ul.actions li a.delete:hover::before{color:#a00}#woocommerce-product-images .inside #product_images_container ul li:hover ul.actions{display:block}#woocommerce-product-data .hndle{padding:10px}#woocommerce-product-data .hndle span{display:block;line-height:24px}#woocommerce-product-data .hndle .type_box{display:inline;line-height:inherit;vertical-align:baseline}#woocommerce-product-data .hndle select{margin:0}#woocommerce-product-data .hndle label{padding-left:1em;font-size:12px;vertical-align:baseline}#woocommerce-product-data .hndle label:first-child{margin-left:1em;border-left:1px solid #dfdfdf}#woocommerce-product-data .hndle input,#woocommerce-product-data .hndle select{margin-top:-3px 0 0;vertical-align:middle}#woocommerce-product-data .hndle select{margin-right:.5em}#woocommerce-product-data>.handlediv{margin-top:4px}#woocommerce-product-data .wrap{margin:0}#woocommerce-coupon-description{padding:3px 8px;font-size:1.7em;line-height:1.42em;height:auto;width:100%;outline:0;margin:10px 0;display:block}#woocommerce-coupon-description::-webkit-input-placeholder{line-height:1.42em;color:#bbb}#woocommerce-coupon-description::-moz-placeholder{line-height:1.42em;color:#bbb}#woocommerce-coupon-description:-ms-input-placeholder{line-height:1.42em;color:#bbb}#woocommerce-coupon-description:-moz-placeholder{line-height:1.42em;color:#bbb}#woocommerce-coupon-data .panel-wrap,#woocommerce-product-data .panel-wrap{background:#fff}#woocommerce-coupon-data .wc-metaboxes-wrapper,#woocommerce-coupon-data .woocommerce_options_panel,#woocommerce-product-data .wc-metaboxes-wrapper,#woocommerce-product-data .woocommerce_options_panel{float:right;width:80%}#woocommerce-coupon-data .wc-metaboxes-wrapper .wc-radios,#woocommerce-coupon-data .woocommerce_options_panel .wc-radios,#woocommerce-product-data .wc-metaboxes-wrapper .wc-radios,#woocommerce-product-data .woocommerce_options_panel .wc-radios{display:block;float:right;margin:0}#woocommerce-coupon-data .wc-metaboxes-wrapper .wc-radios li,#woocommerce-coupon-data .woocommerce_options_panel .wc-radios li,#woocommerce-product-data .wc-metaboxes-wrapper .wc-radios li,#woocommerce-product-data .woocommerce_options_panel .wc-radios li{display:block;padding:0 0 10px}#woocommerce-coupon-data .wc-metaboxes-wrapper .wc-radios li input,#woocommerce-coupon-data .woocommerce_options_panel .wc-radios li input,#woocommerce-product-data .wc-metaboxes-wrapper .wc-radios li input,#woocommerce-product-data .woocommerce_options_panel .wc-radios li input{width:auto}#woocommerce-coupon-data .panel-wrap,#woocommerce-product-data .panel-wrap,.woocommerce .panel-wrap{overflow:hidden}#woocommerce-coupon-data ul.wc-tabs,#woocommerce-product-data ul.wc-tabs,.woocommerce ul.wc-tabs{margin:0;width:20%;float:right;line-height:1em;padding:0 0 10px;position:relative;background-color:#fafafa;border-left:1px solid #eee;box-sizing:border-box}#woocommerce-coupon-data ul.wc-tabs::after,#woocommerce-product-data ul.wc-tabs::after,.woocommerce ul.wc-tabs::after{content:"";display:block;width:100%;height:9999em;position:absolute;bottom:-9999em;right:0;background-color:#fafafa;border-left:1px solid #eee}#woocommerce-coupon-data ul.wc-tabs li,#woocommerce-product-data ul.wc-tabs li,.woocommerce ul.wc-tabs li{margin:0;padding:0;display:block;position:relative}#woocommerce-coupon-data ul.wc-tabs li a,#woocommerce-product-data ul.wc-tabs li a,.woocommerce ul.wc-tabs li a{margin:0;padding:10px;display:block;box-shadow:none;text-decoration:none;line-height:20px!important;border-bottom:1px solid #eee}#woocommerce-coupon-data ul.wc-tabs li a span,#woocommerce-product-data ul.wc-tabs li a span,.woocommerce ul.wc-tabs li a span{margin-right:.618em;margin-left:.618em}#woocommerce-coupon-data ul.wc-tabs li a::before,#woocommerce-product-data ul.wc-tabs li a::before,.woocommerce ul.wc-tabs li a::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;content:"";text-decoration:none}#woocommerce-coupon-data ul.wc-tabs li.general_options a::before,#woocommerce-product-data ul.wc-tabs li.general_options a::before,.woocommerce ul.wc-tabs li.general_options a::before{content:"\f107"}#woocommerce-coupon-data ul.wc-tabs li.inventory_options a::before,#woocommerce-product-data ul.wc-tabs li.inventory_options a::before,.woocommerce ul.wc-tabs li.inventory_options a::before{content:"\f481"}#woocommerce-coupon-data ul.wc-tabs li.shipping_options a::before,#woocommerce-product-data ul.wc-tabs li.shipping_options a::before,.woocommerce ul.wc-tabs li.shipping_options a::before{font-family:WooCommerce;content:"\e01a"}#woocommerce-coupon-data ul.wc-tabs li.linked_product_options a::before,#woocommerce-product-data ul.wc-tabs li.linked_product_options a::before,.woocommerce ul.wc-tabs li.linked_product_options a::before{content:"\f103"}#woocommerce-coupon-data ul.wc-tabs li.attribute_options a::before,#woocommerce-product-data ul.wc-tabs li.attribute_options a::before,.woocommerce ul.wc-tabs li.attribute_options a::before{content:"\f175"}#woocommerce-coupon-data ul.wc-tabs li.advanced_options a::before,#woocommerce-product-data ul.wc-tabs li.advanced_options a::before,.woocommerce ul.wc-tabs li.advanced_options a::before{font-family:Dashicons;content:"\f111"}#woocommerce-coupon-data ul.wc-tabs li.marketplace-suggestions_options a::before,#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_options a::before,.woocommerce ul.wc-tabs li.marketplace-suggestions_options a::before{content:none}#woocommerce-coupon-data ul.wc-tabs li.variations_options a::before,#woocommerce-product-data ul.wc-tabs li.variations_options a::before,.woocommerce ul.wc-tabs li.variations_options a::before{content:"\f509"}#woocommerce-coupon-data ul.wc-tabs li.usage_restriction_options a::before,#woocommerce-product-data ul.wc-tabs li.usage_restriction_options a::before,.woocommerce ul.wc-tabs li.usage_restriction_options a::before{font-family:WooCommerce;content:"\e602"}#woocommerce-coupon-data ul.wc-tabs li.usage_limit_options a::before,#woocommerce-product-data ul.wc-tabs li.usage_limit_options a::before,.woocommerce ul.wc-tabs li.usage_limit_options a::before{font-family:WooCommerce;content:"\e601"}#woocommerce-coupon-data ul.wc-tabs li.general_coupon_data a::before,#woocommerce-product-data ul.wc-tabs li.general_coupon_data a::before,.woocommerce ul.wc-tabs li.general_coupon_data a::before{font-family:WooCommerce;content:"\e600"}#woocommerce-coupon-data ul.wc-tabs li.active a,#woocommerce-product-data ul.wc-tabs li.active a,.woocommerce ul.wc-tabs li.active a{color:#555;position:relative;background-color:#eee}.woocommerce_page_wc-settings input[type=email],.woocommerce_page_wc-settings input[type=url]{direction:rtl}.woocommerce_page_wc-settings .shippingrows th.check-column{padding-top:20px}.woocommerce_page_wc-settings .shippingrows tfoot th{padding-right:10px}.woocommerce_page_wc-settings .shippingrows .add.button::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin-left:.618em;content:"";text-decoration:none}.woocommerce_page_wc-settings h3.wc-settings-sub-title{font-size:1.2em}#woocommerce-coupon-data .inside,#woocommerce-order-data .inside,#woocommerce-order-downloads .inside,#woocommerce-product-data .inside,#woocommerce-product-type-options .inside{margin:0;padding:0}.panel,.woocommerce_options_panel{padding:9px;color:#555}.panel .form-field .woocommerce-help-tip,.woocommerce_options_panel .form-field .woocommerce-help-tip{font-size:1.4em}.panel,.woocommerce_page_settings .woocommerce_options_panel{padding:0}#woocommerce-product-specs .inside,#woocommerce-product-type-options .panel{margin:0;padding:9px}#woocommerce-product-type-options .panel p,.woocommerce_options_panel fieldset.form-field,.woocommerce_options_panel p{margin:0 0 9px;font-size:12px;padding:5px 9px;line-height:24px}#woocommerce-product-type-options .panel p::after,.woocommerce_options_panel fieldset.form-field::after,.woocommerce_options_panel p::after{content:".";display:block;height:0;clear:both;visibility:hidden}.woocommerce_options_panel .checkbox,.woocommerce_variable_attributes .checkbox{margin:4px 0!important;vertical-align:middle;float:right}.woocommerce_options_panel .downloadable_files table,.woocommerce_variations .downloadable_files table{width:100%;padding:0!important}.woocommerce_options_panel .downloadable_files table th,.woocommerce_variations .downloadable_files table th{padding:7px 7px 7px 0!important}.woocommerce_options_panel .downloadable_files table th.sort,.woocommerce_variations .downloadable_files table th.sort{width:17px;padding:7px!important}.woocommerce_options_panel .downloadable_files table th .woocommerce-help-tip,.woocommerce_variations .downloadable_files table th .woocommerce-help-tip{font-size:1.1em;margin-right:0}.woocommerce_options_panel .downloadable_files table td,.woocommerce_variations .downloadable_files table td{vertical-align:middle!important;padding:4px 7px 4px 0!important;position:relative}.woocommerce_options_panel .downloadable_files table td:last-child,.woocommerce_variations .downloadable_files table td:last-child{padding-left:7px!important}.woocommerce_options_panel .downloadable_files table td input.input_text,.woocommerce_variations .downloadable_files table td input.input_text{width:100%;float:none;min-width:0;margin:1px 0}.woocommerce_options_panel .downloadable_files table td .upload_file_button,.woocommerce_variations .downloadable_files table td .upload_file_button{width:auto;float:left;cursor:pointer}.woocommerce_options_panel .downloadable_files table td .delete,.woocommerce_variations .downloadable_files table td .delete{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.2em}.woocommerce_options_panel .downloadable_files table td .delete::before,.woocommerce_variations .downloadable_files table td .delete::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#999}.woocommerce_options_panel .downloadable_files table td .delete:hover::before,.woocommerce_variations .downloadable_files table td .delete:hover::before{color:#a00}.woocommerce_options_panel .downloadable_files table td.sort,.woocommerce_variations .downloadable_files table td.sort{width:17px;cursor:move;font-size:15px;text-align:center;background:#f9f9f9;padding-left:7px!important}.woocommerce_options_panel .downloadable_files table td.sort::before,.woocommerce_variations .downloadable_files table td.sort::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;float:right;height:100%}.woocommerce_options_panel .downloadable_files table td.sort:hover::before,.woocommerce_variations .downloadable_files table td.sort:hover::before{color:#333}.woocommerce_attribute h3 .sort,.woocommerce_variation h3 .sort{width:17px;height:26px;cursor:move;float:left;font-size:15px;font-weight:400;margin-left:.5em;visibility:hidden;text-align:center;vertical-align:middle}.woocommerce_attribute h3 .sort::before,.woocommerce_variation h3 .sort::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:28px;color:#999;display:block;width:17px;float:right;height:100%}.woocommerce_attribute h3 .sort:hover::before,.woocommerce_variation h3 .sort:hover::before{color:#777}.woocommerce_attribute h3:hover .sort,.woocommerce_attribute.ui-sortable-helper .sort,.woocommerce_variation h3:hover .sort,.woocommerce_variation.ui-sortable-helper .sort{visibility:visible}.woocommerce_options_panel{min-height:175px;box-sizing:border-box}.woocommerce_options_panel .downloadable_files{padding:0 162px 0 9px;position:relative;margin:9px 0}.woocommerce_options_panel .downloadable_files label{position:absolute;right:0;margin:0 12px 0 0;line-height:24px}.woocommerce_options_panel p{margin:9px 0}.woocommerce_options_panel fieldset.form-field,.woocommerce_options_panel p.form-field{padding:5px 162px 5px 20px!important}.woocommerce_options_panel .sale_price_dates_fields .short:first-of-type{margin-bottom:1em}.woocommerce_options_panel .sale_price_dates_fields .short:nth-of-type(2){clear:right}.woocommerce_options_panel label,.woocommerce_options_panel legend{float:right;width:150px;padding:0;margin:0 -150px 0 0}.woocommerce_options_panel label .req,.woocommerce_options_panel legend .req{font-weight:700;font-style:normal;color:#a00}.woocommerce_options_panel .description{padding:0;margin:0 7px 0 0;clear:none;display:inline}.woocommerce_options_panel .description-block{margin-right:0;display:block}.woocommerce_options_panel input,.woocommerce_options_panel select,.woocommerce_options_panel textarea{margin:0}.woocommerce_options_panel textarea{float:right;height:3.5em;line-height:1.5em;vertical-align:top}.woocommerce_options_panel input[type=email],.woocommerce_options_panel input[type=number],.woocommerce_options_panel input[type=password],.woocommerce_options_panel input[type=text]{width:50%;float:right}.woocommerce_options_panel input.button{width:auto;margin-right:8px}.woocommerce_options_panel select{float:right}.woocommerce_options_panel .short,.woocommerce_options_panel input[type=email].short,.woocommerce_options_panel input[type=number].short,.woocommerce_options_panel input[type=password].short,.woocommerce_options_panel input[type=text].short{width:50%}.woocommerce_options_panel .sized{width:auto!important;margin-left:6px}.woocommerce_options_panel .options_group{border-top:1px solid #fff;border-bottom:1px solid #eee}.woocommerce_options_panel .options_group:first-child{border-top:0}.woocommerce_options_panel .options_group:last-child{border-bottom:0}.woocommerce_options_panel .options_group fieldset{margin:9px 0;font-size:12px;padding:5px 9px;line-height:24px}.woocommerce_options_panel .options_group fieldset label{width:auto;float:none}.woocommerce_options_panel .options_group fieldset ul{float:right;width:50%;margin:0;padding:0}.woocommerce_options_panel .options_group fieldset ul li{margin:0;width:auto}.woocommerce_options_panel .options_group fieldset ul li input{width:auto;float:none;margin-left:4px}.woocommerce_options_panel .options_group fieldset ul.wc-radios label{margin-right:0}.woocommerce_options_panel .dimensions_field .wrap{display:block;width:50%}.woocommerce_options_panel .dimensions_field .wrap input{width:30.75%;margin-left:3.8%}.woocommerce_options_panel .dimensions_field .wrap .last{margin-left:0}.woocommerce_options_panel.padded{padding:1em}.woocommerce_options_panel .select2-container{float:right}#woocommerce-product-data input.dp-applied{float:right}#grouped_product_options,#simple_product_options,#virtual_product_options{padding:12px;font-style:italic;color:#666}.wc-metaboxes-wrapper .toolbar{margin:0!important;border-top:1px solid #fff;border-bottom:1px solid #eee;padding:9px 12px!important}.wc-metaboxes-wrapper .toolbar:first-child{border-top:0}.wc-metaboxes-wrapper .toolbar:last-child{border-bottom:0}.wc-metaboxes-wrapper .toolbar .add_variation{float:left;margin-right:5px}.wc-metaboxes-wrapper .toolbar .cancel-variation-changes,.wc-metaboxes-wrapper .toolbar .save-variation-changes{float:right;margin-left:5px}.wc-metaboxes-wrapper p.toolbar{overflow:hidden;zoom:1}.wc-metaboxes-wrapper .expand-close{margin-left:2px;color:#777;font-size:12px;font-style:italic}.wc-metaboxes-wrapper .expand-close a{background:0 0;padding:0;font-size:12px;text-decoration:none}.wc-metaboxes-wrapper#product_attributes .expand-close{float:left;line-height:28px}.wc-metaboxes-wrapper .fr,.wc-metaboxes-wrapper button.add_variable_attribute{float:left;margin:0 6px 0 0}.wc-metaboxes-wrapper .wc-metaboxes{border-bottom:1px solid #eee}.wc-metaboxes-wrapper .wc-metabox-sortable-placeholder{border-color:#bbb;background-color:#f5f5f5;margin-bottom:9px;border-width:1px;border-style:dashed}.wc-metaboxes-wrapper .wc-metabox{background:#fff;border-bottom:1px solid #eee;margin:0!important}.wc-metaboxes-wrapper .wc-metabox select{font-weight:400}.wc-metaboxes-wrapper .wc-metabox:last-of-type{border-bottom:0}.wc-metaboxes-wrapper .wc-metabox .handlediv{width:27px;float:left}.wc-metaboxes-wrapper .wc-metabox .handlediv::before{content:"\f142"!important;cursor:pointer;display:inline-block;font:400 20px/1 Dashicons;line-height:.5!important;padding:8px 10px;position:relative;left:12px;top:0}.wc-metaboxes-wrapper .wc-metabox.closed{border-radius:3px}.wc-metaboxes-wrapper .wc-metabox.closed .handlediv::before{content:"\f140"!important}.wc-metaboxes-wrapper .wc-metabox.closed h3{border:0}.wc-metaboxes-wrapper .wc-metabox h3{margin:0!important;padding:.75em 1em .75em .75em!important;font-size:1em!important;overflow:hidden;zoom:1;cursor:move}.wc-metaboxes-wrapper .wc-metabox h3 a.delete,.wc-metaboxes-wrapper .wc-metabox h3 button{float:left}.wc-metaboxes-wrapper .wc-metabox h3 a.delete{color:red;font-weight:400;line-height:26px;text-decoration:none;position:relative;visibility:hidden}.wc-metaboxes-wrapper .wc-metabox h3 strong{font-weight:400;line-height:26px;font-weight:700}.wc-metaboxes-wrapper .wc-metabox h3 select{font-family:sans-serif;max-width:20%;margin:.25em 0 .25em .25em}.wc-metaboxes-wrapper .wc-metabox h3 .handlediv{background-position:6px 5px!important;visibility:hidden;height:26px}.wc-metaboxes-wrapper .wc-metabox h3.fixed{cursor:pointer!important}.wc-metaboxes-wrapper .wc-metabox.woocommerce_attribute h3,.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation h3{cursor:pointer;padding:.5em 1em .5em .75em!important}.wc-metaboxes-wrapper .wc-metabox.woocommerce_attribute h3 .handlediv,.wc-metaboxes-wrapper .wc-metabox.woocommerce_attribute h3 .sort,.wc-metaboxes-wrapper .wc-metabox.woocommerce_attribute h3 a.delete,.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation h3 .handlediv,.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation h3 .sort,.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation h3 a.delete{margin-top:.25em}.wc-metaboxes-wrapper .wc-metabox h3:hover .handlediv,.wc-metaboxes-wrapper .wc-metabox h3:hover a.delete,.wc-metaboxes-wrapper .wc-metabox.ui-sortable-helper .handlediv,.wc-metaboxes-wrapper .wc-metabox.ui-sortable-helper a.delete{visibility:visible}.wc-metaboxes-wrapper .wc-metabox table{width:100%;position:relative;background-color:#fdfdfd;padding:1em;border-top:1px solid #eee}.wc-metaboxes-wrapper .wc-metabox table td{text-align:right;padding:0 0 1em 6px;vertical-align:top;border:0}.wc-metaboxes-wrapper .wc-metabox table td label{text-align:right;display:block;line-height:21px}.wc-metaboxes-wrapper .wc-metabox table td input{float:right;min-width:200px}.wc-metaboxes-wrapper .wc-metabox table td input,.wc-metaboxes-wrapper .wc-metabox table td textarea{width:100%;margin:0;display:block;font-size:14px;padding:4px;color:#555}.wc-metaboxes-wrapper .wc-metabox table td .select2-container,.wc-metaboxes-wrapper .wc-metabox table td select{width:100%!important}.wc-metaboxes-wrapper .wc-metabox table td input.short{width:200px}.wc-metaboxes-wrapper .wc-metabox table td input.checkbox{width:16px;min-width:inherit;vertical-align:text-bottom;display:inline-block;float:none}.wc-metaboxes-wrapper .wc-metabox table td.attribute_name{width:200px}.wc-metaboxes-wrapper .wc-metabox table .minus,.wc-metaboxes-wrapper .wc-metabox table .plus{margin-top:6px}.wc-metaboxes-wrapper .wc-metabox table .fl{float:right}.wc-metaboxes-wrapper .wc-metabox table .fr{float:left}.variations-pagenav{float:left;line-height:24px}.variations-pagenav .displaying-num{color:#777;font-size:12px;font-style:italic}.variations-pagenav a{padding:0 10px 3px;background:rgba(0,0,0,.05);font-size:16px;font-weight:400;text-decoration:none}.variations-pagenav a.disabled,.variations-pagenav a.disabled:active,.variations-pagenav a.disabled:focus,.variations-pagenav a.disabled:hover{color:#a0a5aa;background:rgba(0,0,0,.05)}.variations-defaults{float:right}.variations-defaults select{margin:.25em 0 .25em .25em}.woocommerce_variable_attributes{background-color:#fdfdfd;border-top:1px solid #eee}.woocommerce_variable_attributes .data{padding:1em 2em}.woocommerce_variable_attributes .data::after,.woocommerce_variable_attributes .data::before{content:" ";display:table}.woocommerce_variable_attributes .data::after{clear:both}.woocommerce_variable_attributes .upload_image_button{display:block;width:64px;height:64px;float:right;margin-left:20px;position:relative;cursor:pointer}.woocommerce_variable_attributes .upload_image_button img{width:100%;height:auto;display:none}.woocommerce_variable_attributes .upload_image_button::before{content:"\f128";font-family:Dashicons;position:absolute;top:0;right:0;left:0;bottom:0;text-align:center;line-height:64px;font-size:64px;font-weight:400;-webkit-font-smoothing:antialiased}.woocommerce_variable_attributes .upload_image_button.remove img{display:block}.woocommerce_variable_attributes .upload_image_button.remove::before{content:"\f335";display:none}.woocommerce_variable_attributes .upload_image_button.remove:hover::before{display:block}.woocommerce_variable_attributes .options{border:1px solid #eee;border-width:1px 0;padding:.25em 0}.woocommerce_variable_attributes .options label{display:inline-block;padding:4px 0 2px 1em}.woocommerce_variable_attributes .options input[type=checkbox]{margin:0 .5em 0 5px!important;vertical-align:middle}.form-row label{display:inline-block}.form-row .woocommerce-help-tip{float:left}.form-row input[type=color],.form-row input[type=date],.form-row input[type=datetime-local],.form-row input[type=datetime],.form-row input[type=email],.form-row input[type=month],.form-row input[type=number],.form-row input[type=password],.form-row input[type=search],.form-row input[type=tel],.form-row input[type=text],.form-row input[type=time],.form-row input[type=url],.form-row input[type=week],.form-row select,.form-row textarea{width:100%;vertical-align:middle;margin:2px 0 0;padding:5px}.form-row select{height:40px}.form-row.dimensions_field .wrap{clear:right;display:block}.form-row.dimensions_field input{width:33%;float:right;vertical-align:middle}.form-row.dimensions_field input:last-of-type{margin-left:0;width:34%}.form-row.form-row-first,.form-row.form-row-last{width:48%;float:left}.form-row.form-row-first{clear:both;float:right}.form-row.form-row-full{clear:both}.tips{cursor:help;text-decoration:none}img.tips{padding:5px 0 0}#tiptip_holder{display:none;z-index:8675309;position:absolute;top:0;left:0}#tiptip_holder.tip_top{padding-bottom:5px}#tiptip_holder.tip_top #tiptip_arrow_inner{margin-top:-7px;margin-right:-6px;border-top-color:#333}#tiptip_holder.tip_bottom{padding-top:5px}#tiptip_holder.tip_bottom #tiptip_arrow_inner{margin-top:-5px;margin-right:-6px;border-bottom-color:#333}#tiptip_holder.tip_right{padding-right:5px}#tiptip_holder.tip_right #tiptip_arrow_inner{margin-top:-6px;margin-right:-5px;border-left-color:#333}#tiptip_holder.tip_left{padding-left:5px}#tiptip_holder.tip_left #tiptip_arrow_inner{margin-top:-6px;margin-right:-7px;border-right-color:#333}#tiptip_content,.chart-tooltip,.wc_error_tip{color:#fff;font-size:.8em;max-width:150px;background:#333;text-align:center;border-radius:3px;padding:.618em 1em;box-shadow:0 1px 3px rgba(0,0,0,.2)}#tiptip_content code,.chart-tooltip code,.wc_error_tip code{padding:1px;background:#888}#tiptip_arrow,#tiptip_arrow_inner{position:absolute;border-color:transparent;border-style:solid;border-width:6px;height:0;width:0}#tiptip_arrow{right:50%;margin-right:-6px}.wc_error_tip{max-width:20em;line-height:1.8em;position:absolute;white-space:normal;background:#d82223;margin:1.5em -1em 0 1px;z-index:9999999}.wc_error_tip::after{content:"";display:block;border:8px solid #d82223;border-left-color:transparent;border-right-color:transparent;border-top-color:transparent;position:absolute;top:-3px;right:50%;margin:-1em -3px 0 0}img.ui-datepicker-trigger{vertical-align:middle;margin-top:-1px;cursor:pointer}.wc-metabox-content img.ui-datepicker-trigger,.woocommerce_options_panel img.ui-datepicker-trigger{float:right;margin-left:8px;margin-top:4px;margin-right:4px}#ui-datepicker-div{display:none}.woocommerce-reports-remove-filter{color:red;text-decoration:none}.woocommerce-reports-wide.woocommerce-reports-wrap,.woocommerce-reports-wrap.woocommerce-reports-wrap{margin-right:300px;padding-top:18px}.woocommerce-reports-wide.halved,.woocommerce-reports-wrap.halved{margin:0;overflow:hidden;zoom:1}.woocommerce-reports-wide .widefat th,.woocommerce-reports-wrap .widefat th{padding:7px}.woocommerce-reports-wide .widefat td,.woocommerce-reports-wrap .widefat td{vertical-align:top;padding:7px}.woocommerce-reports-wide .widefat td .description,.woocommerce-reports-wrap .widefat td .description{margin:4px 0 0}.woocommerce-reports-wide .postbox::after,.woocommerce-reports-wrap .postbox::after{content:".";display:block;height:0;clear:both;visibility:hidden}.woocommerce-reports-wide .postbox h3,.woocommerce-reports-wrap .postbox h3{cursor:default!important}.woocommerce-reports-wide .postbox .inside,.woocommerce-reports-wrap .postbox .inside{padding:10px;margin:0!important}.woocommerce-reports-wide .postbox div.stats_range,.woocommerce-reports-wide .postbox h3.stats_range,.woocommerce-reports-wrap .postbox div.stats_range,.woocommerce-reports-wrap .postbox h3.stats_range{border-bottom-color:#dfdfdf;margin:0;padding:0!important}.woocommerce-reports-wide .postbox div.stats_range .export_csv,.woocommerce-reports-wide .postbox h3.stats_range .export_csv,.woocommerce-reports-wrap .postbox div.stats_range .export_csv,.woocommerce-reports-wrap .postbox h3.stats_range .export_csv{float:left;line-height:26px;border-right:1px solid #dfdfdf;padding:10px;display:block;text-decoration:none}.woocommerce-reports-wide .postbox div.stats_range .export_csv::before,.woocommerce-reports-wide .postbox h3.stats_range .export_csv::before,.woocommerce-reports-wrap .postbox div.stats_range .export_csv::before,.woocommerce-reports-wrap .postbox h3.stats_range .export_csv::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;content:"";text-decoration:none;margin-left:4px}.woocommerce-reports-wide .postbox div.stats_range ul,.woocommerce-reports-wide .postbox h3.stats_range ul,.woocommerce-reports-wrap .postbox div.stats_range ul,.woocommerce-reports-wrap .postbox h3.stats_range ul{list-style:none outside;margin:0;padding:0;zoom:1;background:#f5f5f5;border-bottom:1px solid #ccc}.woocommerce-reports-wide .postbox div.stats_range ul::after,.woocommerce-reports-wide .postbox div.stats_range ul::before,.woocommerce-reports-wide .postbox h3.stats_range ul::after,.woocommerce-reports-wide .postbox h3.stats_range ul::before,.woocommerce-reports-wrap .postbox div.stats_range ul::after,.woocommerce-reports-wrap .postbox div.stats_range ul::before,.woocommerce-reports-wrap .postbox h3.stats_range ul::after,.woocommerce-reports-wrap .postbox h3.stats_range ul::before{content:" ";display:table}.woocommerce-reports-wide .postbox div.stats_range ul::after,.woocommerce-reports-wide .postbox h3.stats_range ul::after,.woocommerce-reports-wrap .postbox div.stats_range ul::after,.woocommerce-reports-wrap .postbox h3.stats_range ul::after{clear:both}.woocommerce-reports-wide .postbox div.stats_range ul li,.woocommerce-reports-wide .postbox h3.stats_range ul li,.woocommerce-reports-wrap .postbox div.stats_range ul li,.woocommerce-reports-wrap .postbox h3.stats_range ul li{float:right;margin:0;padding:0;line-height:26px;font-weight:700;font-size:14px}.woocommerce-reports-wide .postbox div.stats_range ul li a,.woocommerce-reports-wide .postbox h3.stats_range ul li a,.woocommerce-reports-wrap .postbox div.stats_range ul li a,.woocommerce-reports-wrap .postbox h3.stats_range ul li a{border-left:1px solid #dfdfdf;padding:10px;display:block;text-decoration:none}.woocommerce-reports-wide .postbox div.stats_range ul li.active,.woocommerce-reports-wide .postbox h3.stats_range ul li.active,.woocommerce-reports-wrap .postbox div.stats_range ul li.active,.woocommerce-reports-wrap .postbox h3.stats_range ul li.active{background:#fff;box-shadow:0 4px 0 0 #fff}.woocommerce-reports-wide .postbox div.stats_range ul li.active a,.woocommerce-reports-wide .postbox h3.stats_range ul li.active a,.woocommerce-reports-wrap .postbox div.stats_range ul li.active a,.woocommerce-reports-wrap .postbox h3.stats_range ul li.active a{color:#777}.woocommerce-reports-wide .postbox div.stats_range ul li.custom,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom{padding:9px 10px;vertical-align:middle}.woocommerce-reports-wide .postbox div.stats_range ul li.custom div,.woocommerce-reports-wide .postbox div.stats_range ul li.custom form,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom div,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom form,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom div,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom form,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom div,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom form{display:inline;margin:0}.woocommerce-reports-wide .postbox div.stats_range ul li.custom div input.range_datepicker,.woocommerce-reports-wide .postbox div.stats_range ul li.custom form input.range_datepicker,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom div input.range_datepicker,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom form input.range_datepicker,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom div input.range_datepicker,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom form input.range_datepicker,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom div input.range_datepicker,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom form input.range_datepicker{padding:0;margin:0 0 0 10px;background:0 0;border:0;color:#777;text-align:center;box-shadow:none}.woocommerce-reports-wide .postbox div.stats_range ul li.custom div input.range_datepicker.from,.woocommerce-reports-wide .postbox div.stats_range ul li.custom form input.range_datepicker.from,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom div input.range_datepicker.from,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom form input.range_datepicker.from,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom div input.range_datepicker.from,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom form input.range_datepicker.from,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom div input.range_datepicker.from,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom form input.range_datepicker.from{margin-left:0}.woocommerce-reports-wide .postbox .chart-with-sidebar,.woocommerce-reports-wrap .postbox .chart-with-sidebar{padding:12px 249px 12px 12px;margin:0!important}.woocommerce-reports-wide .postbox .chart-with-sidebar .chart-sidebar,.woocommerce-reports-wrap .postbox .chart-with-sidebar .chart-sidebar{width:225px;margin-right:-237px;float:right}.woocommerce-reports-wide .postbox .chart-widgets,.woocommerce-reports-wrap .postbox .chart-widgets{margin:0;padding:0}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget{margin:0 0 1em;background:#fafafa;border:1px solid #dfdfdf}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget::after,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget::after{content:".";display:block;height:0;clear:both;visibility:hidden}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget h4,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget h4{background:#fff;border:1px solid #dfdfdf;border-right-width:0;border-left-width:0;padding:10px;margin:0;color:#2ea2cc;border-top-width:0;background-image:-webkit-gradient(linear,left bottom,left top,from(#ececec),to(#f9f9f9));background-image:linear-gradient(to top,#ececec,#f9f9f9)}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget h4.section_title:hover,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget h4.section_title:hover{color:#a00}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title{cursor:pointer}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title span,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title span{display:block}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title span::after,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title span::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin-right:.618em;content:"";text-decoration:none;float:left;font-size:.9em;line-height:1.618}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title.open,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title.open{color:#333}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title.open span::after,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title.open span::after{display:none}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section{border-bottom:1px solid #dfdfdf}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section .select2-container,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section .select2-container{width:100%!important}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section:last-of-type,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section:last-of-type{border-radius:0 0 3px 3px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table{width:100%}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td{padding:7px 10px;vertical-align:top;border-top:1px solid #e5e5e5;line-height:1.4em}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table tr:first-child td,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table tr:first-child td{border-top:0}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td.count,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td.count{background:#f5f5f5}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td.name,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td.name{max-width:175px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td.name a,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td.name a{word-wrap:break-word}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td.sparkline,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td.sparkline{vertical-align:middle}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table .wc_sparkline,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table .wc_sparkline{width:32px;height:1em;display:block;float:left}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table tr.active td,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table tr.active td{background:#f5f5f5}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget form,.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget p,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget form,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget p{margin:0;padding:10px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget form .submit,.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget p .submit,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget form .submit,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget p .submit{margin-top:10px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget #product_ids,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget #product_ids{width:100%}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .select_all,.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .select_none,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .select_all,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .select_none{float:left;color:#999;margin-right:4px;margin-top:10px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .description,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .description{margin-right:.5em;font-weight:400;opacity:.8}.woocommerce-reports-wide .postbox .chart-legend,.woocommerce-reports-wrap .postbox .chart-legend{list-style:none outside;margin:0 0 1em;padding:0;border:1px solid #dfdfdf;border-left-width:0;border-bottom-width:0;background:#fff}.woocommerce-reports-wide .postbox .chart-legend li,.woocommerce-reports-wrap .postbox .chart-legend li{border-left:5px solid #aaa;color:#aaa;padding:1em;display:block;margin:0;-webkit-transition:all ease .5s;transition:all ease .5s;box-shadow:inset 0 -1px 0 0 #dfdfdf}.woocommerce-reports-wide .postbox .chart-legend li strong,.woocommerce-reports-wrap .postbox .chart-legend li strong{font-size:1.618em;line-height:1.2em;color:#464646;font-weight:400;display:block;font-family:HelveticaNeue-Light,"Helvetica Neue Light","Helvetica Neue",sans-serif}.woocommerce-reports-wide .postbox .chart-legend li strong del,.woocommerce-reports-wrap .postbox .chart-legend li strong del{color:#e74c3c;font-weight:400}.woocommerce-reports-wide .postbox .chart-legend li:hover,.woocommerce-reports-wrap .postbox .chart-legend li:hover{box-shadow:inset 0 -1px 0 0 #dfdfdf,inset -300px 0 0 rgba(156,93,144,.1);border-left:5px solid #9c5d90!important;padding-right:1.5em;color:#9c5d90}.woocommerce-reports-wide .postbox .pie-chart-legend,.woocommerce-reports-wrap .postbox .pie-chart-legend{margin:12px 0 0;overflow:hidden}.woocommerce-reports-wide .postbox .pie-chart-legend li,.woocommerce-reports-wrap .postbox .pie-chart-legend li{float:right;margin:0;padding:6px 0 0;border-top:4px solid #999;text-align:center;box-sizing:border-box;width:50%}.woocommerce-reports-wide .postbox .stat,.woocommerce-reports-wrap .postbox .stat{font-size:1.5em!important;font-weight:700;text-align:center}.woocommerce-reports-wide .postbox .chart-placeholder,.woocommerce-reports-wrap .postbox .chart-placeholder{width:100%;height:650px;overflow:hidden;position:relative}.woocommerce-reports-wide .postbox .chart-prompt,.woocommerce-reports-wrap .postbox .chart-prompt{line-height:650px;margin:0;color:#999;font-size:1.2em;font-style:italic;text-align:center}.woocommerce-reports-wide .postbox .chart-container,.woocommerce-reports-wrap .postbox .chart-container{background:#fff;padding:12px;position:relative;border:1px solid #dfdfdf;border-radius:3px}.woocommerce-reports-wide .postbox .main .chart-legend,.woocommerce-reports-wrap .postbox .main .chart-legend{margin-top:12px}.woocommerce-reports-wide .postbox .main .chart-legend li,.woocommerce-reports-wrap .postbox .main .chart-legend li{border-left:0;margin:0 0 0 8px;float:right;border-top:4px solid #aaa}.woocommerce-reports-wide .woocommerce-reports-main,.woocommerce-reports-wrap .woocommerce-reports-main{float:right;min-width:100%}.woocommerce-reports-wide .woocommerce-reports-main table td,.woocommerce-reports-wrap .woocommerce-reports-main table td{padding:9px}.woocommerce-reports-wide .woocommerce-reports-sidebar,.woocommerce-reports-wrap .woocommerce-reports-sidebar{display:inline;width:281px;margin-right:-300px;clear:both;float:right}.woocommerce-reports-wide .woocommerce-reports-left,.woocommerce-reports-wrap .woocommerce-reports-left{width:49.5%;float:right}.woocommerce-reports-wide .woocommerce-reports-right,.woocommerce-reports-wrap .woocommerce-reports-right{width:49.5%;float:left}.woocommerce-wide-reports-wrap{padding-bottom:11px}.woocommerce-wide-reports-wrap .widefat .export-data{float:left}.woocommerce-wide-reports-wrap .widefat td,.woocommerce-wide-reports-wrap .widefat th{vertical-align:middle;padding:7px}form.report_filters p{vertical-align:middle}form.report_filters div,form.report_filters input,form.report_filters label{vertical-align:middle}.chart-tooltip{position:absolute;display:none;line-height:1}table.bar_chart{width:100%}table.bar_chart thead th{text-align:right;color:#ccc;padding:6px 0}table.bar_chart tbody th{padding:6px 0;width:25%;text-align:right!important;font-weight:400!important;border-bottom:1px solid #fee}table.bar_chart tbody td{text-align:left;line-height:24px;padding:6px 0 6px 6px;border-bottom:1px solid #fee}table.bar_chart tbody td span{color:#8a4b75;display:block}table.bar_chart tbody td span.alt{color:#47a03e;margin-top:6px}table.bar_chart tbody td.bars{position:relative;text-align:right;padding:6px 0 6px 6px;border-bottom:1px solid #fee}table.bar_chart tbody td.bars a,table.bar_chart tbody td.bars span{text-decoration:none;clear:both;background:#8a4b75;float:right;display:block;line-height:24px;height:24px;border-radius:3px}table.bar_chart tbody td.bars span.alt{clear:both;background:#47a03e}table.bar_chart tbody td.bars span.alt span{margin:0;color:#c5dec2!important;text-shadow:0 1px 0 #47a03e;background:0 0}.post-type-shop_order .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:""}.post-type-shop_coupon .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:""}.post-type-product .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:""}.woocommerce-BlankState--api .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:""}.woocommerce-BlankState--webhooks .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:""}.woocommerce-BlankState{text-align:center;padding:5em 0 0}.woocommerce-BlankState .woocommerce-BlankState-message{color:#aaa;margin:0 auto 1.5em;line-height:1.5em;font-size:1.2em;max-width:500px}.woocommerce-BlankState .woocommerce-BlankState-message::before{color:#ddd;text-shadow:0 -1px 1px rgba(0,0,0,.2),0 1px 0 rgba(255,255,255,.8);font-size:8em;display:block;position:relative!important;top:auto;right:auto;line-height:1em;margin:0 0 .1875em}.woocommerce-BlankState .woocommerce-BlankState-cta{font-size:1.2em;padding:.75em 1.5em;margin:0 .25em;height:auto;display:inline-block!important}.post-type-product .woocommerce-BlankState,.post-type-shop_order .woocommerce-BlankState{max-width:764px;text-align:center;margin:auto}.post-type-product .woocommerce-BlankState .woocommerce-BlankState-message,.post-type-shop_order .woocommerce-BlankState .woocommerce-BlankState-message{color:#444;font-size:1.5em;margin:0 auto 1em}.post-type-product .woocommerce-BlankState .woocommerce-BlankState-message::before,.post-type-shop_order .woocommerce-BlankState .woocommerce-BlankState-message::before{font-size:120px}.post-type-product .woocommerce-BlankState .woocommerce-BlankState-buttons,.post-type-shop_order .woocommerce-BlankState .woocommerce-BlankState-buttons{margin-bottom:4em}.post-type-product #wp-pointer-2 .wp-pointer-arrow{right:240px}.post-type-product #wp-pointer-3 .wp-pointer-arrow,.post-type-product #wp-pointer-4 .wp-pointer-arrow{right:46%}@media only screen and (max-width:1280px){#order_data .order_data_column{width:48%}#order_data .order_data_column:first-child{width:100%}.woocommerce_options_panel .description{display:block;clear:both;margin-right:0}.woocommerce_options_panel .dimensions_field .wrap,.woocommerce_options_panel .short,.woocommerce_options_panel input[type=email].short,.woocommerce_options_panel input[type=number].short,.woocommerce_options_panel input[type=password].short,.woocommerce_options_panel input[type=text].short{width:80%}.woocommerce_options_panel .downloadable_files,.woocommerce_variations .downloadable_files{padding:0;clear:both}.woocommerce_options_panel .downloadable_files label,.woocommerce_variations .downloadable_files label{position:static}.woocommerce_options_panel .downloadable_files table,.woocommerce_variations .downloadable_files table{margin:0 12px 24px;width:94%}.woocommerce_options_panel .downloadable_files table .sort,.woocommerce_variations .downloadable_files table .sort{visibility:hidden}.woocommerce_options_panel .woocommerce_variable_attributes .downloadable_files table,.woocommerce_variations .woocommerce_variable_attributes .downloadable_files table{margin:0 0 1em;width:100%}}@media only screen and (max-width:900px){#woocommerce-coupon-data ul.coupon_data_tabs,#woocommerce-product-data .wc-tabs-back,#woocommerce-product-data ul.product_data_tabs{width:10%}#woocommerce-coupon-data .wc-metaboxes-wrapper,#woocommerce-coupon-data .woocommerce_options_panel,#woocommerce-product-data .wc-metaboxes-wrapper,#woocommerce-product-data .woocommerce_options_panel{width:90%}#woocommerce-coupon-data ul.coupon_data_tabs li a,#woocommerce-product-data ul.product_data_tabs li a{position:relative;text-indent:-999px;padding:10px}#woocommerce-coupon-data ul.coupon_data_tabs li a::before,#woocommerce-product-data ul.product_data_tabs li a::before{position:absolute;top:0;left:0;bottom:0;right:0;text-indent:0;text-align:center;line-height:40px;width:100%;height:40px}}@media only screen and (max-width:782px){#wp-excerpt-media-buttons a{font-size:16px;line-height:37px;height:39px;padding:0 15px 0 20px}#wp-excerpt-editor-tools{padding-top:20px;padding-left:15px;overflow:hidden;margin-bottom:-1px}#woocommerce-product-data .checkbox{width:25px}.variations-pagenav{float:none;text-align:center;font-size:18px}.variations-pagenav .displaying-num{font-size:16px}.variations-pagenav a{padding:8px 20px 11px;font-size:18px}.variations-pagenav select{padding:0 20px}.variations-defaults{float:none;text-align:center;margin-top:10px}.post-type-product .wp-list-table .column-thumb{display:none;text-align:right;padding-bottom:0}.post-type-product .wp-list-table .column-thumb::before{display:none!important}.post-type-product .wp-list-table .column-thumb img{max-width:32px}.post-type-product .wp-list-table .is-expanded td:not(.hidden){overflow:visible}.post-type-product .wp-list-table .toggle-row{top:-28px}.post-type-shop_order .wp-list-table .column-customer_message,.post-type-shop_order .wp-list-table .column-order_notes{text-align:inherit}.post-type-shop_order .wp-list-table .column-order_notes .note-on{font-size:1.3em;margin:0}.post-type-shop_order .wp-list-table .is-expanded td:not(.hidden){overflow:visible}.post-type-shop_order .wp-list-table .toggle-row{top:-15px}}@media only screen and (max-width:500px){.woocommerce_options_panel label,.woocommerce_options_panel legend{float:none;width:auto;display:block;margin:0}.woocommerce_options_panel fieldset.form-field,.woocommerce_options_panel p.form-field{padding:5px 20px!important}.addons-wcs-banner-block{-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.wc-addons-wrap .addons-wcs-banner-block{padding:40px}.wc-addons-wrap .addons-wcs-banner-block-image{padding:1em;text-align:center;width:100%;padding:2em 0;margin:0}.wc-addons-wrap .addons-wcs-banner-block-image .addons-img{margin:0}}.wc-backbone-modal *{box-sizing:border-box}.wc-backbone-modal .wc-backbone-modal-content{position:fixed;background:#fff;z-index:100000;right:50%;top:50%;-webkit-transform:translate(50%,-50%);-ms-transform:translate(50%,-50%);transform:translate(50%,-50%);max-width:100%;min-width:500px}.wc-backbone-modal .wc-backbone-modal-content article{overflow:auto}.wc-backbone-modal.wc-backbone-modal-shipping-method-settings .wc-backbone-modal-content{width:75%;min-width:500px}.wc-backbone-modal .select2-container{width:100%!important}@media screen and (max-width:782px){.wc-backbone-modal .wc-backbone-modal-content{width:100%;height:100%;min-width:100%}}.wc-backbone-modal-backdrop{position:fixed;top:0;right:0;left:0;bottom:0;min-height:360px;background:#000;opacity:.7;z-index:99900}.wc-backbone-modal-main{padding-bottom:55px}.wc-backbone-modal-main article,.wc-backbone-modal-main header{display:block;position:relative}.wc-backbone-modal-main .wc-backbone-modal-header{height:auto;background:#fcfcfc;padding:1em 1.5em;border-bottom:1px solid #ddd}.wc-backbone-modal-main .wc-backbone-modal-header h1{margin:0;font-size:18px;font-weight:700;line-height:1.5em}.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link{cursor:pointer;color:#777;height:54px;width:54px;padding:0;position:absolute;top:0;left:0;text-align:center;border:0;border-right:1px solid #ddd;background-color:transparent;-webkit-transition:color .1s ease-in-out,background .1s ease-in-out;transition:color .1s ease-in-out,background .1s ease-in-out}.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link::before{font:normal 22px/50px dashicons!important;color:#666;display:block;content:"\f335";font-weight:300}.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link:focus,.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link:hover{background:#ddd;border-color:#ccc;color:#000}.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link:focus{outline:0}.wc-backbone-modal-main article{padding:1.5em}.wc-backbone-modal-main article p{margin:1.5em 0}.wc-backbone-modal-main article p:first-child{margin-top:0}.wc-backbone-modal-main article p:last-child{margin-bottom:0}.wc-backbone-modal-main article .pagination{padding:10px 0 0;text-align:center}.wc-backbone-modal-main article table.widefat{margin:0;width:100%;border:0;box-shadow:none}.wc-backbone-modal-main article table.widefat thead th{padding:0 1em 1em 1em;text-align:right}.wc-backbone-modal-main article table.widefat thead th:first-child{padding-right:0}.wc-backbone-modal-main article table.widefat thead th:last-child{padding-left:0;text-align:left}.wc-backbone-modal-main article table.widefat tbody td,.wc-backbone-modal-main article table.widefat tbody th{padding:1em;text-align:right;vertical-align:middle}.wc-backbone-modal-main article table.widefat tbody td:first-child,.wc-backbone-modal-main article table.widefat tbody th:first-child{padding-right:0}.wc-backbone-modal-main article table.widefat tbody td:last-child,.wc-backbone-modal-main article table.widefat tbody th:last-child{padding-left:0;text-align:left}.wc-backbone-modal-main article table.widefat tbody td .select2-container,.wc-backbone-modal-main article table.widefat tbody td select,.wc-backbone-modal-main article table.widefat tbody th .select2-container,.wc-backbone-modal-main article table.widefat tbody th select{width:100%}.wc-backbone-modal-main footer{position:absolute;right:0;left:0;bottom:0;z-index:100;padding:1em 1.5em;background:#fcfcfc;border-top:1px solid #dfdfdf;box-shadow:0 -4px 4px -4px rgba(0,0,0,.1)}.wc-backbone-modal-main footer .inner{text-align:left;line-height:23px}.wc-backbone-modal-main footer .inner .button{margin-bottom:0}.select2-drop,.select2-dropdown{z-index:999999!important}.select2-results{line-height:1.5em}.select2-results .select2-results__group,.select2-results .select2-results__option{margin:0;padding:8px}.select2-results .description{display:block;color:#999;padding-top:4px}.select2-dropdown{border-color:#ddd}.select2-dropdown--below{box-shadow:0 1px 1px rgba(0,0,0,.1)}.select2-dropdown--above{box-shadow:0 -1px 1px rgba(0,0,0,.1)}.select2-container .select2-selection__rendered.ui-sortable li{cursor:move}.select2-container .select2-selection{border-color:#ddd}.select2-container .select2-search__field{min-width:150px}.select2-container .select2-selection--single{height:40px}.select2-container .select2-selection--single .select2-selection__rendered{line-height:40px;padding-left:24px}.select2-container .select2-selection--single .select2-selection__arrow{left:3px;height:36px}.select2-container .select2-selection--multiple{min-height:28px;border-radius:0;line-height:1.5}.select2-container .select2-selection--multiple li{margin:0}.select2-container .select2-selection--multiple .select2-selection__choice{padding:2px 6px}.select2-container .select2-selection--multiple .select2-selection__choice .description{display:none}.select2-container .select2-selection__clear{color:#999;margin-top:-1px;z-index:1}.select2-container .select2-search--inline .select2-search__field{font-family:inherit;font-size:inherit;font-weight:inherit;padding:3px 0}.woocommerce table.form-table .select2-container{min-width:400px!important}.wc-wp-version-gte-53 .select2-results .select2-results__group:focus,.wc-wp-version-gte-53 .select2-results .select2-results__option:focus{outline:0}.wc-wp-version-gte-53 .select2-dropdown{border-color:#007cba}.wc-wp-version-gte-53 .select2-dropdown::after{position:absolute;right:0;left:0;height:1px;background:#fff;content:""}.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #007cba,0 2px 1px rgba(0,0,0,.1)}.wc-wp-version-gte-53 .select2-dropdown--below::after{top:-1px}.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #007cba,0 -2px 1px rgba(0,0,0,.1)}.wc-wp-version-gte-53 .select2-dropdown--above::after{bottom:-1px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .select2-container{font-size:16px}}.wc-wp-version-gte-53 .select2-container:focus{outline:0}.wc-wp-version-gte-53 .select2-container .select2-selection--single{height:30px;border-color:#7e8993}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .select2-container .select2-selection--single{height:40px}}.wc-wp-version-gte-53 .select2-container .select2-selection--single:focus{outline:0}.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__rendered{line-height:28px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__rendered{line-height:38px}}.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__rendered:hover{color:#007cba}.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__arrow{left:1px;height:28px;width:23px;background:url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E") no-repeat left 5px top 55%;background-size:16px 16px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__arrow{height:38px}}.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__arrow b{display:none}.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#007cba;box-shadow:0 0 0 1px #007cba}.wc-wp-version-gte-53 .select2-container .select2-selection--multiple{min-height:30px;border-color:#7e8993;border-radius:4px}.wc-wp-version-gte-53 .select2-container .select2-search--inline .select2-search__field{padding:0 3px 0 0;min-height:28px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table .select2-container{min-width:100%!important}}.wc-wp-version-gte-55 #woocommerce-product-data .hndle{display:block;line-height:24px}.wc-wp-version-gte-55 #woocommerce-product-data .hndle .type_box{display:inline;line-height:inherit;vertical-align:baseline}.admin-color-blue.wc-wp-version-gte-53 .select2-dropdown{border-color:#096484}.admin-color-blue.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #096484,0 2px 1px rgba(0,0,0,.1)}.admin-color-blue.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #096484,0 -2px 1px rgba(0,0,0,.1)}.admin-color-blue.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#096484}.admin-color-blue.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-blue.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-blue.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#096484;box-shadow:0 0 0 1px #096484}.admin-color-blue.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-blue.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#096484}.admin-color-coffee.wc-wp-version-gte-53 .select2-dropdown{border-color:#c7a589}.admin-color-coffee.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #c7a589,0 2px 1px rgba(0,0,0,.1)}.admin-color-coffee.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #c7a589,0 -2px 1px rgba(0,0,0,.1)}.admin-color-coffee.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#c7a589}.admin-color-coffee.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-coffee.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-coffee.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#c7a589;box-shadow:0 0 0 1px #c7a589}.admin-color-coffee.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-coffee.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#c7a589}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-dropdown{border-color:#a3b745}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #a3b745,0 2px 1px rgba(0,0,0,.1)}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #a3b745,0 -2px 1px rgba(0,0,0,.1)}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#a3b745}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#a3b745;box-shadow:0 0 0 1px #a3b745}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#a3b745}.admin-color-midnight.wc-wp-version-gte-53 .select2-dropdown{border-color:#e14d43}.admin-color-midnight.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #e14d43,0 2px 1px rgba(0,0,0,.1)}.admin-color-midnight.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #e14d43,0 -2px 1px rgba(0,0,0,.1)}.admin-color-midnight.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#e14d43}.admin-color-midnight.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-midnight.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-midnight.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#e14d43;box-shadow:0 0 0 1px #e14d43}.admin-color-midnight.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-midnight.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#e14d43}.admin-color-ocean.wc-wp-version-gte-53 .select2-dropdown{border-color:#9ebaa0}.admin-color-ocean.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #9ebaa0,0 2px 1px rgba(0,0,0,.1)}.admin-color-ocean.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #9ebaa0,0 -2px 1px rgba(0,0,0,.1)}.admin-color-ocean.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#9ebaa0}.admin-color-ocean.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-ocean.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-ocean.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#9ebaa0;box-shadow:0 0 0 1px #9ebaa0}.admin-color-ocean.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-ocean.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#9ebaa0}.admin-color-sunrise.wc-wp-version-gte-53 .select2-dropdown{border-color:#dd823b}.admin-color-sunrise.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #dd823b,0 2px 1px rgba(0,0,0,.1)}.admin-color-sunrise.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #dd823b,0 -2px 1px rgba(0,0,0,.1)}.admin-color-sunrise.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#dd823b}.admin-color-sunrise.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-sunrise.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-sunrise.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#dd823b;box-shadow:0 0 0 1px #dd823b}.admin-color-sunrise.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-sunrise.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#dd823b}.admin-color-light.wc-wp-version-gte-53 .select2-dropdown{border-color:#04a4cc}.admin-color-light.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #04a4cc,0 2px 1px rgba(0,0,0,.1)}.admin-color-light.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #04a4cc,0 -2px 1px rgba(0,0,0,.1)}.admin-color-light.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#04a4cc}.admin-color-light.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-light.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-light.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#04a4cc;box-shadow:0 0 0 1px #04a4cc}.admin-color-light.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-light.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#04a4cc}.post-type-product .tablenav .actions,.post-type-shop_order .tablenav .actions{overflow:visible}.post-type-product .tablenav input,.post-type-product .tablenav select,.post-type-shop_order .tablenav input,.post-type-shop_order .tablenav select{height:32px}.post-type-product .tablenav .select2-container,.post-type-shop_order .tablenav .select2-container{float:right;width:240px!important;font-size:14px;vertical-align:middle;margin:1px 1px 4px 6px}.woocommerce-exporter-wrapper,.woocommerce-importer-wrapper,.woocommerce-progress-form-wrapper{text-align:center;max-width:700px;margin:40px auto}.woocommerce-exporter-wrapper .error,.woocommerce-importer-wrapper .error,.woocommerce-progress-form-wrapper .error{text-align:right}.woocommerce-exporter-wrapper .wc-progress-steps,.woocommerce-importer-wrapper .wc-progress-steps,.woocommerce-progress-form-wrapper .wc-progress-steps{padding:0 0 24px;margin:0;list-style:none outside;overflow:hidden;color:#ccc;width:100%;display:-webkit-inline-box;display:inline-flex}.woocommerce-exporter-wrapper .wc-progress-steps li,.woocommerce-importer-wrapper .wc-progress-steps li,.woocommerce-progress-form-wrapper .wc-progress-steps li{width:25%;float:right;padding:0 0 .8em;margin:0;text-align:center;position:relative;border-bottom:4px solid #ccc;line-height:1.4em}.woocommerce-exporter-wrapper .wc-progress-steps li::before,.woocommerce-importer-wrapper .wc-progress-steps li::before,.woocommerce-progress-form-wrapper .wc-progress-steps li::before{content:"";border:4px solid #ccc;border-radius:100%;width:4px;height:4px;position:absolute;bottom:0;right:50%;margin-right:-6px;margin-bottom:-8px;background:#fff}.woocommerce-exporter-wrapper .wc-progress-steps li.active,.woocommerce-importer-wrapper .wc-progress-steps li.active,.woocommerce-progress-form-wrapper .wc-progress-steps li.active{border-color:#a16696;color:#a16696}.woocommerce-exporter-wrapper .wc-progress-steps li.active::before,.woocommerce-importer-wrapper .wc-progress-steps li.active::before,.woocommerce-progress-form-wrapper .wc-progress-steps li.active::before{border-color:#a16696}.woocommerce-exporter-wrapper .wc-progress-steps li.done,.woocommerce-importer-wrapper .wc-progress-steps li.done,.woocommerce-progress-form-wrapper .wc-progress-steps li.done{border-color:#a16696;color:#a16696}.woocommerce-exporter-wrapper .wc-progress-steps li.done::before,.woocommerce-importer-wrapper .wc-progress-steps li.done::before,.woocommerce-progress-form-wrapper .wc-progress-steps li.done::before{border-color:#a16696;background:#a16696}.woocommerce-exporter-wrapper .button,.woocommerce-importer-wrapper .button,.woocommerce-progress-form-wrapper .button{font-size:1.25em;padding:.5em 1em!important;line-height:1.5em!important;margin-left:.5em;margin-bottom:2px;height:auto!important;border-radius:4px;background-color:#bb77ae;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597;text-shadow:0 -1px 1px #a36597,-1px 0 1px #a36597,0 1px 1px #a36597,1px 0 1px #a36597;margin:0;opacity:1}.woocommerce-exporter-wrapper .button:active,.woocommerce-exporter-wrapper .button:focus,.woocommerce-exporter-wrapper .button:hover,.woocommerce-importer-wrapper .button:active,.woocommerce-importer-wrapper .button:focus,.woocommerce-importer-wrapper .button:hover,.woocommerce-progress-form-wrapper .button:active,.woocommerce-progress-form-wrapper .button:focus,.woocommerce-progress-form-wrapper .button:hover{background:#a36597;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597}.woocommerce-exporter-wrapper .error .button,.woocommerce-importer-wrapper .error .button,.woocommerce-progress-form-wrapper .error .button{font-size:1em}.woocommerce-exporter-wrapper .wc-actions,.woocommerce-importer-wrapper .wc-actions,.woocommerce-progress-form-wrapper .wc-actions{overflow:hidden;border-top:1px solid #eee;margin:0;padding:23px 24px 24px;line-height:3em}.woocommerce-exporter-wrapper .wc-actions .button,.woocommerce-importer-wrapper .wc-actions .button,.woocommerce-progress-form-wrapper .wc-actions .button{float:left}.woocommerce-exporter-wrapper .wc-actions .woocommerce-importer-toggle-advanced-options,.woocommerce-importer-wrapper .wc-actions .woocommerce-importer-toggle-advanced-options,.woocommerce-progress-form-wrapper .wc-actions .woocommerce-importer-toggle-advanced-options{color:#999}.woocommerce-exporter-wrapper .wc-progress-form-content,.woocommerce-exporter-wrapper .woocommerce-exporter,.woocommerce-exporter-wrapper .woocommerce-importer,.woocommerce-importer-wrapper .wc-progress-form-content,.woocommerce-importer-wrapper .woocommerce-exporter,.woocommerce-importer-wrapper .woocommerce-importer,.woocommerce-progress-form-wrapper .wc-progress-form-content,.woocommerce-progress-form-wrapper .woocommerce-exporter,.woocommerce-progress-form-wrapper .woocommerce-importer{background:#fff;overflow:hidden;padding:0;margin:0 0 16px;box-shadow:0 1px 3px rgba(0,0,0,.13);color:#555;text-align:right}.woocommerce-exporter-wrapper .wc-progress-form-content header,.woocommerce-exporter-wrapper .woocommerce-exporter header,.woocommerce-exporter-wrapper .woocommerce-importer header,.woocommerce-importer-wrapper .wc-progress-form-content header,.woocommerce-importer-wrapper .woocommerce-exporter header,.woocommerce-importer-wrapper .woocommerce-importer header,.woocommerce-progress-form-wrapper .wc-progress-form-content header,.woocommerce-progress-form-wrapper .woocommerce-exporter header,.woocommerce-progress-form-wrapper .woocommerce-importer header{border-bottom:1px solid #eee;margin:0;padding:24px 24px 0}.woocommerce-exporter-wrapper .wc-progress-form-content section,.woocommerce-exporter-wrapper .woocommerce-exporter section,.woocommerce-exporter-wrapper .woocommerce-importer section,.woocommerce-importer-wrapper .wc-progress-form-content section,.woocommerce-importer-wrapper .woocommerce-exporter section,.woocommerce-importer-wrapper .woocommerce-importer section,.woocommerce-progress-form-wrapper .wc-progress-form-content section,.woocommerce-progress-form-wrapper .woocommerce-exporter section,.woocommerce-progress-form-wrapper .woocommerce-importer section{padding:24px 24px 0}.woocommerce-exporter-wrapper .wc-progress-form-content h2,.woocommerce-exporter-wrapper .woocommerce-exporter h2,.woocommerce-exporter-wrapper .woocommerce-importer h2,.woocommerce-importer-wrapper .wc-progress-form-content h2,.woocommerce-importer-wrapper .woocommerce-exporter h2,.woocommerce-importer-wrapper .woocommerce-importer h2,.woocommerce-progress-form-wrapper .wc-progress-form-content h2,.woocommerce-progress-form-wrapper .woocommerce-exporter h2,.woocommerce-progress-form-wrapper .woocommerce-importer h2{margin:0 0 24px;color:#555;font-size:24px;font-weight:400;line-height:1em}.woocommerce-exporter-wrapper .wc-progress-form-content p,.woocommerce-exporter-wrapper .woocommerce-exporter p,.woocommerce-exporter-wrapper .woocommerce-importer p,.woocommerce-importer-wrapper .wc-progress-form-content p,.woocommerce-importer-wrapper .woocommerce-exporter p,.woocommerce-importer-wrapper .woocommerce-importer p,.woocommerce-progress-form-wrapper .wc-progress-form-content p,.woocommerce-progress-form-wrapper .woocommerce-exporter p,.woocommerce-progress-form-wrapper .woocommerce-importer p{font-size:1em;line-height:1.75em;font-size:16px;color:#555;margin:0 0 24px}.woocommerce-exporter-wrapper .wc-progress-form-content .form-row,.woocommerce-exporter-wrapper .woocommerce-exporter .form-row,.woocommerce-exporter-wrapper .woocommerce-importer .form-row,.woocommerce-importer-wrapper .wc-progress-form-content .form-row,.woocommerce-importer-wrapper .woocommerce-exporter .form-row,.woocommerce-importer-wrapper .woocommerce-importer .form-row,.woocommerce-progress-form-wrapper .wc-progress-form-content .form-row,.woocommerce-progress-form-wrapper .woocommerce-exporter .form-row,.woocommerce-progress-form-wrapper .woocommerce-importer .form-row{margin-top:24px}.woocommerce-exporter-wrapper .wc-progress-form-content .spinner,.woocommerce-exporter-wrapper .woocommerce-exporter .spinner,.woocommerce-exporter-wrapper .woocommerce-importer .spinner,.woocommerce-importer-wrapper .wc-progress-form-content .spinner,.woocommerce-importer-wrapper .woocommerce-exporter .spinner,.woocommerce-importer-wrapper .woocommerce-importer .spinner,.woocommerce-progress-form-wrapper .wc-progress-form-content .spinner,.woocommerce-progress-form-wrapper .woocommerce-exporter .spinner,.woocommerce-progress-form-wrapper .woocommerce-importer .spinner{display:none}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th{vertical-align:top;line-height:1.75em;padding:0 0 24px 0}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td label,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th label,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td label,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th label,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td label,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th label,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td label,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th label,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td label,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th label,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td label,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th label,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td label,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th label,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td label,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th label,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td label,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th label,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td label,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th label,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td label,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th label,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td label,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th label,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td label,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th label,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td label,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th label,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td label,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th label,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td label,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th label,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td label,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th label,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td label,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th label{color:#555;font-weight:400}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=checkbox],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=checkbox],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=checkbox],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=checkbox],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=checkbox],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=checkbox],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=checkbox],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=checkbox],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=checkbox],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=checkbox],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=checkbox],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=checkbox]{margin:0 0 0 4px;padding:7px}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=number],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=text],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=number],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=text],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=number],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=text],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=number],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=text],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=number],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=text],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=number],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=text],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=number],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=text],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=number],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=text],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=number],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=text],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=number],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=text],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=number],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=text],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=number],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=text],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=number],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=text],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=number],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=text],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=number],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=text],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=number],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=text],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=number],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=text],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=number],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=text],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=number],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=text],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=number],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=text],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=number],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=text],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=number],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=text],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=number],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=text],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=number],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=text],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=number],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=text],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=number],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=text],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=number],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=text],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=number],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=text],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=number],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=text],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=number],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=text],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=number],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=text],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=number],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=text],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=number],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=text],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=number],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=text],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=number],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=text],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=number],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=text]{padding:7px;height:auto;margin:0}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper{border:1px solid #ddd;box-shadow:inset 0 1px 2px rgba(0,0,0,.07);background-color:#fff;color:#32373c;outline:0;line-height:1;display:block}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code{background:0 0;font-size:smaller;padding:0;margin:0;color:#999;padding:7px 7px 0 0;display:inline-block}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input{font-family:Consolas,Monaco,monospace;border:0;margin:0;outline:0;box-shadow:none;display:inline-block;min-width:100%}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th{width:35%;padding-left:20px}.woocommerce-exporter-wrapper .wc-progress-form-content progress,.woocommerce-exporter-wrapper .woocommerce-exporter progress,.woocommerce-exporter-wrapper .woocommerce-importer progress,.woocommerce-importer-wrapper .wc-progress-form-content progress,.woocommerce-importer-wrapper .woocommerce-exporter progress,.woocommerce-importer-wrapper .woocommerce-importer progress,.woocommerce-progress-form-wrapper .wc-progress-form-content progress,.woocommerce-progress-form-wrapper .woocommerce-exporter progress,.woocommerce-progress-form-wrapper .woocommerce-importer progress{width:100%;height:42px;margin:0 auto 24px;display:block;-webkit-appearance:none;border:none;display:none;background:#f5f5f5;border:2px solid #eee;border-radius:4px;padding:0;box-shadow:0 1px 0 0 rgba(255,255,255,.2)}.woocommerce-exporter-wrapper .wc-progress-form-content progress::-webkit-progress-bar,.woocommerce-exporter-wrapper .woocommerce-exporter progress::-webkit-progress-bar,.woocommerce-exporter-wrapper .woocommerce-importer progress::-webkit-progress-bar,.woocommerce-importer-wrapper .wc-progress-form-content progress::-webkit-progress-bar,.woocommerce-importer-wrapper .woocommerce-exporter progress::-webkit-progress-bar,.woocommerce-importer-wrapper .woocommerce-importer progress::-webkit-progress-bar,.woocommerce-progress-form-wrapper .wc-progress-form-content progress::-webkit-progress-bar,.woocommerce-progress-form-wrapper .woocommerce-exporter progress::-webkit-progress-bar,.woocommerce-progress-form-wrapper .woocommerce-importer progress::-webkit-progress-bar{background:transparent none;border:0;border-radius:4px;padding:0;box-shadow:none}.woocommerce-exporter-wrapper .wc-progress-form-content progress::-webkit-progress-value,.woocommerce-exporter-wrapper .woocommerce-exporter progress::-webkit-progress-value,.woocommerce-exporter-wrapper .woocommerce-importer progress::-webkit-progress-value,.woocommerce-importer-wrapper .wc-progress-form-content progress::-webkit-progress-value,.woocommerce-importer-wrapper .woocommerce-exporter progress::-webkit-progress-value,.woocommerce-importer-wrapper .woocommerce-importer progress::-webkit-progress-value,.woocommerce-progress-form-wrapper .wc-progress-form-content progress::-webkit-progress-value,.woocommerce-progress-form-wrapper .woocommerce-exporter progress::-webkit-progress-value,.woocommerce-progress-form-wrapper .woocommerce-importer progress::-webkit-progress-value{border-radius:3px;box-shadow:inset 0 1px 1px 0 rgba(255,255,255,.4);background:#a46497;background:-webkit-gradient(linear,left top,left bottom,from(#a46497),to(#66405f)),#a46497;background:linear-gradient(to bottom,#a46497,#66405f),#a46497;-webkit-transition:width 1s ease;transition:width 1s ease}.woocommerce-exporter-wrapper .wc-progress-form-content progress::-moz-progress-bar,.woocommerce-exporter-wrapper .woocommerce-exporter progress::-moz-progress-bar,.woocommerce-exporter-wrapper .woocommerce-importer progress::-moz-progress-bar,.woocommerce-importer-wrapper .wc-progress-form-content progress::-moz-progress-bar,.woocommerce-importer-wrapper .woocommerce-exporter progress::-moz-progress-bar,.woocommerce-importer-wrapper .woocommerce-importer progress::-moz-progress-bar,.woocommerce-progress-form-wrapper .wc-progress-form-content progress::-moz-progress-bar,.woocommerce-progress-form-wrapper .woocommerce-exporter progress::-moz-progress-bar,.woocommerce-progress-form-wrapper .woocommerce-importer progress::-moz-progress-bar{border-radius:3px;box-shadow:inset 0 1px 1px 0 rgba(255,255,255,.4);background:#a46497;background:linear-gradient(to bottom,#a46497,#66405f),#a46497;-moz-transition:width 1s ease;transition:width 1s ease}.woocommerce-exporter-wrapper .wc-progress-form-content progress::-ms-fill,.woocommerce-exporter-wrapper .woocommerce-exporter progress::-ms-fill,.woocommerce-exporter-wrapper .woocommerce-importer progress::-ms-fill,.woocommerce-importer-wrapper .wc-progress-form-content progress::-ms-fill,.woocommerce-importer-wrapper .woocommerce-exporter progress::-ms-fill,.woocommerce-importer-wrapper .woocommerce-importer progress::-ms-fill,.woocommerce-progress-form-wrapper .wc-progress-form-content progress::-ms-fill,.woocommerce-progress-form-wrapper .woocommerce-exporter progress::-ms-fill,.woocommerce-progress-form-wrapper .woocommerce-importer progress::-ms-fill{border-radius:3px;box-shadow:inset 0 1px 1px 0 rgba(255,255,255,.4);background:#a46497;background:linear-gradient(to bottom,#a46497,#66405f),#a46497;-ms-transition:width 1s ease;transition:width 1s ease}.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .spinner,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-importer__importing .spinner,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .spinner,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-importer__importing .spinner,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-exporter__exporting .spinner,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-importer__importing .spinner,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .spinner,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-importer__importing .spinner,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .spinner,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-importer__importing .spinner,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-exporter__exporting .spinner,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-importer__importing .spinner,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .spinner,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-importer__importing .spinner,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .spinner,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-importer__importing .spinner,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-exporter__exporting .spinner,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-importer__importing .spinner{display:block}.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-exporter__exporting progress,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-importer__importing progress,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-exporter__exporting progress,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-importer__importing progress,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-exporter__exporting progress,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-importer__importing progress,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-exporter__exporting progress,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-importer__importing progress,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-exporter__exporting progress,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-importer__importing progress,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-exporter__exporting progress,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-importer__importing progress,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-exporter__exporting progress,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-importer__importing progress,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-exporter__exporting progress,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-importer__importing progress,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-exporter__exporting progress,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-importer__importing progress{display:block}.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .wc-actions,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-importer__importing .wc-actions,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .wc-actions,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-importer__importing .wc-actions,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-exporter__exporting .wc-actions,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-importer__importing .wc-actions,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .wc-actions,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-importer__importing .wc-actions,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .wc-actions,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-importer__importing .wc-actions,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-exporter__exporting .wc-actions,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-importer__importing .wc-actions,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .wc-actions,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-importer__importing .wc-actions,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .wc-actions,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-importer__importing .wc-actions,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-exporter__exporting .wc-actions,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-importer__importing .wc-actions,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-importer__importing .woocommerce-exporter-options{display:none}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table-wrapper{padding:0}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table{margin:0;border:0;box-shadow:none;width:100%;table-layout:fixed}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table td,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table td,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table td,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table td,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table td,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table td,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table td,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table td,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table td,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table td,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table td,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table td,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table td,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table td,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table td,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table td,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table td,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table td,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th{border:0;padding:12px;vertical-align:middle;word-wrap:break-word}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table td select,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th select,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table td select,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th select,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table td select,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th select,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table td select,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th select,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table td select,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th select,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table td select,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th select,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table td select,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th select,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table td select,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th select,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table td select,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th select,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table td select,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th select,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table td select,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th select,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table td select,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th select,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table td select,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th select,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table td select,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th select,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table td select,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th select,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table td select,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th select,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table td select,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th select,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table td select,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th select{width:100%}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) th{background:#fbfbfb}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th{font-weight:700}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table td:first-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th:first-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table td:first-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th:first-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table td:first-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th:first-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table td:first-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th:first-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table td:first-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th:first-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table td:first-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th:first-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table td:first-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th:first-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table td:first-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th:first-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table td:first-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th:first-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table td:first-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th:first-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table td:first-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th:first-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table td:first-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th:first-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table td:first-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th:first-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table td:first-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th:first-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table td:first-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th:first-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table td:first-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th:first-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table td:first-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th:first-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table td:first-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th:first-child{padding-right:24px}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table td:last-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th:last-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table td:last-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th:last-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table td:last-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th:last-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table td:last-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th:last-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table td:last-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th:last-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table td:last-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th:last-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table td:last-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th:last-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table td:last-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th:last-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table td:last-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th:last-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table td:last-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th:last-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table td:last-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th:last-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table td:last-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th:last-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table td:last-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th:last-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table td:last-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th:last-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table td:last-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th:last-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table td:last-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th:last-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table td:last-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th:last-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table td:last-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th:last-child{padding-left:24px}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name{width:50%}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description{color:#999;margin-top:4px;display:block}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description code{background:0 0;padding:0;white-space:pre-line;word-wrap:break-word;word-break:break-all}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-done,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-done,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-done,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-done,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-done,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-done,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-done,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-done,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-done{text-align:center;padding:48px 24px;font-size:1.5em;line-height:1.75em}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-done::before,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-done::before,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-done::before,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-done::before,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-done::before,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-done::before,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-done::before,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-done::before,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-done::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";color:#a16696;position:static;font-size:100px;display:block;float:none;margin:0 0 24px}.wc-pointer .wc-pointer-buttons .close{float:right;margin:6px 15px 0 0}.wc-quick-edit-warning{color:#8b0000;font-weight:700}@media screen and (min-width:600px){.wc-addons-wrap .marketplace-header{padding-right:84px}.wc-addons-wrap .storefront h2{margin-top:0}.wc-addons-wrap .storefront img{float:right;margin:0 auto 0 16px;width:278px}} \ No newline at end of file diff --git a/assets/css/admin.css b/assets/css/admin.css new file mode 100644 index 0000000..dfb23e9 --- /dev/null +++ b/assets/css/admin.css @@ -0,0 +1,2 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;margin:0 0 -4px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir=rtl] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left;padding:0}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin:0;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:#fff;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected],.select2-results__option[data-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff}.select2-hidden-accessible{border:0!important;clip:rect(0 0 0 0)!important;height:1px!important;margin:-1px!important;overflow:hidden!important;padding:0!important;position:absolute!important;width:1px!important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:700}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir=rtl] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir=rtl] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:#fff;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none;margin:5px 5px 0 0}.select2-container--default .select2-selection--multiple .select2-selection__rendered li:before{content:'';display:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:700;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:700;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-search--inline,.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__placeholder{float:right}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir=rtl] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid #000 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple,.select2-container--default.select2-container--open.select2-container--above .select2-selection--single{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple,.select2-container--default.select2-container--open.select2-container--below .select2-selection--single{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:0 0;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[data-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected],.select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#0073aa;color:#fff}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(50%,#fff),to(#eee));background-image:linear-gradient(to bottom,#fff 50%,#eee 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--single:focus{border:1px solid #0073aa}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:700;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(50%,#eee),to(#ccc));background-image:linear-gradient(to bottom,#eee 50%,#ccc 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir=rtl] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir=rtl] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #0073aa}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:0 0;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-gradient(linear,left top,left bottom,from(white),color-stop(50%,#eee));background-image:linear-gradient(to bottom,#fff 0,#eee 50%);background-repeat:repeat-x}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(50%,#eee),to(white));background-image:linear-gradient(to bottom,#eee 50%,#fff 100%);background-repeat:repeat-x}.select2-container--classic .select2-selection--multiple{background-color:#fff;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #0073aa}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:700;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir=rtl] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #0073aa}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected],.select2-container--classic .select2-results__option--highlighted[data-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#0073aa} +@charset "UTF-8";:root{--woocommerce:#a46497;--wc-green:#7ad03a;--wc-red:#a00;--wc-orange:#ffba00;--wc-blue:#2ea2cc;--wc-primary:#a46497;--wc-primary-text:white;--wc-secondary:#ebe9eb;--wc-secondary-text:#515151;--wc-highlight:#77a464;--wc-highligh-text:white;--wc-content-bg:#fff;--wc-subtext:#767676}@-webkit-keyframes spin{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spin{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@font-face{font-family:star;src:url(../fonts/star.eot);src:url(../fonts/star.eot?#iefix) format("embedded-opentype"),url(../fonts/star.woff) format("woff"),url(../fonts/star.ttf) format("truetype"),url(../fonts/star.svg#star) format("svg");font-weight:400;font-style:normal}@font-face{font-family:WooCommerce;src:url(../fonts/WooCommerce.eot);src:url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"),url(../fonts/WooCommerce.woff) format("woff"),url(../fonts/WooCommerce.ttf) format("truetype"),url(../fonts/WooCommerce.svg#WooCommerce) format("svg");font-weight:400;font-style:normal}.blockUI.blockOverlay::before{height:1em;width:1em;display:block;position:absolute;top:50%;left:50%;margin-left:-.5em;margin-top:-.5em;content:"";-webkit-animation:spin 1s ease-in-out infinite;animation:spin 1s ease-in-out infinite;background:url(../images/icons/loader.svg) center center;background-size:cover;line-height:1;text-align:center;font-size:2em;color:rgba(0,0,0,.75)}.wc-addons-wrap .marketplace-header{background-image:url(../images/marketplace-header-bg@2x.png);background-position:right;background-size:cover;box-sizing:border-box;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:center;justify-content:center;min-height:216px;padding:24px 16px;width:100%}.wc-addons-wrap .marketplace-header__title{color:#fff;font-size:32px;font-style:normal;font-weight:400;line-height:1.15;margin-bottom:8px;padding:0}.wc-addons-wrap .marketplace-header__description{color:#fff;font-size:16px;line-height:24px;margin-bottom:24px;margin-top:0}.wc-addons-wrap .marketplace-header__search-form{clear:both;display:block;max-width:318px;position:relative}.wc-addons-wrap .marketplace-header__search-form input{border:1px solid #ddd;box-shadow:none;font-size:13px;height:48px;padding-left:16px;padding-right:50px;width:100%;margin:0}.wc-addons-wrap .marketplace-header__search-form button{background:0 0;border:none;cursor:pointer;height:48px;position:absolute;right:0;width:53px}.wc-addons-wrap .top-bar{background:#fff;box-shadow:inset 0 -1px 0 #ccc;display:block;height:60px;margin:0 0 16px}@media only screen and (min-width:768px){.wc-addons-wrap .top-bar{margin-bottom:24px}}.wc-addons-wrap .top-bar .current-section-dropdown{position:relative;width:100%}@media only screen and (min-width:600px){.wc-addons-wrap .top-bar .current-section-dropdown{margin-left:70px;width:288px}}.wc-addons-wrap .top-bar .current-section-name{cursor:pointer;font-weight:600;font-size:14px;line-height:20px;padding:20px 16px;position:relative}.wc-addons-wrap .top-bar .current-section-name::after{background-image:url(../images/icons/gridicons-chevron-down.svg);background-size:contain;content:"";display:block;height:20px;position:absolute;right:20px;top:20px;width:20px}.wc-addons-wrap .top-bar ul{background:#fff;border-radius:2px;display:none;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:left;justify-content:left;left:0;margin:0;padding:14px 0;position:absolute;top:50px;width:100%;z-index:10}@media only screen and (min-width:600px){.wc-addons-wrap .top-bar ul{border:1px solid #1e1e1e}}@media only screen and (min-width:1100px){.wc-addons-wrap .top-bar ul{-webkit-box-pack:center;justify-content:center}}.wc-addons-wrap .top-bar ul li{font-size:13px;line-height:16px;margin:0}.wc-addons-wrap .top-bar ul a,.wc-addons-wrap .top-bar ul a:focus,.wc-addons-wrap .top-bar ul a:hover,.wc-addons-wrap .top-bar ul a:visited{border:none;box-shadow:none;box-sizing:border-box;color:#1e1e1e;display:inline-block;text-decoration:none;outline:0;padding:14px 18px;position:relative;width:100%}@media only screen and (min-width:600px){.wc-addons-wrap .top-bar ul a,.wc-addons-wrap .top-bar ul a:focus,.wc-addons-wrap .top-bar ul a:hover,.wc-addons-wrap .top-bar ul a:visited{padding:10px 18px}}.wc-addons-wrap .top-bar ul a.current::after{background-image:url(../images/icons/gridicons-checkmark.svg);content:"";display:block;height:20px;position:absolute;right:20px;top:7px;width:20px}.wc-addons-wrap .top-bar .current-section-dropdown.is-open ul{display:-webkit-box;display:flex}.wc-addons-wrap .top-bar .current-section-dropdown.is-open .current-section-name::after{-webkit-transform:rotate(.5turn);-ms-transform:rotate(.5turn);transform:rotate(.5turn)}.wc-addons-wrap .update-plugins .update-count{background-color:#d54e21;border-radius:10px;color:#fff;display:inline-block;font-size:9px;font-weight:600;line-height:17px;margin:1px 0 0 2px;padding:0 6px;vertical-align:text-top}.wc-addons-wrap h1.search-form-title{clear:left;font-size:20px;font-family:sans-serif;line-height:1.2;margin:48px 0 16px;padding:0}.wc-addons-wrap .addons-featured{margin:0}.wc-addons-wrap ul.subsubsub.subsubsub{margin:-2px 0 12px}.wc-addons-wrap .subsubsub li::after{content:"|"}.wc-addons-wrap .subsubsub li:last-child::after{content:""}.wc-addons-wrap .addons-button{border-radius:3px;cursor:pointer;display:block;height:37px;line-height:37px;margin-top:16px;text-align:center;text-decoration:none;width:124px}.wc-addons-wrap .addons-wcs-banner-block{-webkit-box-align:center;align-items:center;background:#fff;border:1px solid #ddd;display:-webkit-box;display:flex;margin:0 0 1em 0;padding:2em 2em 1em}.wc-addons-wrap .addons-wcs-banner-block-image{background:#f7f7f7;border:1px solid #e6e6e6;margin-right:2em;padding:4em;max-width:200px}.wc-addons-wrap .addons-wcs-banner-block-image .addons-img{max-height:86px;max-width:97px}.wc-addons-wrap .addons-wcs-banner-block-image.is-full-image{padding:0;background:0 0;border:none}.wc-addons-wrap .addons-wcs-banner-block-image.is-full-image .addons-img{max-height:100%;max-width:100%}.wc-addons-wrap .addons-shipping-methods .addons-wcs-banner-block{margin-left:0;margin-right:0;margin-top:1em}.wc-addons-wrap .addons-wcs-banner-block-content{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;justify-content:space-around;align-self:stretch;padding:1em 0}.wc-addons-wrap .addons-wcs-banner-block-content h1{padding-bottom:0}.wc-addons-wrap .addons-wcs-banner-block-content p{margin-bottom:0}.wc-addons-wrap .addons-wcs-banner-block-content .wcs-logos-container{display:-webkit-box;display:flex;-webkit-box-align:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-pack:center;justify-content:center}@media screen and (min-width:500px){.wc-addons-wrap .addons-wcs-banner-block-content .wcs-logos-container{-webkit-box-pack:left;justify-content:left}}.wc-addons-wrap .addons-wcs-banner-block-content .wcs-logos-container li{margin-right:8px}.wc-addons-wrap .addons-wcs-banner-block-content .wcs-logos-container li:last-child{margin-right:0}.wc-addons-wrap .addons-wcs-banner-block-content .wcs-service-logo{max-width:45px}.wc-addons-wrap .addons-column{-webkit-box-flex:1;flex:1;width:50%;padding:0 .5em}.wc-addons-wrap .addons-column:nth-child(2){margin-right:0}.wc-addons-wrap .addons-small-dark-items{display:-webkit-box;display:flex;flex-wrap:wrap;justify-content:space-around}.wc-addons-wrap .addons-small-dark-item{margin:0 0 20px}.wc-addons-wrap .addons-small-dark-item-icon img{height:30px}.wc-addons-wrap .addons-small-dark-item a{margin:28px auto 0}.wc-addons-wrap .addons-button-solid{background-color:#674399;color:#fff}.wc-addons-wrap .addons-button-promoted{float:right;width:auto;padding:0 20px;margin-top:0}.wc-addons-wrap .addons-button-promoted:hover{opacity:.8}.wc-addons-wrap .addons-button-expandable{display:inline-block;padding:0 16px;width:auto}.wc-addons-wrap .addons-button-solid:hover{color:#fff;opacity:.8}.wc-addons-wrap .addons-button-outline-green{border:1px solid #73ae39;color:#73ae39}.wc-addons-wrap .addons-button-outline-green:hover{color:#73ae39;opacity:.8}.wc-addons-wrap .addons-button-outline-purple{border:1px solid #674399;color:#674399}.wc-addons-wrap .addons-button-outline-purple:hover{color:#674399;opacity:.8}.wc-addons-wrap .addons-button-outline-white{border:1px solid #fff;color:#fff}.wc-addons-wrap .addons-button-outline-white:hover{color:#fff;opacity:.8}.wc-addons-wrap .addons-button-installed{background:#e6e6e6;color:#3c3c3c}.wc-addons-wrap .addons-button-installed:hover{color:#3c3c3c;opacity:.8}@media only screen and (max-width:400px){.wc-addons-wrap .addons-featured{margin:-1% -5%}.wc-addons-wrap .addons-button{width:100%}.wc-addons-wrap .addons-small-dark-item{width:100%}}.wc-addons-wrap .marketplace-content-wrapper{font-family:helveticaneue-light,"Helvetica Neue Light","Helvetica Neue",sans-serif;margin:0 auto;max-width:1032px;width:100%}.wc-addons-wrap .addon-product-group-title{font-family:sans-serif;letter-spacing:.38px}.wc-addons-wrap .addon-product-group-description-container{-webkit-box-align:center;align-items:center;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;font-size:14px;-webkit-box-pack:justify;justify-content:space-between;line-height:20px}.wc-addons-wrap .addon-product-group-description-container .addon-product-group-see-more,.wc-addons-wrap .addon-product-group-description-container .addon-product-group-see-more:visited{color:#007cba;display:block;font-size:13px;text-decoration:none}.wc-addons-wrap .products{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row;flex-wrap:wrap;font-weight:400;-webkit-box-pack:justify;justify-content:space-between;margin:0;max-width:1032px;overflow:hidden}.wc-addons-wrap .products .product.addons-buttons-banner,.wc-addons-wrap .products .product.addons-product-banner{max-width:calc(100% - 2px)}@media screen and (min-width:960px){.wc-addons-wrap .products.addons-products-three-column li.product{max-width:calc(33.33% - 12px)}.wc-addons-wrap .products.addons-products-three-column li.product h2,.wc-addons-wrap .products.addons-products-three-column li.product h3{font-size:16px}}.wc-addons-wrap .products li{background:#fff;border:1px solid #dcdcde;border-radius:2px;display:-webkit-box;display:flex;-webkit-box-flex:1;flex:1 0 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:justify;justify-content:space-between;margin:12px 0;max-width:calc(50% - 12px);min-width:280px;min-height:220px;overflow:hidden;padding:0;vertical-align:top}.wc-addons-wrap .products li.addons-full-width{max-width:100%}@media only screen and (max-width:768px){.wc-addons-wrap .products li{max-width:none;width:100%}}.wc-addons-wrap .products li a{text-decoration:none}.wc-addons-wrap .products li .product-details{padding:24px;position:relative}.wc-addons-wrap .products li .product-details .product-img-wrap{display:block;margin-left:24px;position:absolute;right:24px;top:24px}.wc-addons-wrap .products li .product-details .product-img-wrap img{border-radius:3px;display:block;margin:0;max-width:48px;max-height:48px}.wc-addons-wrap .products li .product-details.addon-product-banner-details{-webkit-box-align:center;align-items:center;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-pack:justify;justify-content:space-between}.wc-addons-wrap .products li .product-details.addon-product-banner-details .product-img-wrap{position:unset}.wc-addons-wrap .products li .product-details.addon-product-banner-details .product-img-wrap img{max-width:150px;max-height:150px}.wc-addons-wrap .products li .product-details h2,.wc-addons-wrap .products li .product-details h3{color:#007cba;font-size:20px;font-weight:400;letter-spacing:-.32px;line-height:28px;margin:0!important;max-width:calc(100% - 48px)}.wc-addons-wrap .products li .product-details .addons-buttons-banner-details h2{color:#1d2327}.wc-addons-wrap .products li .product-details.featured .label,.wc-addons-wrap .products li .product-details.promoted .label{-webkit-box-align:center;align-items:center;border-radius:2px;background:#dcdcde;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;height:20px;-webkit-box-pack:end;justify-content:flex-end;margin-bottom:8px;max-width:52px;padding:3px 12px;top:28px;right:24px;text-align:center}.wc-addons-wrap .products li .product-details.featured .label.promoted,.wc-addons-wrap .products li .product-details.promoted .label.promoted{float:right;max-width:58px}.wc-addons-wrap .products li .product-details.featured h2,.wc-addons-wrap .products li .product-details.promoted h2{color:#2c3338}.wc-addons-wrap .products li .product-details p{color:#2c3338;font-size:14px;line-height:20px;margin:14px 64px 0 0;width:100%}.wc-addons-wrap .products li .product-details .addons-buttons-banner-details p{font-size:14px;margin-bottom:14px;max-width:none}.wc-addons-wrap .products li .product-details .product-developed-by{color:#50575e;font-size:12px;line-height:20px;margin-top:4px}.wc-addons-wrap .products li .product-details .product-developed-by .product-vendor-link{color:#50575e}.wc-addons-wrap .products li .product-details .product-developed-by{color:#50575e;font-size:12px;font-family:sans-serif;line-height:20px;margin-top:4px}.wc-addons-wrap .products li .product-details .product-developed-by .product-vendor-link{color:#50575e}.wc-addons-wrap .products li .product-footer{-webkit-box-align:center;align-items:center;border-top:1px solid #dcdcde;display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;-webkit-box-pack:justify;justify-content:space-between;padding:24px}.wc-addons-wrap .products li .product-footer .price{font-size:16px;color:#1d2327}.wc-addons-wrap .products li .product-footer .price-suffix{color:#646970}.wc-addons-wrap .products li .product-footer .product-reviews-block{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row;margin-top:4px}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-rating-star{background-repeat:no-repeat;background-size:contain;height:16px;margin:4px 4px 4px 0;width:17px}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-rating-star__fill{background-image:url(../images/icons/star-golden.svg)}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-rating-star__half-fill{background-image:url(../images/icons/star-half-filled.svg)}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-rating-star__no-fill{background-image:url(../images/icons/star-gray.svg)}.wc-addons-wrap .products li .product-footer .product-reviews-block .product-reviews-count{color:#646970;font-size:12px;font-family:sans-serif;line-height:24px;letter-spacing:-.154px;margin-left:4px}.wc-addons-wrap .products li .product-footer .button{background-color:#fff;border-color:#007cba;color:#007cba;float:right;font-size:13px;height:36px;line-height:30px;padding:2px 14px}.wc-addons-wrap .products .product-footer-promoted{-webkit-box-align:end;align-items:flex-end;display:-webkit-box;display:flex;-webkit-box-pack:justify;justify-content:space-between;padding:24px}.wc-addons-wrap .products .product-footer-promoted .icon img{border-radius:4px;width:80px}.wc-addons-wrap .products .addons-buttons-banner{display:-webkit-box;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row}.wc-addons-wrap .products .addons-buttons-banner .addons-buttons-banner-image{background-repeat:no-repeat;background-size:cover;height:190px;margin:24px;width:200px}.wc-addons-wrap .products .addons-buttons-banner .addons-buttons-banner-details-container{padding-left:0;width:calc(100% - 198px - 24px - 24px)}.wc-addons-wrap .products .addons-buttons-banner .addons-buttons-banner-details-container{display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;-webkit-box-pack:justify;justify-content:space-between}.wc-addons-wrap .products .addons-buttons-banner .button.addons-buttons-banner-button,.wc-addons-wrap .products .addons-buttons-banner .button.addons-buttons-banner-button:hover{background:#fff;border:1.5px solid #624594;color:#624594;padding:4px 12px;margin-right:16px}.wc-addons-wrap .products .addons-buttons-banner .button.addons-buttons-banner-button.addons-buttons-banner-button-primary,.wc-addons-wrap .products .addons-buttons-banner .button.addons-buttons-banner-button:hover.addons-buttons-banner-button-primary{background-color:#624594;color:#fff}.wc-addons-wrap .storefront{max-width:990px;background:url(../images/storefront-bg.jpg) bottom right #f6f6f6;border:1px solid #ddd;margin:1em auto;padding:24px;overflow:hidden;zoom:1}.wc-addons-wrap .storefront img{display:block;width:100%;max-width:400px;height:auto;margin:0 auto 16px;box-shadow:0 1px 6px rgba(0,0,0,.1)}.wc-addons-wrap .storefront p:last-of-type{margin-bottom:0}.wc-addons-wrap .storefront p{max-width:750px}.no-js .wc-addons-wrap .current-section-dropdown:hover ul,.no-touch .wc-addons-wrap .current-section-dropdown:hover ul{display:-webkit-box;display:flex}.no-js .wc-addons-wrap .current-section-dropdown:hover .current-section-name::after,.no-touch .wc-addons-wrap .current-section-dropdown:hover .current-section-name::after{-webkit-transform:rotate(.5turn);-ms-transform:rotate(.5turn);transform:rotate(.5turn)}.wc-subscriptions-wrap{max-width:1200px}.woocommerce-page-wc-marketplace .notice{margin-left:20px;margin-right:20px}.woocommerce-page-wc-marketplace.woocommerce-page .wrap{margin-top:32px}.woocommerce-page-wc-subscriptions #wpbody-content .screen-reader-text+.notice{margin-top:32px}.woocommerce-embed-page.woocommerce-page-wc-marketplace #screen-meta-links{position:absolute;right:0}.woocommerce-BlankState a.button-primary,.woocommerce-BlankState button.button-primary,.woocommerce-message a.button-primary,.woocommerce-message button.button-primary{background:#bb77ae;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597;color:#fff;text-shadow:0 -1px 1px #a36597,1px 0 1px #a36597,0 1px 1px #a36597,-1px 0 1px #a36597;display:inline-block}.woocommerce-BlankState a.button-primary:active,.woocommerce-BlankState a.button-primary:focus,.woocommerce-BlankState a.button-primary:hover,.woocommerce-BlankState button.button-primary:active,.woocommerce-BlankState button.button-primary:focus,.woocommerce-BlankState button.button-primary:hover,.woocommerce-message a.button-primary:active,.woocommerce-message a.button-primary:focus,.woocommerce-message a.button-primary:hover,.woocommerce-message button.button-primary:active,.woocommerce-message button.button-primary:focus,.woocommerce-message button.button-primary:hover{background:#a36597;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597}.woocommerce-message{position:relative;overflow:hidden}.woocommerce-message.updated{border-left-color:#cc99c2!important}.woocommerce-message a.docs,.woocommerce-message a.skip{text-decoration:none!important}.woocommerce-message a.woocommerce-message-close{position:static;float:right;padding:0 15px 10px 28px;margin-top:-10px;font-size:13px;line-height:1.23076923;text-decoration:none}.woocommerce-message a.woocommerce-message-close::before{position:relative;top:18px;left:-20px;-webkit-transition:all .1s ease-in-out;transition:all .1s ease-in-out}.woocommerce-message .twitter-share-button{margin-top:-3px;margin-left:3px;vertical-align:middle}#variable_product_options #message,#variable_product_options .notice{margin:10px}#variable_product_options .form-row select{max-width:100%}#variable_product_options .toolbar-top .button{margin:1px}#product_attributes .toolbar-top .button{margin:1px}.clear{clear:both}.wrap.woocommerce div.error,.wrap.woocommerce div.updated{margin-top:10px}mark.amount{background:transparent none;color:inherit}.woocommerce-help-tip{color:#666;display:inline-block;font-size:1.1em;font-style:normal;height:16px;line-height:16px;position:relative;vertical-align:middle;width:16px}.woocommerce-help-tip::after{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";cursor:help}.wc-wp-version-gte-53 .woocommerce-help-tip{font-size:1.2em;cursor:help}h2 .woocommerce-help-tip{margin-top:-5px;margin-left:.25em}table.wc_status_table{margin-bottom:1em}table.wc_status_table h2{font-size:14px;margin:0}table.wc_status_table tr:nth-child(2n) td,table.wc_status_table tr:nth-child(2n) th{background:#fcfcfc}table.wc_status_table th{font-weight:700;padding:9px}table.wc_status_table td:first-child{width:33%}table.wc_status_table td.help{width:1em}table.wc_status_table td,table.wc_status_table th{font-size:1.1em;font-weight:400}table.wc_status_table td.run-tool,table.wc_status_table th.run-tool{text-align:right}table.wc_status_table td strong.name,table.wc_status_table th strong.name{display:block;margin-bottom:.5em}table.wc_status_table td mark,table.wc_status_table th mark{background:transparent none}table.wc_status_table td mark.yes,table.wc_status_table th mark.yes{color:#7ad03a}table.wc_status_table td mark.no,table.wc_status_table th mark.no{color:#999}table.wc_status_table td .red,table.wc_status_table td mark.error,table.wc_status_table th .red,table.wc_status_table th mark.error{color:#a00}table.wc_status_table td ul,table.wc_status_table th ul{margin:0}table.wc_status_table .help_tip{cursor:help}table.wc_status_table--tools td,table.wc_status_table--tools th{padding:2em}.taxonomy-product_cat .check-column .woocommerce-help-tip{font-size:1.5em;margin:-3px 0 0 5px;display:block;position:absolute}#debug-report{display:none;margin:10px 0;padding:0;position:relative}#debug-report textarea{font-family:monospace;width:100%;margin:0;height:300px;padding:20px;border-radius:0;resize:none;font-size:12px;line-height:20px;outline:0}.wp-list-table.logs .log-level{display:inline;padding:.2em .6em .3em;font-size:80%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.2em}.wp-list-table.logs .log-level:empty{display:none}.wp-list-table.logs .log-level--alert,.wp-list-table.logs .log-level--emergency{background-color:#ff4136}.wp-list-table.logs .log-level--critical,.wp-list-table.logs .log-level--error{background-color:#ff851b}.wp-list-table.logs .log-level--notice,.wp-list-table.logs .log-level--warning{color:#222;background-color:#ffdc00}.wp-list-table.logs .log-level--info{background-color:#0074d9}.wp-list-table.logs .log-level--debug{background-color:#3d9970}@media screen and (min-width:783px){.wp-list-table.logs .column-timestamp{width:18%}.wp-list-table.logs .column-level{width:14%}.wp-list-table.logs .column-source{width:15%}}#log-viewer-select{padding:10px 0 8px;line-height:28px}#log-viewer-select h2 a{vertical-align:middle}#log-viewer{background:#fff;border:1px solid #e5e5e5;box-shadow:0 1px 1px rgba(0,0,0,.04);padding:5px 20px}#log-viewer pre{font-family:monospace;white-space:pre-wrap;word-wrap:break-word}.inline-edit-product.quick-edit-row .inline-edit-col-center,.inline-edit-product.quick-edit-row .inline-edit-col-right{float:right!important}#woocommerce-fields.inline-edit-col{clear:left}#woocommerce-fields.inline-edit-col label.featured,#woocommerce-fields.inline-edit-col label.manage_stock{margin-left:10px}#woocommerce-fields.inline-edit-col label.stock_status_field{clear:both;float:left}#woocommerce-fields.inline-edit-col .dimensions div{display:block;margin:.2em 0}#woocommerce-fields.inline-edit-col .dimensions div span.title{display:block;float:left;width:5em}#woocommerce-fields.inline-edit-col .dimensions div span.input-text-wrap{display:block;margin-left:5em}#woocommerce-fields.inline-edit-col .text{box-sizing:border-box;width:99%;float:left;margin:1px 1% 1px 1px}#woocommerce-fields.inline-edit-col .height,#woocommerce-fields.inline-edit-col .length,#woocommerce-fields.inline-edit-col .width{width:32.33%}#woocommerce-fields.inline-edit-col .height{margin-right:0}#woocommerce-fields-bulk.inline-edit-col label{clear:left}#woocommerce-fields-bulk.inline-edit-col .inline-edit-group label{clear:none;width:49%;margin:.2em 0}#woocommerce-fields-bulk.inline-edit-col .inline-edit-group.dimensions label{width:75%;max-width:75%}#woocommerce-fields-bulk.inline-edit-col .length,#woocommerce-fields-bulk.inline-edit-col .regular_price,#woocommerce-fields-bulk.inline-edit-col .sale_price,#woocommerce-fields-bulk.inline-edit-col .stock,#woocommerce-fields-bulk.inline-edit-col .weight{box-sizing:border-box;width:100%;margin-left:4.4em}#woocommerce-fields-bulk.inline-edit-col .height,#woocommerce-fields-bulk.inline-edit-col .length,#woocommerce-fields-bulk.inline-edit-col .width{box-sizing:border-box;width:25%}.column-coupon_code{line-height:2.25em}.column-coupon_code,ul.wc_coupon_list{margin:0;overflow:hidden;zoom:1;clear:both}ul.wc_coupon_list{padding-bottom:5px}ul.wc_coupon_list li{margin:0}ul.wc_coupon_list li.code{display:inline-block;position:relative;padding:0 .5em;background-color:#fff;border:1px solid #aaa;box-shadow:0 1px 0 #dfdfdf;border-radius:4px;margin-right:5px;margin-top:5px}ul.wc_coupon_list li.code.editable{padding-right:2em}ul.wc_coupon_list li.code .tips{cursor:pointer}ul.wc_coupon_list li.code .tips span{color:#888}ul.wc_coupon_list li.code .tips span:hover{color:#000}ul.wc_coupon_list li.code .remove-coupon{text-decoration:none;color:#888;position:absolute;top:7px;right:20px}ul.wc_coupon_list li.code .remove-coupon::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}ul.wc_coupon_list li.code .remove-coupon:hover::before{color:#a00}ul.wc_coupon_list_block{margin:0;padding-bottom:2px}ul.wc_coupon_list_block li{border-top:1px solid #fff;border-bottom:1px solid #ccc;line-height:2.5em;margin:0;padding:.5em 0}ul.wc_coupon_list_block li:first-child{border-top:0;padding-top:0}ul.wc_coupon_list_block li:last-child{border-bottom:0;padding-bottom:0}.button.wc-reload{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;padding:0;height:28px;width:28px!important;display:inline-block}.button.wc-reload::after{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";line-height:28px}#woocommerce-order-data .handlediv,#woocommerce-order-data .hndle,#woocommerce-order-data .postbox-header{display:none}#woocommerce-order-data .inside{display:block!important}#order_data{padding:23px 24px 12px}#order_data h2{margin:0;font-family:HelveticaNeue-Light,"Helvetica Neue Light","Helvetica Neue",sans-serif;font-size:21px;font-weight:400;line-height:1.2;text-shadow:1px 1px 1px #fff;padding:0}#order_data h3{font-size:14px}#order_data h3,#order_data h4{color:#333;margin:1.33em 0 0}#order_data p{color:#777}#order_data p.order_number{margin:0;font-family:HelveticaNeue-Light,"Helvetica Neue Light","Helvetica Neue",sans-serif;font-weight:400;line-height:1.6em;font-size:16px}#order_data .order_data_column_container{clear:both}#order_data .order_data_column_container p._billing_email_field{margin-top:13px}#order_data .order_data_column{width:32%;padding:0 2% 0 0;float:left}#order_data .order_data_column>h3 span{display:block}#order_data .order_data_column:last-child{padding-right:0}#order_data .order_data_column p{padding:0!important}#order_data .order_data_column .address strong{display:block}#order_data .order_data_column .form-field{float:left;clear:left;width:48%;padding:0;margin:9px 0 0}#order_data .order_data_column .form-field label{display:block;padding:0 0 3px}#order_data .order_data_column .form-field input,#order_data .order_data_column .form-field textarea{width:100%}#order_data .order_data_column .form-field select{width:100%;max-width:100%}#order_data .order_data_column .form-field .select2-container{width:100%!important}#order_data .order_data_column .form-field .date-picker{width:50%}#order_data .order_data_column .form-field .hour,#order_data .order_data_column .form-field .minute{width:3.5em}#order_data .order_data_column .form-field small{display:block;margin:5px 0 0;color:#999}#order_data .order_data_column ._billing_address_2_field,#order_data .order_data_column ._billing_last_name_field,#order_data .order_data_column ._billing_phone_field,#order_data .order_data_column ._billing_postcode_field,#order_data .order_data_column ._billing_state_field,#order_data .order_data_column ._shipping_address_2_field,#order_data .order_data_column ._shipping_last_name_field,#order_data .order_data_column ._shipping_postcode_field,#order_data .order_data_column ._shipping_state_field,#order_data .order_data_column .form-field.last{float:right;clear:right}#order_data .order_data_column ._billing_company_field,#order_data .order_data_column ._shipping_company_field,#order_data .order_data_column ._transaction_id_field,#order_data .order_data_column .form-field-wide{width:100%;clear:both}#order_data .order_data_column ._billing_company_field .wc-category-search,#order_data .order_data_column ._billing_company_field .wc-customer-search,#order_data .order_data_column ._billing_company_field .wc-enhanced-select,#order_data .order_data_column ._billing_company_field input,#order_data .order_data_column ._billing_company_field select,#order_data .order_data_column ._billing_company_field textarea,#order_data .order_data_column ._shipping_company_field .wc-category-search,#order_data .order_data_column ._shipping_company_field .wc-customer-search,#order_data .order_data_column ._shipping_company_field .wc-enhanced-select,#order_data .order_data_column ._shipping_company_field input,#order_data .order_data_column ._shipping_company_field select,#order_data .order_data_column ._shipping_company_field textarea,#order_data .order_data_column ._transaction_id_field .wc-category-search,#order_data .order_data_column ._transaction_id_field .wc-customer-search,#order_data .order_data_column ._transaction_id_field .wc-enhanced-select,#order_data .order_data_column ._transaction_id_field input,#order_data .order_data_column ._transaction_id_field select,#order_data .order_data_column ._transaction_id_field textarea,#order_data .order_data_column .form-field-wide .wc-category-search,#order_data .order_data_column .form-field-wide .wc-customer-search,#order_data .order_data_column .form-field-wide .wc-enhanced-select,#order_data .order_data_column .form-field-wide input,#order_data .order_data_column .form-field-wide select,#order_data .order_data_column .form-field-wide textarea{width:100%}#order_data .order_data_column p.none_set{color:#999}#order_data .order_data_column div.edit_address{display:none;zoom:1;padding-right:1px}#order_data .order_data_column div.edit_address .select2-container .select2-selection--single{height:32px}#order_data .order_data_column div.edit_address .select2-container .select2-selection--single .select2-selection__rendered{line-height:32px}#order_data .order_data_column .wc-customer-user label a,#order_data .order_data_column .wc-order-status label a{float:right;margin-left:8px}#order_data .order_data_column a.edit_address{width:14px;height:0;padding:14px 0 0;margin:0 0 0 6px;overflow:hidden;position:relative;color:#999;border:0;float:right}#order_data .order_data_column a.edit_address:focus,#order_data .order_data_column a.edit_address:hover{color:#000}#order_data .order_data_column a.edit_address::after{font-family:WooCommerce;position:absolute;top:0;left:0;text-align:center;vertical-align:top;line-height:14px;font-size:14px;font-weight:400}#order_data .order_data_column a.edit_address::after{font-family:Dashicons;content:"\f464"}#order_data .order_data_column .billing-same-as-shipping,#order_data .order_data_column .load_customer_billing,#order_data .order_data_column .load_customer_shipping{font-size:13px;display:inline-block;font-weight:400}#order_data .order_data_column .load_customer_shipping{margin-right:.3em}.order_actions{margin:0;overflow:hidden;zoom:1}.order_actions li{border-top:1px solid #fff;border-bottom:1px solid #ddd;padding:6px 0;margin:0;line-height:1.6em;float:left;width:50%;text-align:center}.order_actions li a{float:none;text-align:center;text-decoration:underline}.order_actions li.wide{width:auto;float:none;clear:both;padding:6px;text-align:left;overflow:hidden}.order_actions li #delete-action{line-height:25px;vertical-align:middle;text-align:left;float:left}.order_actions li .save_order{float:right}.order_actions li#actions{overflow:hidden}.order_actions li#actions .button{width:24px;box-sizing:border-box;float:right}.order_actions li#actions select{width:225px;box-sizing:border-box;float:left}#woocommerce-order-items .inside{margin:0;padding:0;background:#fefefe}#woocommerce-order-items .wc-order-data-row{border-bottom:1px solid #dfdfdf;padding:1.5em 2em;background:#f8f8f8;line-height:2em;text-align:right}#woocommerce-order-items .wc-order-data-row::after,#woocommerce-order-items .wc-order-data-row::before{content:" ";display:table}#woocommerce-order-items .wc-order-data-row::after{clear:both}#woocommerce-order-items .wc-order-data-row p{margin:0;line-height:2em}#woocommerce-order-items .wc-order-data-row .wc-used-coupons{text-align:left}#woocommerce-order-items .wc-order-data-row .wc-used-coupons .tips{display:inline-block}#woocommerce-order-items .wc-used-coupons{float:left;width:50%}#woocommerce-order-items .wc-order-totals{float:right;width:50%;margin:0;padding:0;text-align:right}#woocommerce-order-items .wc-order-totals .amount{font-weight:700}#woocommerce-order-items .wc-order-totals .label{vertical-align:top}#woocommerce-order-items .wc-order-totals .total{font-size:1em!important;width:10em;margin:0 0 0 .5em;box-sizing:border-box}#woocommerce-order-items .wc-order-totals .total input[type=text]{width:96%;float:right}#woocommerce-order-items .wc-order-totals .refunded-total{color:#a00}#woocommerce-order-items .wc-order-totals .label-highlight{font-weight:700}#woocommerce-order-items .refund-actions{margin-top:5px;padding-top:12px;border-top:1px solid #dfdfdf}#woocommerce-order-items .refund-actions .button{float:right;margin-left:4px}#woocommerce-order-items .refund-actions .cancel-action{float:left;margin-left:0}#woocommerce-order-items .add_meta{margin-left:0!important}#woocommerce-order-items h3 small{color:#999}#woocommerce-order-items .amount{white-space:nowrap}#woocommerce-order-items .add-items .description{margin-right:10px}#woocommerce-order-items .add-items .button{float:left;margin-right:.25em}#woocommerce-order-items .add-items .button-primary{float:none;margin-right:0}#woocommerce-order-items .inside{display:block!important}#woocommerce-order-items .handlediv,#woocommerce-order-items .hndle,#woocommerce-order-items .postbox-header{display:none}#woocommerce-order-items .woocommerce_order_items_wrapper{margin:0;overflow-x:auto}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items{width:100%;background:#fff}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th{text-align:left;padding:1em;font-weight:400;color:#999;background:#f8f8f8;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th.sortable{cursor:pointer}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th:last-child{padding-right:2em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th:first-child{padding-left:2em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items thead th .wc-arrow{float:right;position:relative;margin-right:-1em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td{padding:1.5em 1em 1em;text-align:left;line-height:1.5em;vertical-align:top;border-bottom:1px solid #f8f8f8}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th textarea,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td textarea{width:100%}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td select{width:50%}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th textarea,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td textarea{font-size:14px;padding:4px;color:#555}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th:last-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td:last-child{padding-right:2em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody th:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td:first-child{padding-left:2em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody tr:last-child td{border-bottom:1px solid #dfdfdf}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody tr:first-child td{border-top:8px solid #f8f8f8}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tbody#order_line_items tr:first-child td{border-top:none}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.thumb{text-align:left;width:38px;padding-bottom:1.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.thumb .wc-order-item-thumbnail{width:38px;height:38px;border:2px solid #e8e8e8;background:#f8f8f8;color:#ccc;position:relative;font-size:21px;display:block;text-align:center}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.thumb .wc-order-item-thumbnail::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";width:38px;line-height:38px;display:block}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.thumb .wc-order-item-thumbnail img{width:100%;height:100%;margin:0;padding:0;position:relative}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.name .wc-order-item-sku,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.name .wc-order-item-variation{display:block;margin-top:.5em;font-size:.92em!important;color:#888}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item{min-width:200px}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .center,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .variation-id{text-align:center}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class{text-align:right}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class label{white-space:nowrap;color:#999;font-size:.833em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax label input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class label input{display:inline}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class input{width:70px;vertical-align:middle;text-align:right}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax select,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class select{width:85px;height:26px;vertical-align:middle;font-size:1em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input{display:inline-block;background:#fff;border:1px solid #ddd;box-shadow:inset 0 1px 2px rgba(0,0,0,.07);margin:1px 0;min-width:80px;overflow:hidden;line-height:1em;text-align:right}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input{width:100%;box-sizing:border-box}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input label{font-size:.75em;padding:4px 6px 0;color:#555;display:block}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input input{width:100%;box-sizing:border-box;border:0;box-shadow:none;margin:0;padding:0 6px 4px;color:#555;background:0 0}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input input::-webkit-input-placeholder,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input input::-webkit-input-placeholder{color:#ddd}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input:first-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input:first-child{border-bottom:1px dashed #ddd;background:#fff}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input:first-child label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input:first-child label{color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .split-input div.input:first-child input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .split-input div.input:first-child input{color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .view,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .view{white-space:nowrap}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .edit,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .edit{text-align:left}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class del,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class small.times{font-size:.92em!important;color:#888}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-taxes,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-refund-fields,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-taxes{margin:0}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-taxes label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-refund-fields label,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-taxes label{display:block}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax .wc-order-item-discount,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class .wc-order-item-discount{display:block;margin-top:.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .item_cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_cost small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .line_tax small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax small.times,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .tax_class small.times{margin-right:.25em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity{text-align:center}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .quantity input{text-align:center;width:50px}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items span.subtotal{opacity:.5}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.tax_class,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.tax_class{text-align:left}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .calculated{border-color:#ae8ca2;border-style:dotted}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta{width:100%}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta{margin:.5em 0 0;font-size:.92em!important;color:#888}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr th,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr th{border:0;padding:0 4px .5em 0;line-height:1.5em;width:20%}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td{padding:0 4px .5em 0;border:0;line-height:1.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td input,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td input{width:100%;margin:0;position:relative;border-bottom:0;box-shadow:none}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td textarea,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td textarea{width:100%;height:4em;margin:0;box-shadow:none}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td input:focus+textarea,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td input:focus+textarea{border-top-color:#999}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td p,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td p{margin:0 0 .5em;line-height:1.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.display_meta tr td p:last-child,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items table.meta tr td p:last-child{margin:0}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items .refund_by{border-bottom:1px dotted #999}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.fee .thumb div{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.5em;line-height:1em;vertical-align:middle;margin:0 auto}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.fee .thumb div::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.refund .thumb div{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.5em;line-height:1em;vertical-align:middle;margin:0 auto}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.refund .thumb div::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.shipping .thumb div{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.5em;line-height:1em;vertical-align:middle;margin:0 auto}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.shipping .thumb div::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#ccc}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.shipping .shipping_method,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items tr.shipping .shipping_method_name{width:100%;margin:0 0 .5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax{white-space:nowrap}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.line_tax .delete-order-tax,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax .delete-order-tax{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;float:right;font-size:14px;visibility:hidden;margin:3px -18px 0 0}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.line_tax .delete-order-tax::before,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax .delete-order-tax::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#999}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.line_tax .delete-order-tax:hover::before,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax .delete-order-tax:hover::before{color:#a00}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items td.line_tax:hover .delete-order-tax,#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items th.line_tax:hover .delete-order-tax{visibility:visible}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items small.refunded{display:block;color:#a00;white-space:nowrap;margin-top:.5em}#woocommerce-order-items .woocommerce_order_items_wrapper table.woocommerce_order_items small.refunded::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";position:relative;top:auto;left:auto;margin:-1px 4px 0 0;vertical-align:middle;line-height:1em}#woocommerce-order-items .wc-order-edit-line-item{padding-left:0}#woocommerce-order-items .wc-order-edit-line-item-actions{width:44px;text-align:right;padding-left:0;vertical-align:middle}#woocommerce-order-items .wc-order-edit-line-item-actions a{color:#ccc;display:inline-block;cursor:pointer;padding:0 0 .5em;margin:0 0 0 12px;vertical-align:middle;text-decoration:none;line-height:16px;width:16px;overflow:hidden}#woocommerce-order-items .wc-order-edit-line-item-actions a::before{margin:0;padding:0;font-size:16px;width:16px;height:16px}#woocommerce-order-items .wc-order-edit-line-item-actions a:hover::before{color:#999}#woocommerce-order-items .wc-order-edit-line-item-actions a:first-child{margin-left:0}#woocommerce-order-items .wc-order-edit-line-item-actions .edit-order-item::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";position:relative}#woocommerce-order-items .wc-order-edit-line-item-actions .delete-order-item::before,#woocommerce-order-items .wc-order-edit-line-item-actions .delete_refund::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";position:relative}#woocommerce-order-items .wc-order-edit-line-item-actions .delete-order-item:hover::before,#woocommerce-order-items .wc-order-edit-line-item-actions .delete_refund:hover::before{color:#a00}#woocommerce-order-items tbody tr .wc-order-edit-line-item-actions{visibility:hidden}#woocommerce-order-items tbody tr:hover .wc-order-edit-line-item-actions{visibility:visible}#woocommerce-order-items .wc-order-totals .wc-order-edit-line-item-actions{width:1.5em;visibility:visible!important}#woocommerce-order-items .wc-order-totals .wc-order-edit-line-item-actions a{padding:0}#woocommerce-order-downloads .buttons{float:left;padding:0;margin:0;vertical-align:top}#woocommerce-order-downloads .buttons .add_item_id,#woocommerce-order-downloads .buttons .select2-container{width:400px!important;margin-right:9px;vertical-align:top;float:left}#woocommerce-order-downloads .buttons button{margin:2px 0 0}#woocommerce-order-downloads h3 small{color:#999}#poststuff #woocommerce-order-actions .inside{margin:0;padding:0}#poststuff #woocommerce-order-actions .inside ul.order_actions li{padding:6px 10px;box-sizing:border-box}#poststuff #woocommerce-order-actions .inside ul.order_actions li:last-child{border-bottom:0}#poststuff #woocommerce-order-actions .inside button{margin:1px}#poststuff #woocommerce-order-notes .inside{margin:0;padding:0}#poststuff #woocommerce-order-notes .inside ul.order_notes li{padding:0 10px}#poststuff #woocommerce-order-notes .inside button{margin:1px;vertical-align:top}#woocommerce_customers p.search-box{margin:6px 0 4px;float:left}#woocommerce_customers .tablenav{float:right;clear:none}.widefat.customers td{vertical-align:middle;padding:4px 7px}.widefat .column-order_title{width:15%}.widefat .column-order_title time{display:block;color:#999;margin:3px 0}.widefat .column-orders,.widefat .column-paying,.widefat .column-spent{text-align:center;width:8%}.widefat .column-last_order{width:11%}.widefat .column-wc_actions{width:110px}.widefat .column-wc_actions a.button{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;display:inline-block;margin:2px 4px 2px 0;padding:0!important;height:2em!important;width:2em;overflow:hidden;vertical-align:middle}.widefat .column-wc_actions a.button::after{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;line-height:1.85}.widefat .column-wc_actions a.button img{display:block;width:12px;height:auto}.widefat .column-wc_actions a.edit::after{content:"\f464"}.widefat .column-wc_actions a.link::after{font-family:WooCommerce;content:"\e00d"}.widefat .column-wc_actions a.view::after{content:"\f177"}.widefat .column-wc_actions a.refresh::after{font-family:WooCommerce;content:"\e031"}.widefat .column-wc_actions a.processing::after{font-family:WooCommerce;content:"\e00f"}.widefat .column-wc_actions a.complete::after{content:"\f147"}.widefat small.meta{display:block;color:#999;font-size:inherit;margin:3px 0}.wc-wp-version-gte-53 .widefat .column-wc_actions a.button::after{margin-top:2px}.post-type-shop_order .tablenav .one-page .displaying-num{display:none}.post-type-shop_order .tablenav .select2-selection--single{height:32px}.post-type-shop_order .tablenav .select2-selection--single .select2-selection__rendered{line-height:29px}.post-type-shop_order .tablenav .select2-selection--single .select2-selection__arrow{height:30px}.post-type-shop_order .wp-list-table{margin-top:1em}.post-type-shop_order .wp-list-table tfoot th,.post-type-shop_order .wp-list-table thead th{padding:.75em 1em}.post-type-shop_order .wp-list-table tfoot th.sortable a,.post-type-shop_order .wp-list-table tfoot th.sorted a,.post-type-shop_order .wp-list-table thead th.sortable a,.post-type-shop_order .wp-list-table thead th.sorted a{padding:0}.post-type-shop_order .wp-list-table tfoot th:first-child,.post-type-shop_order .wp-list-table thead th:first-child{padding-left:2em}.post-type-shop_order .wp-list-table tfoot th:last-child,.post-type-shop_order .wp-list-table thead th:last-child{padding-right:2em}.post-type-shop_order .wp-list-table tbody td,.post-type-shop_order .wp-list-table tbody th{padding:1em;line-height:26px}.post-type-shop_order .wp-list-table tbody td:first-child{padding-left:2em}.post-type-shop_order .wp-list-table tbody td:last-child{padding-right:2em}.post-type-shop_order .wp-list-table tbody tr{border-top:1px solid #f5f5f5}.post-type-shop_order .wp-list-table tbody tr:hover:not(.status-trash):not(.no-link) td{cursor:pointer}.post-type-shop_order .wp-list-table .no-link{cursor:default!important}.post-type-shop_order .wp-list-table td,.post-type-shop_order .wp-list-table th{width:12ch;vertical-align:middle}.post-type-shop_order .wp-list-table td p,.post-type-shop_order .wp-list-table th p{margin:0}.post-type-shop_order .wp-list-table .check-column{width:1px;white-space:nowrap;padding:1em 1em 1em 1em!important;vertical-align:middle}.post-type-shop_order .wp-list-table .check-column input{vertical-align:text-top;margin:1px 0}.post-type-shop_order .wp-list-table .column-order_number{width:20ch}.post-type-shop_order .wp-list-table .column-order_total{width:8ch;text-align:right}.post-type-shop_order .wp-list-table .column-order_total a span{float:right}.post-type-shop_order .wp-list-table .column-order_date,.post-type-shop_order .wp-list-table .column-order_status{width:10ch}.post-type-shop_order .wp-list-table .column-order_status{width:14ch}.post-type-shop_order .wp-list-table .column-billing_address,.post-type-shop_order .wp-list-table .column-shipping_address{width:20ch;line-height:1.5em}.post-type-shop_order .wp-list-table .column-billing_address .description,.post-type-shop_order .wp-list-table .column-shipping_address .description{display:block;color:#999}.post-type-shop_order .wp-list-table .column-wc_actions{text-align:right}.post-type-shop_order .wp-list-table .column-wc_actions a.button{text-indent:9999px;margin:2px 0 2px 4px}.post-type-shop_order .wp-list-table .order-preview{float:right;width:16px;padding:20px 4px 4px 4px;height:0;overflow:hidden;position:relative;border:2px solid transparent;border-radius:4px}.post-type-shop_order .wp-list-table .order-preview::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";line-height:16px;font-size:14px;vertical-align:middle;top:4px}.post-type-shop_order .wp-list-table .order-preview:hover{border:2px solid #00a0d2}.post-type-shop_order .wp-list-table .order-preview.disabled::before{content:"";background:url(../images/wpspin-2x.gif) no-repeat center top;background-size:71%}.order-status{display:-webkit-inline-box;display:inline-flex;line-height:2.5em;color:#777;background:#e5e5e5;border-radius:4px;border-bottom:1px solid rgba(0,0,0,.05);margin:-.25em 0;cursor:inherit!important;white-space:nowrap;max-width:100%}.order-status.status-completed{background:#c8d7e1;color:#2e4453}.order-status.status-on-hold{background:#f8dda7;color:#94660c}.order-status.status-failed{background:#eba3a3;color:#761919}.order-status.status-processing{background:#c6e1c6;color:#5b841b}.order-status.status-trash{background:#eba3a3;color:#761919}.order-status>span{margin:0 1em;overflow:hidden;text-overflow:ellipsis}.wc-order-preview .order-status{float:right;margin-right:54px}.wc-order-preview article{padding:0!important}.wc-order-preview .modal-close{border-radius:0}.wc-order-preview .wc-order-preview-table{width:100%;margin:0}.wc-order-preview .wc-order-preview-table td,.wc-order-preview .wc-order-preview-table th{padding:1em 1.5em;text-align:left;border:0;border-bottom:1px solid #eee;margin:0;background:0 0;box-shadow:none;text-align:right;vertical-align:top}.wc-order-preview .wc-order-preview-table td:first-child,.wc-order-preview .wc-order-preview-table th:first-child{text-align:left}.wc-order-preview .wc-order-preview-table th{border-color:#ccc}.wc-order-preview .wc-order-preview-table tr:last-child td{border:0}.wc-order-preview .wc-order-preview-table .wc-order-item-sku{margin-top:.5em}.wc-order-preview .wc-order-preview-table .wc-order-item-meta{margin-top:.5em}.wc-order-preview .wc-order-preview-table .wc-order-item-meta td,.wc-order-preview .wc-order-preview-table .wc-order-item-meta th{padding:0;border:0;text-align:left;vertical-align:top}.wc-order-preview .wc-order-preview-table .wc-order-item-meta td:last-child{padding-left:.5em}.wc-order-preview .wc-order-preview-addresses{overflow:hidden;padding-bottom:1.5em}.wc-order-preview .wc-order-preview-addresses .wc-order-preview-address,.wc-order-preview .wc-order-preview-addresses .wc-order-preview-note{width:50%;float:left;padding:1.5em 1.5em 0;box-sizing:border-box;word-wrap:break-word}.wc-order-preview .wc-order-preview-addresses .wc-order-preview-address h2,.wc-order-preview .wc-order-preview-addresses .wc-order-preview-note h2{margin-top:0}.wc-order-preview .wc-order-preview-addresses .wc-order-preview-address strong,.wc-order-preview .wc-order-preview-addresses .wc-order-preview-note strong{display:block;margin-top:1.5em}.wc-order-preview .wc-order-preview-addresses .wc-order-preview-address strong:first-child,.wc-order-preview .wc-order-preview-addresses .wc-order-preview-note strong:first-child{margin-top:0}.wc-order-preview footer .wc-action-button-group{display:inline-block;float:left}.wc-order-preview footer .button.button-large{margin-left:10px;padding:0 10px!important;line-height:28px;height:auto;display:inline-block}.wc-order-preview .wc-action-button-group label{display:none}.wc-action-button-group{vertical-align:middle;line-height:26px;text-align:left}.wc-action-button-group label{margin-right:6px;cursor:default;font-weight:700;line-height:28px}.wc-action-button-group .wc-action-button-group__items{display:-webkit-inline-box;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;align-content:flex-start;-webkit-box-pack:start;justify-content:flex-start}.wc-action-button-group .wc-action-button{margin:0 0 0 -1px!important;border:1px solid #ccc;padding:0 10px!important;border-radius:0!important;float:none;line-height:28px;height:auto;z-index:1;position:relative;overflow:hidden;text-overflow:ellipsis;-webkit-box-flex:1;flex:1 0 auto;box-sizing:border-box;text-align:center;white-space:nowrap}.wc-action-button-group .wc-action-button:focus,.wc-action-button-group .wc-action-button:hover{border:1px solid #999;z-index:2}.wc-action-button-group .wc-action-button:first-child{margin-left:0!important;border-top-left-radius:3px!important;border-bottom-left-radius:3px!important}.wc-action-button-group .wc-action-button:last-child{border-top-right-radius:3px!important;border-bottom-right-radius:3px!important}@media screen and (max-width:782px){.wc-order-preview footer .wc-action-button-group .wc-action-button-group__items{display:-webkit-box;display:flex}.wc-order-preview footer .wc-action-button-group{float:none;display:block;margin-bottom:4px}.wc-order-preview footer .button.button-large{width:100%;float:none;text-align:center;margin:0;display:block}.post-type-shop_order .wp-list-table td.check-column{width:1em}.post-type-shop_order .wp-list-table td.column-order_number{padding-left:0;padding-bottom:.5em}.post-type-shop_order .wp-list-table td.column-order_date,.post-type-shop_order .wp-list-table td.column-order_status{display:inline-block!important;padding:0 1em 1em 1em!important}.post-type-shop_order .wp-list-table td.column-order_date::before,.post-type-shop_order .wp-list-table td.column-order_status::before{display:none!important}.post-type-shop_order .wp-list-table td.column-order_date{padding-left:0!important}.post-type-shop_order .wp-list-table td.column-order_status{float:right}}.column-customer_message .note-on{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;margin:0 auto;color:#999}.column-customer_message .note-on::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";line-height:16px}.column-order_notes .note-on{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;margin:0 auto;color:#999}.column-order_notes .note-on::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";line-height:16px}.attributes-table td,.attributes-table th{width:15%;vertical-align:top}.attributes-table .attribute-terms{width:32%}.attributes-table .attribute-actions{width:2em}.attributes-table .attribute-actions .configure-terms{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;padding:0!important;height:2em!important;width:2em}.attributes-table .attribute-actions .configure-terms::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";font-family:Dashicons;line-height:1.85}ul.order_notes{padding:2px 0 0}ul.order_notes li .note_content{padding:10px;background:#efefef;position:relative}ul.order_notes li .note_content p{margin:0;padding:0;word-wrap:break-word}ul.order_notes li p.meta{padding:10px;color:#999;margin:0;font-size:11px}ul.order_notes li p.meta .exact-date{border-bottom:1px dotted #999}ul.order_notes li a.delete_note{color:#a00}ul.order_notes li .note_content::after{content:"";display:block;position:absolute;bottom:-10px;left:20px;width:0;height:0;border-width:10px 10px 0 0;border-style:solid;border-color:#efefef transparent}ul.order_notes li.system-note .note_content{background:#d7cad2}ul.order_notes li.system-note .note_content::after{border-color:#d7cad2 transparent}ul.order_notes li.customer-note .note_content{background:#a7cedc}ul.order_notes li.customer-note .note_content::after{border-color:#a7cedc transparent}.add_note{border-top:1px solid #ddd;padding:10px 10px 0}.add_note h4{margin-top:5px!important}.add_note #add_order_note{width:100%;height:50px}table.wp-list-table .column-thumb{width:52px;text-align:center;white-space:nowrap}table.wp-list-table .column-handle{width:17px;display:none}table.wp-list-table tbody td.column-handle{cursor:move;width:17px;text-align:center;vertical-align:text-top}table.wp-list-table tbody td.column-handle::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;height:100%;margin:4px 0 0 0}table.wp-list-table .column-name{width:22%}table.wp-list-table .column-product_cat,table.wp-list-table .column-product_tag{width:11%!important}table.wp-list-table .column-featured,table.wp-list-table .column-product_type{width:48px;text-align:left!important}table.wp-list-table .column-customer_message,table.wp-list-table .column-order_notes{width:48px;text-align:center}table.wp-list-table .column-customer_message img,table.wp-list-table .column-order_notes img{margin:0 auto;padding-top:0!important}table.wp-list-table .manage-column.column-featured img,table.wp-list-table .manage-column.column-product_type img{padding-left:2px}table.wp-list-table .column-price .woocommerce-price-suffix{display:none}table.wp-list-table img{margin:1px 2px}table.wp-list-table .row-actions{color:#999}table.wp-list-table .row-actions span.id{padding-top:8px}table.wp-list-table td.column-thumb img{margin:0;width:auto;height:auto;max-width:40px;max-height:40px;vertical-align:middle}table.wp-list-table span.na{color:#999}table.wp-list-table .column-sku{width:10%}table.wp-list-table .column-price{width:10ch}table.wp-list-table .column-is_in_stock{text-align:left!important;width:12ch}table.wp-list-table span.wc-featured,table.wp-list-table span.wc-image{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;margin:0 auto}table.wp-list-table span.wc-featured::before,table.wp-list-table span.wc-image::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}table.wp-list-table span.wc-featured::before{content:"\f155"}table.wp-list-table span.wc-featured.not-featured::before{content:"\f154"}table.wp-list-table td.column-featured span.wc-featured{font-size:1.6em;cursor:pointer}table.wp-list-table mark.instock,table.wp-list-table mark.onbackorder,table.wp-list-table mark.outofstock{font-weight:700;background:transparent none;line-height:1}table.wp-list-table mark.instock{color:#7ad03a}table.wp-list-table mark.outofstock{color:#a44}table.wp-list-table mark.onbackorder{color:#eaa600}table.wp-list-table .notes_head,table.wp-list-table .order-notes_head,table.wp-list-table .status_head{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;margin:0 auto}table.wp-list-table .notes_head::after,table.wp-list-table .order-notes_head::after,table.wp-list-table .status_head::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}table.wp-list-table .order-notes_head::after{content:"\e028"}table.wp-list-table .notes_head::after{content:"\e026"}table.wp-list-table .status_head::after{content:"\e011"}table.wp-list-table .column-order_items{width:12%}table.wp-list-table .column-order_items table.order_items{width:100%;margin:3px 0 0;padding:0;display:none}table.wp-list-table .column-order_items table.order_items td{border:0;margin:0;padding:0 0 3px}table.wp-list-table .column-order_items table.order_items td.qty{color:#999;padding-right:6px;text-align:left}mark.notice{background:#fff;color:#a00;margin:0 0 0 10px}a.export_rates,a.import_rates{float:right;margin-left:9px;margin-top:-2px;margin-bottom:0}#rates-search{float:right}#rates-search input.wc-tax-rates-search-field{padding:4px 8px;font-size:1.2em}#rates-pagination{float:right;margin-right:.5em}#rates-pagination .tablenav{margin:0}.wc_input_table_wrapper{overflow-x:auto;display:block}table.wc_input_table,table.wc_tax_rates{width:100%}table.wc_input_table td,table.wc_input_table th,table.wc_tax_rates td,table.wc_tax_rates th{display:table-cell!important}table.wc_input_table span.tips,table.wc_tax_rates span.tips{color:#2ea2cc}table.wc_input_table th,table.wc_tax_rates th{white-space:nowrap;padding:10px}table.wc_input_table td,table.wc_tax_rates td{padding:0;border-right:1px solid #dfdfdf;border-bottom:1px solid #dfdfdf;border-top:0;background:#fff;cursor:default}table.wc_input_table td input[type=number],table.wc_input_table td input[type=text],table.wc_tax_rates td input[type=number],table.wc_tax_rates td input[type=text]{width:100%!important;min-width:100px;padding:8px 10px;margin:0;border:0;outline:0;background:transparent none}table.wc_input_table td input[type=number]:focus,table.wc_input_table td input[type=text]:focus,table.wc_tax_rates td input[type=number]:focus,table.wc_tax_rates td input[type=text]:focus{outline:0;box-shadow:none}table.wc_input_table td.apply_to_shipping,table.wc_input_table td.compound,table.wc_tax_rates td.apply_to_shipping,table.wc_tax_rates td.compound{padding:5px 7px;vertical-align:middle}table.wc_input_table td.apply_to_shipping input,table.wc_input_table td.compound input,table.wc_tax_rates td.apply_to_shipping input,table.wc_tax_rates td.compound input{padding:0}table.wc_input_table td:last-child,table.wc_tax_rates td:last-child{border-right:0}table.wc_input_table tr.current td,table.wc_tax_rates tr.current td{background-color:#fefbcc}table.wc_input_table .cost,table.wc_input_table .item_cost,table.wc_tax_rates .cost,table.wc_tax_rates .item_cost{text-align:right}table.wc_input_table .cost input,table.wc_input_table .item_cost input,table.wc_tax_rates .cost input,table.wc_tax_rates .item_cost input{text-align:right}table.wc_input_table th.sort,table.wc_tax_rates th.sort{width:17px;padding:0 4px}table.wc_input_table td.sort,table.wc_tax_rates td.sort{padding:0 4px}table.wc_input_table .ui-sortable:not(.ui-sortable-disabled) td.sort,table.wc_tax_rates .ui-sortable:not(.ui-sortable-disabled) td.sort{cursor:move;font-size:15px;background:#f9f9f9;text-align:center;vertical-align:middle}table.wc_input_table .ui-sortable:not(.ui-sortable-disabled) td.sort::before,table.wc_tax_rates .ui-sortable:not(.ui-sortable-disabled) td.sort::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;float:left;height:100%}table.wc_input_table .ui-sortable:not(.ui-sortable-disabled) td.sort:hover::before,table.wc_tax_rates .ui-sortable:not(.ui-sortable-disabled) td.sort:hover::before{color:#333}table.wc_input_table .button,table.wc_tax_rates .button{float:left;margin-right:5px}table.wc_input_table .export,table.wc_input_table .import,table.wc_tax_rates .export,table.wc_tax_rates .import{float:right;margin-right:0;margin-left:5px}table.wc_input_table span.tips,table.wc_tax_rates span.tips{padding:0 3px}table.wc_input_table .pagination,table.wc_tax_rates .pagination{float:right}table.wc_input_table .pagination .button,table.wc_tax_rates .pagination .button{margin-left:5px;margin-right:0}table.wc_input_table .pagination .current,table.wc_tax_rates .pagination .current{background:#bbb;text-shadow:none}table.wc_input_table tr:last-child td,table.wc_tax_rates tr:last-child td{border-bottom:0}table.wc_tax_rates td.country{position:relative}table.wc_emails,table.wc_gateways,table.wc_shipping{position:relative}table.wc_emails td,table.wc_emails th,table.wc_gateways td,table.wc_gateways th,table.wc_shipping td,table.wc_shipping th{display:table-cell!important;padding:1em!important;vertical-align:top;line-height:1.75em}table.wc_emails.wc_emails td,table.wc_gateways.wc_emails td,table.wc_shipping.wc_emails td{vertical-align:middle}table.wc_emails tr:nth-child(odd) td,table.wc_gateways tr:nth-child(odd) td,table.wc_shipping tr:nth-child(odd) td{background:#f9f9f9}table.wc_emails td.name,table.wc_gateways td.name,table.wc_shipping td.name{font-weight:700}table.wc_emails .settings,table.wc_gateways .settings,table.wc_shipping .settings{text-align:right}table.wc_emails .default,table.wc_emails .radio,table.wc_emails .status,table.wc_gateways .default,table.wc_gateways .radio,table.wc_gateways .status,table.wc_shipping .default,table.wc_shipping .radio,table.wc_shipping .status{text-align:center}table.wc_emails .default .tips,table.wc_emails .radio .tips,table.wc_emails .status .tips,table.wc_gateways .default .tips,table.wc_gateways .radio .tips,table.wc_gateways .status .tips,table.wc_shipping .default .tips,table.wc_shipping .radio .tips,table.wc_shipping .status .tips{margin:0 auto}table.wc_emails .default input,table.wc_emails .radio input,table.wc_emails .status input,table.wc_gateways .default input,table.wc_gateways .radio input,table.wc_gateways .status input,table.wc_shipping .default input,table.wc_shipping .radio input,table.wc_shipping .status input{margin:0}table.wc_emails td.sort,table.wc_gateways td.sort,table.wc_shipping td.sort{font-size:15px;text-align:center}table.wc_emails td.sort .wc-item-reorder-nav,table.wc_gateways td.sort .wc-item-reorder-nav,table.wc_shipping td.sort .wc-item-reorder-nav{white-space:nowrap;width:72px}table.wc_emails td.sort .wc-item-reorder-nav::before,table.wc_gateways td.sort .wc-item-reorder-nav::before,table.wc_shipping td.sort .wc-item-reorder-nav::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:24px;float:left;height:100%;line-height:24px;cursor:move}table.wc_emails td.sort .wc-item-reorder-nav button,table.wc_gateways td.sort .wc-item-reorder-nav button,table.wc_shipping td.sort .wc-item-reorder-nav button{position:relative;overflow:hidden;float:left;display:block;width:24px;height:24px;margin:0;background:0 0;border:none;box-shadow:none;color:#82878c;text-indent:-9999px;cursor:pointer;outline:0}table.wc_emails td.sort .wc-item-reorder-nav button::before,table.wc_gateways td.sort .wc-item-reorder-nav button::before,table.wc_shipping td.sort .wc-item-reorder-nav button::before{display:inline-block;position:absolute;top:0;right:0;width:100%;height:100%;font:normal 20px/23px dashicons;text-align:center;text-indent:0;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}table.wc_emails td.sort .wc-item-reorder-nav button:focus,table.wc_emails td.sort .wc-item-reorder-nav button:hover,table.wc_gateways td.sort .wc-item-reorder-nav button:focus,table.wc_gateways td.sort .wc-item-reorder-nav button:hover,table.wc_shipping td.sort .wc-item-reorder-nav button:focus,table.wc_shipping td.sort .wc-item-reorder-nav button:hover{color:#191e23}table.wc_emails td.sort .wc-item-reorder-nav .wc-move-down::before,table.wc_gateways td.sort .wc-item-reorder-nav .wc-move-down::before,table.wc_shipping td.sort .wc-item-reorder-nav .wc-move-down::before{content:"\f347"}table.wc_emails td.sort .wc-item-reorder-nav .wc-move-up::before,table.wc_gateways td.sort .wc-item-reorder-nav .wc-move-up::before,table.wc_shipping td.sort .wc-item-reorder-nav .wc-move-up::before{content:"\f343"}table.wc_emails td.sort .wc-item-reorder-nav .wc-move-disabled,table.wc_gateways td.sort .wc-item-reorder-nav .wc-move-disabled,table.wc_shipping td.sort .wc-item-reorder-nav .wc-move-disabled{color:#d5d5d5!important;cursor:default;pointer-events:none}table.wc_emails .wc-payment-gateway-method-name,table.wc_gateways .wc-payment-gateway-method-name,table.wc_shipping .wc-payment-gateway-method-name{font-weight:400}table.wc_emails .wc-email-settings-table-name,table.wc_gateways .wc-email-settings-table-name,table.wc_shipping .wc-email-settings-table-name{font-weight:700}table.wc_emails .wc-email-settings-table-name span,table.wc_gateways .wc-email-settings-table-name span,table.wc_shipping .wc-email-settings-table-name span{font-weight:400;color:#999;margin:0 0 0 4px!important}table.wc_emails .wc-payment-gateway-method-toggle-disabled,table.wc_emails .wc-payment-gateway-method-toggle-enabled,table.wc_gateways .wc-payment-gateway-method-toggle-disabled,table.wc_gateways .wc-payment-gateway-method-toggle-enabled,table.wc_shipping .wc-payment-gateway-method-toggle-disabled,table.wc_shipping .wc-payment-gateway-method-toggle-enabled{padding-top:1px;display:block;outline:0;box-shadow:none}table.wc_emails .wc-email-settings-table-status,table.wc_gateways .wc-email-settings-table-status,table.wc_shipping .wc-email-settings-table-status{text-align:center;width:1em}table.wc_emails .wc-email-settings-table-status .tips,table.wc_gateways .wc-email-settings-table-status .tips,table.wc_shipping .wc-email-settings-table-status .tips{margin:0 auto}.wc-shipping-zone-settings th{padding:24px 24px 24px 0}.wc-shipping-zone-settings td.forminp input,.wc-shipping-zone-settings td.forminp textarea{padding:8px;max-width:100%!important}.wc-shipping-zone-settings td.forminp .wc-shipping-zone-region-select{width:448px;max-width:100%!important}.wc-shipping-zone-settings td.forminp .wc-shipping-zone-region-select .select2-choices{padding:8px 8px 4px;border-color:#ddd;min-height:0;line-height:1}.wc-shipping-zone-settings td.forminp .wc-shipping-zone-region-select .select2-choices input{padding:0}.wc-shipping-zone-settings td.forminp .wc-shipping-zone-region-select .select2-choices li{margin:0 4px 4px 0}.wc-shipping-zone-settings .wc-shipping-zone-postcodes-toggle{margin:.5em 0 0;font-size:.9em;text-decoration:underline;display:block}.wc-shipping-zone-settings .wc-shipping-zone-postcodes-toggle+.wc-shipping-zone-postcodes{display:none}.wc-shipping-zone-settings .wc-shipping-zone-postcodes textarea{margin:10px 0}.wc-shipping-zone-settings .wc-shipping-zone-postcodes .description{font-size:.9em;color:#999}.wc-shipping-zone-settings+p.submit{margin-top:0}.wc-shipping-zone-settings tbody{display:table-row-group}table tr table.wc-shipping-zone-methods tr .row-actions,table tr:hover table.wc-shipping-zone-methods tr .row-actions{position:relative}table tr table.wc-shipping-zone-methods tr:hover .row-actions,table tr:hover table.wc-shipping-zone-methods tr:hover .row-actions{position:static}.wc-shipping-zones-heading .page-title-action{display:inline-block}table.wc-shipping-classes td,table.wc-shipping-classes th,table.wc-shipping-zone-methods td,table.wc-shipping-zone-methods th,table.wc-shipping-zones td,table.wc-shipping-zones th{vertical-align:top;line-height:24px;padding:1em!important;font-size:14px;background:#fff;display:table-cell!important}table.wc-shipping-classes td li,table.wc-shipping-classes th li,table.wc-shipping-zone-methods td li,table.wc-shipping-zone-methods th li,table.wc-shipping-zones td li,table.wc-shipping-zones th li{line-height:24px;font-size:14px}table.wc-shipping-classes td .woocommerce-help-tip,table.wc-shipping-classes th .woocommerce-help-tip,table.wc-shipping-zone-methods td .woocommerce-help-tip,table.wc-shipping-zone-methods th .woocommerce-help-tip,table.wc-shipping-zones td .woocommerce-help-tip,table.wc-shipping-zones th .woocommerce-help-tip{margin:0!important}table.wc-shipping-classes thead th,table.wc-shipping-zone-methods thead th,table.wc-shipping-zones thead th{vertical-align:middle}table.wc-shipping-classes thead .wc-shipping-zone-sort,table.wc-shipping-zone-methods thead .wc-shipping-zone-sort,table.wc-shipping-zones thead .wc-shipping-zone-sort{text-align:center}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state,table.wc-shipping-classes td.wc-shipping-zones-blank-state,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state,table.wc-shipping-zones td.wc-shipping-zones-blank-state{background:#f7f1f6!important;overflow:hidden;position:relative;padding:7.5em 7.5%!important;border-bottom:2px solid #eee2ec}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-classes td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state,table.wc-shipping-zones td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state{padding:2em!important}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-classes td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state.wc-shipping-zone-method-blank-state p,table.wc-shipping-zones td.wc-shipping-zones-blank-state.wc-shipping-zone-method-blank-state p{margin-bottom:0}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state li,table.wc-shipping-classes td.wc-shipping-zone-method-blank-state p,table.wc-shipping-classes td.wc-shipping-zones-blank-state li,table.wc-shipping-classes td.wc-shipping-zones-blank-state p,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state li,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state p,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state li,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state p,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state li,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state p,table.wc-shipping-zones td.wc-shipping-zones-blank-state li,table.wc-shipping-zones td.wc-shipping-zones-blank-state p{color:#a46497;font-size:1.5em;line-height:1.5em;margin:0 0 1em;position:relative;z-index:1;text-shadow:1px 1px 1px #fff}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state li.main,table.wc-shipping-classes td.wc-shipping-zone-method-blank-state p.main,table.wc-shipping-classes td.wc-shipping-zones-blank-state li.main,table.wc-shipping-classes td.wc-shipping-zones-blank-state p.main,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state li.main,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state p.main,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state li.main,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state p.main,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state li.main,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state p.main,table.wc-shipping-zones td.wc-shipping-zones-blank-state li.main,table.wc-shipping-zones td.wc-shipping-zones-blank-state p.main{font-size:2em}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state li,table.wc-shipping-classes td.wc-shipping-zones-blank-state li,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state li,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state li,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state li,table.wc-shipping-zones td.wc-shipping-zones-blank-state li{margin-left:1em;list-style:circle inside}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state::before,table.wc-shipping-classes td.wc-shipping-zones-blank-state::before,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state::before,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state::before,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state::before,table.wc-shipping-zones td.wc-shipping-zones-blank-state::before{content:"\e01b";font-family:WooCommerce;text-align:center;line-height:1;color:#eee2ec;display:block;width:1em;font-size:40em;top:50%;right:-3.75%;margin-top:-.1875em;position:absolute}table.wc-shipping-classes td.wc-shipping-zone-method-blank-state .button-primary,table.wc-shipping-classes td.wc-shipping-zones-blank-state .button-primary,table.wc-shipping-zone-methods td.wc-shipping-zone-method-blank-state .button-primary,table.wc-shipping-zone-methods td.wc-shipping-zones-blank-state .button-primary,table.wc-shipping-zones td.wc-shipping-zone-method-blank-state .button-primary,table.wc-shipping-zones td.wc-shipping-zones-blank-state .button-primary{background-color:#804877;border-color:#804877;box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);margin:0;opacity:1;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f;font-size:1.5em;padding:.75em 1em;height:auto;position:relative;z-index:1}table.wc-shipping-classes .wc-shipping-zone-method-rows tr:nth-child(even) td,table.wc-shipping-zone-methods .wc-shipping-zone-method-rows tr:nth-child(even) td,table.wc-shipping-zones .wc-shipping-zone-method-rows tr:nth-child(even) td{background:#f9f9f9}table.wc-shipping-classes .wc-shipping-class-rows tr:nth-child(odd) td,table.wc-shipping-classes tr.odd td,table.wc-shipping-zone-methods .wc-shipping-class-rows tr:nth-child(odd) td,table.wc-shipping-zone-methods tr.odd td,table.wc-shipping-zones .wc-shipping-class-rows tr:nth-child(odd) td,table.wc-shipping-zones tr.odd td{background:#f9f9f9}table.wc-shipping-classes tbody.wc-shipping-zone-rows td,table.wc-shipping-zone-methods tbody.wc-shipping-zone-rows td,table.wc-shipping-zones tbody.wc-shipping-zone-rows td{border-top:2px solid #f9f9f9}table.wc-shipping-classes tbody.wc-shipping-zone-rows tr:first-child td,table.wc-shipping-zone-methods tbody.wc-shipping-zone-rows tr:first-child td,table.wc-shipping-zones tbody.wc-shipping-zone-rows tr:first-child td{border-top:0}table.wc-shipping-classes tr.wc-shipping-zone-worldwide td,table.wc-shipping-zone-methods tr.wc-shipping-zone-worldwide td,table.wc-shipping-zones tr.wc-shipping-zone-worldwide td{background:#f9f9f9;border-top:2px solid #e1e1e1}table.wc-shipping-classes p,table.wc-shipping-classes ul,table.wc-shipping-zone-methods p,table.wc-shipping-zone-methods ul,table.wc-shipping-zones p,table.wc-shipping-zones ul{margin:0}table.wc-shipping-classes td.wc-shipping-zone-method-sort,table.wc-shipping-classes td.wc-shipping-zone-sort,table.wc-shipping-zone-methods td.wc-shipping-zone-method-sort,table.wc-shipping-zone-methods td.wc-shipping-zone-sort,table.wc-shipping-zones td.wc-shipping-zone-method-sort,table.wc-shipping-zones td.wc-shipping-zone-sort{cursor:move;font-size:15px;text-align:center}table.wc-shipping-classes td.wc-shipping-zone-method-sort::before,table.wc-shipping-classes td.wc-shipping-zone-sort::before,table.wc-shipping-zone-methods td.wc-shipping-zone-method-sort::before,table.wc-shipping-zone-methods td.wc-shipping-zone-sort::before,table.wc-shipping-zones td.wc-shipping-zone-method-sort::before,table.wc-shipping-zones td.wc-shipping-zone-sort::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;float:left;height:100%;line-height:24px}table.wc-shipping-classes td.wc-shipping-zone-method-sort:hover::before,table.wc-shipping-classes td.wc-shipping-zone-sort:hover::before,table.wc-shipping-zone-methods td.wc-shipping-zone-method-sort:hover::before,table.wc-shipping-zone-methods td.wc-shipping-zone-sort:hover::before,table.wc-shipping-zones td.wc-shipping-zone-method-sort:hover::before,table.wc-shipping-zones td.wc-shipping-zone-sort:hover::before{color:#333}table.wc-shipping-classes td.wc-shipping-zone-worldwide,table.wc-shipping-zone-methods td.wc-shipping-zone-worldwide,table.wc-shipping-zones td.wc-shipping-zone-worldwide{text-align:center}table.wc-shipping-classes td.wc-shipping-zone-worldwide::before,table.wc-shipping-zone-methods td.wc-shipping-zone-worldwide::before,table.wc-shipping-zones td.wc-shipping-zone-worldwide::before{content:"\f319";font-family:dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;float:left;height:100%;line-height:24px}table.wc-shipping-classes .wc-shipping-zone-methods,table.wc-shipping-classes .wc-shipping-zone-name,table.wc-shipping-zone-methods .wc-shipping-zone-methods,table.wc-shipping-zone-methods .wc-shipping-zone-name,table.wc-shipping-zones .wc-shipping-zone-methods,table.wc-shipping-zones .wc-shipping-zone-name{width:25%}table.wc-shipping-classes .wc-shipping-class-description input,table.wc-shipping-classes .wc-shipping-class-description select,table.wc-shipping-classes .wc-shipping-class-description textarea,table.wc-shipping-classes .wc-shipping-class-name input,table.wc-shipping-classes .wc-shipping-class-name select,table.wc-shipping-classes .wc-shipping-class-name textarea,table.wc-shipping-classes .wc-shipping-class-slug input,table.wc-shipping-classes .wc-shipping-class-slug select,table.wc-shipping-classes .wc-shipping-class-slug textarea,table.wc-shipping-classes .wc-shipping-zone-name input,table.wc-shipping-classes .wc-shipping-zone-name select,table.wc-shipping-classes .wc-shipping-zone-name textarea,table.wc-shipping-classes .wc-shipping-zone-region input,table.wc-shipping-classes .wc-shipping-zone-region select,table.wc-shipping-classes .wc-shipping-zone-region textarea,table.wc-shipping-zone-methods .wc-shipping-class-description input,table.wc-shipping-zone-methods .wc-shipping-class-description select,table.wc-shipping-zone-methods .wc-shipping-class-description textarea,table.wc-shipping-zone-methods .wc-shipping-class-name input,table.wc-shipping-zone-methods .wc-shipping-class-name select,table.wc-shipping-zone-methods .wc-shipping-class-name textarea,table.wc-shipping-zone-methods .wc-shipping-class-slug input,table.wc-shipping-zone-methods .wc-shipping-class-slug select,table.wc-shipping-zone-methods .wc-shipping-class-slug textarea,table.wc-shipping-zone-methods .wc-shipping-zone-name input,table.wc-shipping-zone-methods .wc-shipping-zone-name select,table.wc-shipping-zone-methods .wc-shipping-zone-name textarea,table.wc-shipping-zone-methods .wc-shipping-zone-region input,table.wc-shipping-zone-methods .wc-shipping-zone-region select,table.wc-shipping-zone-methods .wc-shipping-zone-region textarea,table.wc-shipping-zones .wc-shipping-class-description input,table.wc-shipping-zones .wc-shipping-class-description select,table.wc-shipping-zones .wc-shipping-class-description textarea,table.wc-shipping-zones .wc-shipping-class-name input,table.wc-shipping-zones .wc-shipping-class-name select,table.wc-shipping-zones .wc-shipping-class-name textarea,table.wc-shipping-zones .wc-shipping-class-slug input,table.wc-shipping-zones .wc-shipping-class-slug select,table.wc-shipping-zones .wc-shipping-class-slug textarea,table.wc-shipping-zones .wc-shipping-zone-name input,table.wc-shipping-zones .wc-shipping-zone-name select,table.wc-shipping-zones .wc-shipping-zone-name textarea,table.wc-shipping-zones .wc-shipping-zone-region input,table.wc-shipping-zones .wc-shipping-zone-region select,table.wc-shipping-zones .wc-shipping-zone-region textarea{width:100%}table.wc-shipping-classes .wc-shipping-class-description a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-class-description a.wc-shipping-zone-delete,table.wc-shipping-classes .wc-shipping-class-name a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-class-name a.wc-shipping-zone-delete,table.wc-shipping-classes .wc-shipping-class-slug a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-class-slug a.wc-shipping-zone-delete,table.wc-shipping-classes .wc-shipping-zone-name a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-zone-name a.wc-shipping-zone-delete,table.wc-shipping-classes .wc-shipping-zone-region a.wc-shipping-class-delete,table.wc-shipping-classes .wc-shipping-zone-region a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-class-description a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-class-description a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-class-name a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-class-name a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-class-slug a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-class-slug a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-zone-name a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-zone-name a.wc-shipping-zone-delete,table.wc-shipping-zone-methods .wc-shipping-zone-region a.wc-shipping-class-delete,table.wc-shipping-zone-methods .wc-shipping-zone-region a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-class-description a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-class-description a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-class-name a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-class-name a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-class-slug a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-class-slug a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-zone-name a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-zone-name a.wc-shipping-zone-delete,table.wc-shipping-zones .wc-shipping-zone-region a.wc-shipping-class-delete,table.wc-shipping-zones .wc-shipping-zone-region a.wc-shipping-zone-delete{color:#a00}table.wc-shipping-classes .wc-shipping-class-description a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-class-description a.wc-shipping-zone-delete:hover,table.wc-shipping-classes .wc-shipping-class-name a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-class-name a.wc-shipping-zone-delete:hover,table.wc-shipping-classes .wc-shipping-class-slug a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-class-slug a.wc-shipping-zone-delete:hover,table.wc-shipping-classes .wc-shipping-zone-name a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-zone-name a.wc-shipping-zone-delete:hover,table.wc-shipping-classes .wc-shipping-zone-region a.wc-shipping-class-delete:hover,table.wc-shipping-classes .wc-shipping-zone-region a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-description a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-description a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-name a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-name a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-slug a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-class-slug a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-zone-name a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-zone-name a.wc-shipping-zone-delete:hover,table.wc-shipping-zone-methods .wc-shipping-zone-region a.wc-shipping-class-delete:hover,table.wc-shipping-zone-methods .wc-shipping-zone-region a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-class-description a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-class-description a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-class-name a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-class-name a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-class-slug a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-class-slug a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-zone-name a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-zone-name a.wc-shipping-zone-delete:hover,table.wc-shipping-zones .wc-shipping-zone-region a.wc-shipping-class-delete:hover,table.wc-shipping-zones .wc-shipping-zone-region a.wc-shipping-zone-delete:hover{color:red}table.wc-shipping-classes .wc-shipping-class-count,table.wc-shipping-zone-methods .wc-shipping-class-count,table.wc-shipping-zones .wc-shipping-class-count{text-align:center}table.wc-shipping-classes td.wc-shipping-zone-methods,table.wc-shipping-zone-methods td.wc-shipping-zone-methods,table.wc-shipping-zones td.wc-shipping-zone-methods{color:#555}table.wc-shipping-classes td.wc-shipping-zone-methods .method_disabled,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .method_disabled,table.wc-shipping-zones td.wc-shipping-zone-methods .method_disabled{text-decoration:line-through}table.wc-shipping-classes td.wc-shipping-zone-methods ul,table.wc-shipping-zone-methods td.wc-shipping-zone-methods ul,table.wc-shipping-zones td.wc-shipping-zone-methods ul{position:relative;padding-right:32px}table.wc-shipping-classes td.wc-shipping-zone-methods ul li,table.wc-shipping-zone-methods td.wc-shipping-zone-methods ul li,table.wc-shipping-zones td.wc-shipping-zone-methods ul li{color:#555;display:inline;margin:0}table.wc-shipping-classes td.wc-shipping-zone-methods ul li::before,table.wc-shipping-zone-methods td.wc-shipping-zone-methods ul li::before,table.wc-shipping-zones td.wc-shipping-zone-methods ul li::before{content:", "}table.wc-shipping-classes td.wc-shipping-zone-methods ul li:first-child::before,table.wc-shipping-zone-methods td.wc-shipping-zone-methods ul li:first-child::before,table.wc-shipping-zones td.wc-shipping-zone-methods ul li:first-child::before{content:""}table.wc-shipping-classes td.wc-shipping-zone-methods .add_shipping_method,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .add_shipping_method,table.wc-shipping-zones td.wc-shipping-zone-methods .add_shipping_method{display:block;width:24px;padding:24px 0 0;height:0;overflow:hidden;cursor:pointer}table.wc-shipping-classes td.wc-shipping-zone-methods .add_shipping_method::before,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .add_shipping_method::before,table.wc-shipping-zones td.wc-shipping-zone-methods .add_shipping_method::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";font-family:Dashicons;content:"\f502";color:#999;vertical-align:middle;line-height:24px;font-size:16px;margin:0}table.wc-shipping-classes td.wc-shipping-zone-methods .add_shipping_method.disabled,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .add_shipping_method.disabled,table.wc-shipping-zones td.wc-shipping-zone-methods .add_shipping_method.disabled{cursor:not-allowed}table.wc-shipping-classes td.wc-shipping-zone-methods .add_shipping_method.disabled::before,table.wc-shipping-zone-methods td.wc-shipping-zone-methods .add_shipping_method.disabled::before,table.wc-shipping-zones td.wc-shipping-zone-methods .add_shipping_method.disabled::before{color:#ccc}table.wc-shipping-classes .wc-shipping-zone-method-title,table.wc-shipping-zone-methods .wc-shipping-zone-method-title,table.wc-shipping-zones .wc-shipping-zone-method-title{width:25%}table.wc-shipping-classes .wc-shipping-zone-method-title .wc-shipping-zone-method-delete,table.wc-shipping-zone-methods .wc-shipping-zone-method-title .wc-shipping-zone-method-delete,table.wc-shipping-zones .wc-shipping-zone-method-title .wc-shipping-zone-method-delete{color:red}table.wc-shipping-classes .wc-shipping-zone-method-enabled,table.wc-shipping-zone-methods .wc-shipping-zone-method-enabled,table.wc-shipping-zones .wc-shipping-zone-method-enabled{text-align:center}table.wc-shipping-classes .wc-shipping-zone-method-enabled a,table.wc-shipping-zone-methods .wc-shipping-zone-method-enabled a,table.wc-shipping-zones .wc-shipping-zone-method-enabled a{display:inline-block}table.wc-shipping-classes .wc-shipping-zone-method-enabled .woocommerce-input-toggle,table.wc-shipping-zone-methods .wc-shipping-zone-method-enabled .woocommerce-input-toggle,table.wc-shipping-zones .wc-shipping-zone-method-enabled .woocommerce-input-toggle{margin-top:3px}table.wc-shipping-classes .wc-shipping-zone-method-type,table.wc-shipping-zone-methods .wc-shipping-zone-method-type,table.wc-shipping-zones .wc-shipping-zone-method-type{display:block}table.wc-shipping-classes tfoot input,table.wc-shipping-classes tfoot select,table.wc-shipping-zone-methods tfoot input,table.wc-shipping-zone-methods tfoot select,table.wc-shipping-zones tfoot input,table.wc-shipping-zones tfoot select{vertical-align:middle!important}table.wc-shipping-classes tfoot .button-secondary,table.wc-shipping-zone-methods tfoot .button-secondary,table.wc-shipping-zones tfoot .button-secondary{float:right}table.wc-shipping-classes .editing .wc-shipping-zone-edit,table.wc-shipping-classes .editing .wc-shipping-zone-view,table.wc-shipping-zone-methods .editing .wc-shipping-zone-edit,table.wc-shipping-zone-methods .editing .wc-shipping-zone-view,table.wc-shipping-zones .editing .wc-shipping-zone-edit,table.wc-shipping-zones .editing .wc-shipping-zone-view{display:none}.woocommerce-input-toggle{height:16px;width:32px;border:2px solid #935687;background-color:#935687;display:inline-block;text-indent:-9999px;border-radius:10em;position:relative;margin-top:-1px;vertical-align:text-top}.woocommerce-input-toggle::before{content:"";display:block;width:16px;height:16px;background:#fff;position:absolute;top:0;right:0;border-radius:100%}.woocommerce-input-toggle.woocommerce-input-toggle--disabled{border-color:#999;background-color:#999}.woocommerce-input-toggle.woocommerce-input-toggle--disabled::before{right:auto;left:0}.woocommerce-input-toggle.woocommerce-input-toggle--loading{opacity:.5}.wc-modal-shipping-method-settings{background:#f8f8f8;padding:1em!important}.wc-modal-shipping-method-settings form .form-table{width:100%;background:#fff;margin:0 0 1.5em}.wc-modal-shipping-method-settings form .form-table tr th{width:30%;position:relative}.wc-modal-shipping-method-settings form .form-table tr th .woocommerce-help-tip{float:right;margin:-8px -.5em 0 0;vertical-align:middle;right:0;top:50%;position:absolute}.wc-modal-shipping-method-settings form .form-table tr td input,.wc-modal-shipping-method-settings form .form-table tr td select,.wc-modal-shipping-method-settings form .form-table tr td textarea{width:50%;min-width:250px}.wc-modal-shipping-method-settings form .form-table tr td input[type=checkbox]{width:auto;min-width:16px}.wc-modal-shipping-method-settings form .form-table tr td,.wc-modal-shipping-method-settings form .form-table tr th{vertical-align:middle;margin:0;line-height:24px;padding:1em;border-bottom:1px solid #f8f8f8}.wc-modal-shipping-method-settings form .form-table:last-of-type{margin-bottom:0}.wc-backbone-modal .wc-shipping-zone-method-selector p{margin-top:0}.wc-backbone-modal .wc-shipping-zone-method-selector .wc-shipping-zone-method-description{margin:.75em 1px 0;line-height:1.5em;color:#999;font-style:italic}.wc-backbone-modal .wc-shipping-zone-method-selector select{width:100%;cursor:pointer}img.help_tip{margin:0 0 0 9px;vertical-align:middle}.postbox img.help_tip{margin-top:0}.postbox .woocommerce-help-tip{margin:0 0 0 9px}.status-disabled,.status-enabled,.status-manual{font-size:1.4em;display:block;text-indent:-9999px;position:relative;height:1em;width:1em}.status-manual::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#999}.status-enabled::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#a46497}.status-disabled::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#ccc}.woocommerce h2.woo-nav-tab-wrapper{margin-bottom:1em}.woocommerce nav.woo-nav-tab-wrapper{margin:1.5em 0 1em}.woocommerce .subsubsub{margin:-8px 0 0}.woocommerce .wc-admin-breadcrumb{margin-left:.5em}.woocommerce .wc-admin-breadcrumb a{color:#a46497}.woocommerce #template div{margin:0}.woocommerce #template div p .button{float:right;margin-left:10px;margin-top:-4px}.woocommerce #template div .editor textarea{margin-bottom:8px}.woocommerce textarea[disabled=disabled]{background:#dfdfdf!important}.woocommerce table.form-table{margin:0;position:relative;table-layout:fixed}.woocommerce table.form-table .forminp-radio ul{margin:0}.woocommerce table.form-table .forminp-radio ul li{line-height:1.4em}.woocommerce table.form-table input[type=email],.woocommerce table.form-table input[type=number],.woocommerce table.form-table input[type=text]{height:auto}.woocommerce table.form-table textarea.input-text{height:100%;min-width:150px;display:block}.woocommerce table.form-table input.regular-input,.woocommerce table.form-table input[type=date],.woocommerce table.form-table input[type=datetime-local],.woocommerce table.form-table input[type=datetime],.woocommerce table.form-table input[type=email],.woocommerce table.form-table input[type=number],.woocommerce table.form-table input[type=password],.woocommerce table.form-table input[type=tel],.woocommerce table.form-table input[type=text],.woocommerce table.form-table input[type=time],.woocommerce table.form-table input[type=url],.woocommerce table.form-table input[type=week],.woocommerce table.form-table textarea{width:400px;margin:0;padding:6px;box-sizing:border-box;vertical-align:top}.woocommerce table.form-table input[type=date],.woocommerce table.form-table input[type=datetime-local],.woocommerce table.form-table input[type=tel],.woocommerce table.form-table input[type=time],.woocommerce table.form-table input[type=week]{width:200px}.woocommerce table.form-table select{width:400px;margin:0;box-sizing:border-box;line-height:32px;vertical-align:top}.woocommerce table.form-table input[size]{width:auto!important}.woocommerce table.form-table table input.regular-input,.woocommerce table.form-table table input[type=email],.woocommerce table.form-table table input[type=number],.woocommerce table.form-table table input[type=text],.woocommerce table.form-table table select,.woocommerce table.form-table table textarea{width:auto}.woocommerce table.form-table textarea.wide-input{width:100%}.woocommerce table.form-table .woocommerce-help-tip,.woocommerce table.form-table img.help_tip{padding:0;margin:-4px 0 0 5px;vertical-align:middle;cursor:help;line-height:1}.woocommerce table.form-table span.help_tip{cursor:help;color:#2ea2cc}.woocommerce table.form-table th{position:relative;padding-right:24px}.woocommerce table.form-table th label{position:relative;display:block}.woocommerce table.form-table th label .woocommerce-help-tip,.woocommerce table.form-table th label img.help_tip{margin:-8px -24px 0 0;position:absolute;right:0;top:50%}.woocommerce table.form-table th label+.woocommerce-help-tip{margin:0;position:absolute;right:0;top:20px}.woocommerce table.form-table .select2-container{vertical-align:top;margin-bottom:3px}.woocommerce table.form-table .select2-container+span.description{display:block;margin-top:8px}.woocommerce table.form-table table.widefat th{padding-right:inherit}.woocommerce table.form-table .wp-list-table .woocommerce-help-tip{float:none}.woocommerce table.form-table fieldset{margin-top:4px}.woocommerce table.form-table fieldset .woocommerce-help-tip,.woocommerce table.form-table fieldset img.help_tip{margin:-3px 0 0 5px}.woocommerce table.form-table fieldset p.description{margin-bottom:8px}.woocommerce table.form-table fieldset:first-child{margin-top:0}.woocommerce table.form-table .iris-picker{z-index:100;display:none;position:absolute;border:1px solid #ccc;border-radius:3px;box-shadow:0 1px 3px rgba(0,0,0,.2)}.woocommerce table.form-table .iris-picker .ui-slider{border:0!important;margin:0!important;width:auto!important;height:auto!important;background:none transparent!important}.woocommerce table.form-table .iris-picker .ui-slider .ui-slider-handle{margin-bottom:0!important}.woocommerce table.form-table .iris-error{background-color:#ffafaf}.woocommerce table.form-table .colorpickpreview{padding:7px 0;line-height:1em;display:inline-block;width:26px;border:1px solid #ddd;font-size:14px}.woocommerce table.form-table .image_width_settings{vertical-align:middle}.woocommerce table.form-table .image_width_settings label{margin-left:10px}.woocommerce table.form-table .image_width_settings input{width:auto}.woocommerce table.form-table .wc_emails_wrapper,.woocommerce table.form-table .wc_payment_gateways_wrapper{padding:0 15px 10px 0}.woocommerce .wc-shipping-zone-settings td.forminp input,.woocommerce .wc-shipping-zone-settings td.forminp textarea{width:448px;padding:6px 11px}.woocommerce .wc-shipping-zone-settings td.forminp .select2-search input{padding:6px}.wc-wp-version-gte-53 .woocommerce h2.wc-table-list-header{margin:1em 0 .35em 0}.wc-wp-version-gte-53 .woocommerce input+.subsubsub{margin:8px 0 0}.wc-wp-version-gte-53 .woocommerce table.form-table input.regular-input,.wc-wp-version-gte-53 .woocommerce table.form-table input[type=date],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=datetime-local],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=datetime],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=email],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=number],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=password],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=tel],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=text],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=time],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=url],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=week],.wc-wp-version-gte-53 .woocommerce table.form-table textarea{padding:0 8px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table input.regular-input,.wc-wp-version-gte-53 .woocommerce table.form-table input[type=date],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=datetime-local],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=datetime],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=email],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=number],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=password],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=tel],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=text],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=time],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=url],.wc-wp-version-gte-53 .woocommerce table.form-table input[type=week],.wc-wp-version-gte-53 .woocommerce table.form-table textarea{width:100%}}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table select{width:100%}}.wc-wp-version-gte-53 .woocommerce table.form-table th label .woocommerce-help-tip,.wc-wp-version-gte-53 .woocommerce table.form-table th label img.help_tip{margin:-7px -24px 0 0}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table th label .woocommerce-help-tip,.wc-wp-version-gte-53 .woocommerce table.form-table th label img.help_tip{right:auto;margin-left:5px}}.wc-wp-version-gte-53 .woocommerce table.form-table .forminp-color{font-size:0}.wc-wp-version-gte-53 .woocommerce table.form-table .colorpickpreview{padding:0;width:30px;height:30px;box-shadow:inset 0 0 0 1px rgba(0,0,0,.2);font-size:16px;border-radius:4px;margin-right:3px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table .colorpickpreview{float:left;width:40px;height:40px}}.woocommerce #tabs-wrap table a.remove{margin-left:4px}.woocommerce #tabs-wrap table p{margin:0 0 4px!important;overflow:hidden;zoom:1}.woocommerce #tabs-wrap table p a.add{float:left}#wp-excerpt-editor-container{background:#fff}#product_variation-parent #parent_id{width:100%}#postimagediv img{border:1px solid #d5d5d5;max-width:100%}#woocommerce-product-images .inside{margin:0;padding:0}#woocommerce-product-images .inside .add_product_images{padding:0 12px 12px}#woocommerce-product-images .inside #product_images_container{padding:0 0 0 9px}#woocommerce-product-images .inside #product_images_container ul{margin:0;padding:0}#woocommerce-product-images .inside #product_images_container ul::after,#woocommerce-product-images .inside #product_images_container ul::before{content:" ";display:table}#woocommerce-product-images .inside #product_images_container ul::after{clear:both}#woocommerce-product-images .inside #product_images_container ul li.add,#woocommerce-product-images .inside #product_images_container ul li.image,#woocommerce-product-images .inside #product_images_container ul li.wc-metabox-sortable-placeholder{width:80px;float:left;cursor:move;border:1px solid #d5d5d5;margin:9px 9px 0 0;background:#f7f7f7;border-radius:2px;position:relative;box-sizing:border-box}#woocommerce-product-images .inside #product_images_container ul li.add img,#woocommerce-product-images .inside #product_images_container ul li.image img,#woocommerce-product-images .inside #product_images_container ul li.wc-metabox-sortable-placeholder img{width:100%;height:auto;display:block}#woocommerce-product-images .inside #product_images_container ul li.wc-metabox-sortable-placeholder{border:3px dashed #ddd;position:relative}#woocommerce-product-images .inside #product_images_container ul li.wc-metabox-sortable-placeholder::after{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";font-size:2.618em;line-height:72px;color:#ddd}#woocommerce-product-images .inside #product_images_container ul ul.actions{position:absolute;top:-8px;right:-8px;padding:2px;display:none}@media (max-width:768px){#woocommerce-product-images .inside #product_images_container ul ul.actions{display:block}}#woocommerce-product-images .inside #product_images_container ul ul.actions li{float:right;margin:0 0 0 2px}#woocommerce-product-images .inside #product_images_container ul ul.actions li a{width:1em;height:1em;margin:0;height:0;display:block;overflow:hidden}#woocommerce-product-images .inside #product_images_container ul ul.actions li a.tips{cursor:pointer}#woocommerce-product-images .inside #product_images_container ul ul.actions li a.delete{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.4em}#woocommerce-product-images .inside #product_images_container ul ul.actions li a.delete::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#999;background:#fff;border-radius:50%;height:1em;width:1em;line-height:1em}#woocommerce-product-images .inside #product_images_container ul ul.actions li a.delete:hover::before{color:#a00}#woocommerce-product-images .inside #product_images_container ul li:hover ul.actions{display:block}#woocommerce-product-data .hndle{padding:10px}#woocommerce-product-data .hndle span{display:block;line-height:24px}#woocommerce-product-data .hndle .type_box{display:inline;line-height:inherit;vertical-align:baseline}#woocommerce-product-data .hndle select{margin:0}#woocommerce-product-data .hndle label{padding-right:1em;font-size:12px;vertical-align:baseline}#woocommerce-product-data .hndle label:first-child{margin-right:1em;border-right:1px solid #dfdfdf}#woocommerce-product-data .hndle input,#woocommerce-product-data .hndle select{margin-top:-3px 0 0;vertical-align:middle}#woocommerce-product-data .hndle select{margin-left:.5em}#woocommerce-product-data>.handlediv{margin-top:4px}#woocommerce-product-data .wrap{margin:0}#woocommerce-coupon-description{padding:3px 8px;font-size:1.7em;line-height:1.42em;height:auto;width:100%;outline:0;margin:10px 0;display:block}#woocommerce-coupon-description::-webkit-input-placeholder{line-height:1.42em;color:#bbb}#woocommerce-coupon-description::-moz-placeholder{line-height:1.42em;color:#bbb}#woocommerce-coupon-description:-ms-input-placeholder{line-height:1.42em;color:#bbb}#woocommerce-coupon-description:-moz-placeholder{line-height:1.42em;color:#bbb}#woocommerce-coupon-data .panel-wrap,#woocommerce-product-data .panel-wrap{background:#fff}#woocommerce-coupon-data .wc-metaboxes-wrapper,#woocommerce-coupon-data .woocommerce_options_panel,#woocommerce-product-data .wc-metaboxes-wrapper,#woocommerce-product-data .woocommerce_options_panel{float:left;width:80%}#woocommerce-coupon-data .wc-metaboxes-wrapper .wc-radios,#woocommerce-coupon-data .woocommerce_options_panel .wc-radios,#woocommerce-product-data .wc-metaboxes-wrapper .wc-radios,#woocommerce-product-data .woocommerce_options_panel .wc-radios{display:block;float:left;margin:0}#woocommerce-coupon-data .wc-metaboxes-wrapper .wc-radios li,#woocommerce-coupon-data .woocommerce_options_panel .wc-radios li,#woocommerce-product-data .wc-metaboxes-wrapper .wc-radios li,#woocommerce-product-data .woocommerce_options_panel .wc-radios li{display:block;padding:0 0 10px}#woocommerce-coupon-data .wc-metaboxes-wrapper .wc-radios li input,#woocommerce-coupon-data .woocommerce_options_panel .wc-radios li input,#woocommerce-product-data .wc-metaboxes-wrapper .wc-radios li input,#woocommerce-product-data .woocommerce_options_panel .wc-radios li input{width:auto}#woocommerce-coupon-data .panel-wrap,#woocommerce-product-data .panel-wrap,.woocommerce .panel-wrap{overflow:hidden}#woocommerce-coupon-data ul.wc-tabs,#woocommerce-product-data ul.wc-tabs,.woocommerce ul.wc-tabs{margin:0;width:20%;float:left;line-height:1em;padding:0 0 10px;position:relative;background-color:#fafafa;border-right:1px solid #eee;box-sizing:border-box}#woocommerce-coupon-data ul.wc-tabs::after,#woocommerce-product-data ul.wc-tabs::after,.woocommerce ul.wc-tabs::after{content:"";display:block;width:100%;height:9999em;position:absolute;bottom:-9999em;left:0;background-color:#fafafa;border-right:1px solid #eee}#woocommerce-coupon-data ul.wc-tabs li,#woocommerce-product-data ul.wc-tabs li,.woocommerce ul.wc-tabs li{margin:0;padding:0;display:block;position:relative}#woocommerce-coupon-data ul.wc-tabs li a,#woocommerce-product-data ul.wc-tabs li a,.woocommerce ul.wc-tabs li a{margin:0;padding:10px;display:block;box-shadow:none;text-decoration:none;line-height:20px!important;border-bottom:1px solid #eee}#woocommerce-coupon-data ul.wc-tabs li a span,#woocommerce-product-data ul.wc-tabs li a span,.woocommerce ul.wc-tabs li a span{margin-left:.618em;margin-right:.618em}#woocommerce-coupon-data ul.wc-tabs li a::before,#woocommerce-product-data ul.wc-tabs li a::before,.woocommerce ul.wc-tabs li a::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;content:"";text-decoration:none}#woocommerce-coupon-data ul.wc-tabs li.general_options a::before,#woocommerce-product-data ul.wc-tabs li.general_options a::before,.woocommerce ul.wc-tabs li.general_options a::before{content:"\f107"}#woocommerce-coupon-data ul.wc-tabs li.inventory_options a::before,#woocommerce-product-data ul.wc-tabs li.inventory_options a::before,.woocommerce ul.wc-tabs li.inventory_options a::before{content:"\f481"}#woocommerce-coupon-data ul.wc-tabs li.shipping_options a::before,#woocommerce-product-data ul.wc-tabs li.shipping_options a::before,.woocommerce ul.wc-tabs li.shipping_options a::before{font-family:WooCommerce;content:"\e01a"}#woocommerce-coupon-data ul.wc-tabs li.linked_product_options a::before,#woocommerce-product-data ul.wc-tabs li.linked_product_options a::before,.woocommerce ul.wc-tabs li.linked_product_options a::before{content:"\f103"}#woocommerce-coupon-data ul.wc-tabs li.attribute_options a::before,#woocommerce-product-data ul.wc-tabs li.attribute_options a::before,.woocommerce ul.wc-tabs li.attribute_options a::before{content:"\f175"}#woocommerce-coupon-data ul.wc-tabs li.advanced_options a::before,#woocommerce-product-data ul.wc-tabs li.advanced_options a::before,.woocommerce ul.wc-tabs li.advanced_options a::before{font-family:Dashicons;content:"\f111"}#woocommerce-coupon-data ul.wc-tabs li.marketplace-suggestions_options a::before,#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_options a::before,.woocommerce ul.wc-tabs li.marketplace-suggestions_options a::before{content:none}#woocommerce-coupon-data ul.wc-tabs li.variations_options a::before,#woocommerce-product-data ul.wc-tabs li.variations_options a::before,.woocommerce ul.wc-tabs li.variations_options a::before{content:"\f509"}#woocommerce-coupon-data ul.wc-tabs li.usage_restriction_options a::before,#woocommerce-product-data ul.wc-tabs li.usage_restriction_options a::before,.woocommerce ul.wc-tabs li.usage_restriction_options a::before{font-family:WooCommerce;content:"\e602"}#woocommerce-coupon-data ul.wc-tabs li.usage_limit_options a::before,#woocommerce-product-data ul.wc-tabs li.usage_limit_options a::before,.woocommerce ul.wc-tabs li.usage_limit_options a::before{font-family:WooCommerce;content:"\e601"}#woocommerce-coupon-data ul.wc-tabs li.general_coupon_data a::before,#woocommerce-product-data ul.wc-tabs li.general_coupon_data a::before,.woocommerce ul.wc-tabs li.general_coupon_data a::before{font-family:WooCommerce;content:"\e600"}#woocommerce-coupon-data ul.wc-tabs li.active a,#woocommerce-product-data ul.wc-tabs li.active a,.woocommerce ul.wc-tabs li.active a{color:#555;position:relative;background-color:#eee}.woocommerce_page_wc-settings input[type=email],.woocommerce_page_wc-settings input[type=url]{direction:ltr}.woocommerce_page_wc-settings .shippingrows th.check-column{padding-top:20px}.woocommerce_page_wc-settings .shippingrows tfoot th{padding-left:10px}.woocommerce_page_wc-settings .shippingrows .add.button::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin-right:.618em;content:"";text-decoration:none}.woocommerce_page_wc-settings h3.wc-settings-sub-title{font-size:1.2em}#woocommerce-coupon-data .inside,#woocommerce-order-data .inside,#woocommerce-order-downloads .inside,#woocommerce-product-data .inside,#woocommerce-product-type-options .inside{margin:0;padding:0}.panel,.woocommerce_options_panel{padding:9px;color:#555}.panel .form-field .woocommerce-help-tip,.woocommerce_options_panel .form-field .woocommerce-help-tip{font-size:1.4em}.panel,.woocommerce_page_settings .woocommerce_options_panel{padding:0}#woocommerce-product-specs .inside,#woocommerce-product-type-options .panel{margin:0;padding:9px}#woocommerce-product-type-options .panel p,.woocommerce_options_panel fieldset.form-field,.woocommerce_options_panel p{margin:0 0 9px;font-size:12px;padding:5px 9px;line-height:24px}#woocommerce-product-type-options .panel p::after,.woocommerce_options_panel fieldset.form-field::after,.woocommerce_options_panel p::after{content:".";display:block;height:0;clear:both;visibility:hidden}.woocommerce_options_panel .checkbox,.woocommerce_variable_attributes .checkbox{margin:4px 0!important;vertical-align:middle;float:left}.woocommerce_options_panel .downloadable_files table,.woocommerce_variations .downloadable_files table{width:100%;padding:0!important}.woocommerce_options_panel .downloadable_files table th,.woocommerce_variations .downloadable_files table th{padding:7px 0 7px 7px!important}.woocommerce_options_panel .downloadable_files table th.sort,.woocommerce_variations .downloadable_files table th.sort{width:17px;padding:7px!important}.woocommerce_options_panel .downloadable_files table th .woocommerce-help-tip,.woocommerce_variations .downloadable_files table th .woocommerce-help-tip{font-size:1.1em;margin-left:0}.woocommerce_options_panel .downloadable_files table td,.woocommerce_variations .downloadable_files table td{vertical-align:middle!important;padding:4px 0 4px 7px!important;position:relative}.woocommerce_options_panel .downloadable_files table td:last-child,.woocommerce_variations .downloadable_files table td:last-child{padding-right:7px!important}.woocommerce_options_panel .downloadable_files table td input.input_text,.woocommerce_variations .downloadable_files table td input.input_text{width:100%;float:none;min-width:0;margin:1px 0}.woocommerce_options_panel .downloadable_files table td .upload_file_button,.woocommerce_variations .downloadable_files table td .upload_file_button{width:auto;float:right;cursor:pointer}.woocommerce_options_panel .downloadable_files table td .delete,.woocommerce_variations .downloadable_files table td .delete{display:block;text-indent:-9999px;position:relative;height:1em;width:1em;font-size:1.2em}.woocommerce_options_panel .downloadable_files table td .delete::before,.woocommerce_variations .downloadable_files table td .delete::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#999}.woocommerce_options_panel .downloadable_files table td .delete:hover::before,.woocommerce_variations .downloadable_files table td .delete:hover::before{color:#a00}.woocommerce_options_panel .downloadable_files table td.sort,.woocommerce_variations .downloadable_files table td.sort{width:17px;cursor:move;font-size:15px;text-align:center;background:#f9f9f9;padding-right:7px!important}.woocommerce_options_panel .downloadable_files table td.sort::before,.woocommerce_variations .downloadable_files table td.sort::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:1;color:#999;display:block;width:17px;float:left;height:100%}.woocommerce_options_panel .downloadable_files table td.sort:hover::before,.woocommerce_variations .downloadable_files table td.sort:hover::before{color:#333}.woocommerce_attribute h3 .sort,.woocommerce_variation h3 .sort{width:17px;height:26px;cursor:move;float:right;font-size:15px;font-weight:400;margin-right:.5em;visibility:hidden;text-align:center;vertical-align:middle}.woocommerce_attribute h3 .sort::before,.woocommerce_variation h3 .sort::before{content:"\f333";font-family:Dashicons;text-align:center;line-height:28px;color:#999;display:block;width:17px;float:left;height:100%}.woocommerce_attribute h3 .sort:hover::before,.woocommerce_variation h3 .sort:hover::before{color:#777}.woocommerce_attribute h3:hover .sort,.woocommerce_attribute.ui-sortable-helper .sort,.woocommerce_variation h3:hover .sort,.woocommerce_variation.ui-sortable-helper .sort{visibility:visible}.woocommerce_options_panel{min-height:175px;box-sizing:border-box}.woocommerce_options_panel .downloadable_files{padding:0 9px 0 162px;position:relative;margin:9px 0}.woocommerce_options_panel .downloadable_files label{position:absolute;left:0;margin:0 0 0 12px;line-height:24px}.woocommerce_options_panel p{margin:9px 0}.woocommerce_options_panel fieldset.form-field,.woocommerce_options_panel p.form-field{padding:5px 20px 5px 162px!important}.woocommerce_options_panel .sale_price_dates_fields .short:first-of-type{margin-bottom:1em}.woocommerce_options_panel .sale_price_dates_fields .short:nth-of-type(2){clear:left}.woocommerce_options_panel label,.woocommerce_options_panel legend{float:left;width:150px;padding:0;margin:0 0 0 -150px}.woocommerce_options_panel label .req,.woocommerce_options_panel legend .req{font-weight:700;font-style:normal;color:#a00}.woocommerce_options_panel .description{padding:0;margin:0 0 0 7px;clear:none;display:inline}.woocommerce_options_panel .description-block{margin-left:0;display:block}.woocommerce_options_panel input,.woocommerce_options_panel select,.woocommerce_options_panel textarea{margin:0}.woocommerce_options_panel textarea{float:left;height:3.5em;line-height:1.5em;vertical-align:top}.woocommerce_options_panel input[type=email],.woocommerce_options_panel input[type=number],.woocommerce_options_panel input[type=password],.woocommerce_options_panel input[type=text]{width:50%;float:left}.woocommerce_options_panel input.button{width:auto;margin-left:8px}.woocommerce_options_panel select{float:left}.woocommerce_options_panel .short,.woocommerce_options_panel input[type=email].short,.woocommerce_options_panel input[type=number].short,.woocommerce_options_panel input[type=password].short,.woocommerce_options_panel input[type=text].short{width:50%}.woocommerce_options_panel .sized{width:auto!important;margin-right:6px}.woocommerce_options_panel .options_group{border-top:1px solid #fff;border-bottom:1px solid #eee}.woocommerce_options_panel .options_group:first-child{border-top:0}.woocommerce_options_panel .options_group:last-child{border-bottom:0}.woocommerce_options_panel .options_group fieldset{margin:9px 0;font-size:12px;padding:5px 9px;line-height:24px}.woocommerce_options_panel .options_group fieldset label{width:auto;float:none}.woocommerce_options_panel .options_group fieldset ul{float:left;width:50%;margin:0;padding:0}.woocommerce_options_panel .options_group fieldset ul li{margin:0;width:auto}.woocommerce_options_panel .options_group fieldset ul li input{width:auto;float:none;margin-right:4px}.woocommerce_options_panel .options_group fieldset ul.wc-radios label{margin-left:0}.woocommerce_options_panel .dimensions_field .wrap{display:block;width:50%}.woocommerce_options_panel .dimensions_field .wrap input{width:30.75%;margin-right:3.8%}.woocommerce_options_panel .dimensions_field .wrap .last{margin-right:0}.woocommerce_options_panel.padded{padding:1em}.woocommerce_options_panel .select2-container{float:left}#woocommerce-product-data input.dp-applied{float:left}#grouped_product_options,#simple_product_options,#virtual_product_options{padding:12px;font-style:italic;color:#666}.wc-metaboxes-wrapper .toolbar{margin:0!important;border-top:1px solid #fff;border-bottom:1px solid #eee;padding:9px 12px!important}.wc-metaboxes-wrapper .toolbar:first-child{border-top:0}.wc-metaboxes-wrapper .toolbar:last-child{border-bottom:0}.wc-metaboxes-wrapper .toolbar .add_variation{float:right;margin-left:5px}.wc-metaboxes-wrapper .toolbar .cancel-variation-changes,.wc-metaboxes-wrapper .toolbar .save-variation-changes{float:left;margin-right:5px}.wc-metaboxes-wrapper p.toolbar{overflow:hidden;zoom:1}.wc-metaboxes-wrapper .expand-close{margin-right:2px;color:#777;font-size:12px;font-style:italic}.wc-metaboxes-wrapper .expand-close a{background:0 0;padding:0;font-size:12px;text-decoration:none}.wc-metaboxes-wrapper#product_attributes .expand-close{float:right;line-height:28px}.wc-metaboxes-wrapper .fr,.wc-metaboxes-wrapper button.add_variable_attribute{float:right;margin:0 0 0 6px}.wc-metaboxes-wrapper .wc-metaboxes{border-bottom:1px solid #eee}.wc-metaboxes-wrapper .wc-metabox-sortable-placeholder{border-color:#bbb;background-color:#f5f5f5;margin-bottom:9px;border-width:1px;border-style:dashed}.wc-metaboxes-wrapper .wc-metabox{background:#fff;border-bottom:1px solid #eee;margin:0!important}.wc-metaboxes-wrapper .wc-metabox select{font-weight:400}.wc-metaboxes-wrapper .wc-metabox:last-of-type{border-bottom:0}.wc-metaboxes-wrapper .wc-metabox .handlediv{width:27px;float:right}.wc-metaboxes-wrapper .wc-metabox .handlediv::before{content:"\f142"!important;cursor:pointer;display:inline-block;font:400 20px/1 Dashicons;line-height:.5!important;padding:8px 10px;position:relative;right:12px;top:0}.wc-metaboxes-wrapper .wc-metabox.closed{border-radius:3px}.wc-metaboxes-wrapper .wc-metabox.closed .handlediv::before{content:"\f140"!important}.wc-metaboxes-wrapper .wc-metabox.closed h3{border:0}.wc-metaboxes-wrapper .wc-metabox h3{margin:0!important;padding:.75em .75em .75em 1em!important;font-size:1em!important;overflow:hidden;zoom:1;cursor:move}.wc-metaboxes-wrapper .wc-metabox h3 a.delete,.wc-metaboxes-wrapper .wc-metabox h3 button{float:right}.wc-metaboxes-wrapper .wc-metabox h3 a.delete{color:red;font-weight:400;line-height:26px;text-decoration:none;position:relative;visibility:hidden}.wc-metaboxes-wrapper .wc-metabox h3 strong{font-weight:400;line-height:26px;font-weight:700}.wc-metaboxes-wrapper .wc-metabox h3 select{font-family:sans-serif;max-width:20%;margin:.25em .25em .25em 0}.wc-metaboxes-wrapper .wc-metabox h3 .handlediv{background-position:6px 5px!important;visibility:hidden;height:26px}.wc-metaboxes-wrapper .wc-metabox h3.fixed{cursor:pointer!important}.wc-metaboxes-wrapper .wc-metabox.woocommerce_attribute h3,.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation h3{cursor:pointer;padding:.5em .75em .5em 1em!important}.wc-metaboxes-wrapper .wc-metabox.woocommerce_attribute h3 .handlediv,.wc-metaboxes-wrapper .wc-metabox.woocommerce_attribute h3 .sort,.wc-metaboxes-wrapper .wc-metabox.woocommerce_attribute h3 a.delete,.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation h3 .handlediv,.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation h3 .sort,.wc-metaboxes-wrapper .wc-metabox.woocommerce_variation h3 a.delete{margin-top:.25em}.wc-metaboxes-wrapper .wc-metabox h3:hover .handlediv,.wc-metaboxes-wrapper .wc-metabox h3:hover a.delete,.wc-metaboxes-wrapper .wc-metabox.ui-sortable-helper .handlediv,.wc-metaboxes-wrapper .wc-metabox.ui-sortable-helper a.delete{visibility:visible}.wc-metaboxes-wrapper .wc-metabox table{width:100%;position:relative;background-color:#fdfdfd;padding:1em;border-top:1px solid #eee}.wc-metaboxes-wrapper .wc-metabox table td{text-align:left;padding:0 6px 1em 0;vertical-align:top;border:0}.wc-metaboxes-wrapper .wc-metabox table td label{text-align:left;display:block;line-height:21px}.wc-metaboxes-wrapper .wc-metabox table td input{float:left;min-width:200px}.wc-metaboxes-wrapper .wc-metabox table td input,.wc-metaboxes-wrapper .wc-metabox table td textarea{width:100%;margin:0;display:block;font-size:14px;padding:4px;color:#555}.wc-metaboxes-wrapper .wc-metabox table td .select2-container,.wc-metaboxes-wrapper .wc-metabox table td select{width:100%!important}.wc-metaboxes-wrapper .wc-metabox table td input.short{width:200px}.wc-metaboxes-wrapper .wc-metabox table td input.checkbox{width:16px;min-width:inherit;vertical-align:text-bottom;display:inline-block;float:none}.wc-metaboxes-wrapper .wc-metabox table td.attribute_name{width:200px}.wc-metaboxes-wrapper .wc-metabox table .minus,.wc-metaboxes-wrapper .wc-metabox table .plus{margin-top:6px}.wc-metaboxes-wrapper .wc-metabox table .fl{float:left}.wc-metaboxes-wrapper .wc-metabox table .fr{float:right}.variations-pagenav{float:right;line-height:24px}.variations-pagenav .displaying-num{color:#777;font-size:12px;font-style:italic}.variations-pagenav a{padding:0 10px 3px;background:rgba(0,0,0,.05);font-size:16px;font-weight:400;text-decoration:none}.variations-pagenav a.disabled,.variations-pagenav a.disabled:active,.variations-pagenav a.disabled:focus,.variations-pagenav a.disabled:hover{color:#a0a5aa;background:rgba(0,0,0,.05)}.variations-defaults{float:left}.variations-defaults select{margin:.25em .25em .25em 0}.woocommerce_variable_attributes{background-color:#fdfdfd;border-top:1px solid #eee}.woocommerce_variable_attributes .data{padding:1em 2em}.woocommerce_variable_attributes .data::after,.woocommerce_variable_attributes .data::before{content:" ";display:table}.woocommerce_variable_attributes .data::after{clear:both}.woocommerce_variable_attributes .upload_image_button{display:block;width:64px;height:64px;float:left;margin-right:20px;position:relative;cursor:pointer}.woocommerce_variable_attributes .upload_image_button img{width:100%;height:auto;display:none}.woocommerce_variable_attributes .upload_image_button::before{content:"\f128";font-family:Dashicons;position:absolute;top:0;left:0;right:0;bottom:0;text-align:center;line-height:64px;font-size:64px;font-weight:400;-webkit-font-smoothing:antialiased}.woocommerce_variable_attributes .upload_image_button.remove img{display:block}.woocommerce_variable_attributes .upload_image_button.remove::before{content:"\f335";display:none}.woocommerce_variable_attributes .upload_image_button.remove:hover::before{display:block}.woocommerce_variable_attributes .options{border:1px solid #eee;border-width:1px 0;padding:.25em 0}.woocommerce_variable_attributes .options label{display:inline-block;padding:4px 1em 2px 0}.woocommerce_variable_attributes .options input[type=checkbox]{margin:0 5px 0 .5em!important;vertical-align:middle}.form-row label{display:inline-block}.form-row .woocommerce-help-tip{float:right}.form-row input[type=color],.form-row input[type=date],.form-row input[type=datetime-local],.form-row input[type=datetime],.form-row input[type=email],.form-row input[type=month],.form-row input[type=number],.form-row input[type=password],.form-row input[type=search],.form-row input[type=tel],.form-row input[type=text],.form-row input[type=time],.form-row input[type=url],.form-row input[type=week],.form-row select,.form-row textarea{width:100%;vertical-align:middle;margin:2px 0 0;padding:5px}.form-row select{height:40px}.form-row.dimensions_field .wrap{clear:left;display:block}.form-row.dimensions_field input{width:33%;float:left;vertical-align:middle}.form-row.dimensions_field input:last-of-type{margin-right:0;width:34%}.form-row.form-row-first,.form-row.form-row-last{width:48%;float:right}.form-row.form-row-first{clear:both;float:left}.form-row.form-row-full{clear:both}.tips{cursor:help;text-decoration:none}img.tips{padding:5px 0 0}#tiptip_holder{display:none;z-index:8675309;position:absolute;top:0;left:0}#tiptip_holder.tip_top{padding-bottom:5px}#tiptip_holder.tip_top #tiptip_arrow_inner{margin-top:-7px;margin-left:-6px;border-top-color:#333}#tiptip_holder.tip_bottom{padding-top:5px}#tiptip_holder.tip_bottom #tiptip_arrow_inner{margin-top:-5px;margin-left:-6px;border-bottom-color:#333}#tiptip_holder.tip_right{padding-left:5px}#tiptip_holder.tip_right #tiptip_arrow_inner{margin-top:-6px;margin-left:-5px;border-right-color:#333}#tiptip_holder.tip_left{padding-right:5px}#tiptip_holder.tip_left #tiptip_arrow_inner{margin-top:-6px;margin-left:-7px;border-left-color:#333}#tiptip_content,.chart-tooltip,.wc_error_tip{color:#fff;font-size:.8em;max-width:150px;background:#333;text-align:center;border-radius:3px;padding:.618em 1em;box-shadow:0 1px 3px rgba(0,0,0,.2)}#tiptip_content code,.chart-tooltip code,.wc_error_tip code{padding:1px;background:#888}#tiptip_arrow,#tiptip_arrow_inner{position:absolute;border-color:transparent;border-style:solid;border-width:6px;height:0;width:0}.wc_error_tip{max-width:20em;line-height:1.8em;position:absolute;white-space:normal;background:#d82223;margin:1.5em 1px 0 -1em;z-index:9999999}.wc_error_tip::after{content:"";display:block;border:8px solid #d82223;border-right-color:transparent;border-left-color:transparent;border-top-color:transparent;position:absolute;top:-3px;left:50%;margin:-1em 0 0 -3px}img.ui-datepicker-trigger{vertical-align:middle;margin-top:-1px;cursor:pointer}.wc-metabox-content img.ui-datepicker-trigger,.woocommerce_options_panel img.ui-datepicker-trigger{float:left;margin-right:8px;margin-top:4px;margin-left:4px}#ui-datepicker-div{display:none}.woocommerce-reports-remove-filter{color:red;text-decoration:none}.woocommerce-reports-wide.woocommerce-reports-wrap,.woocommerce-reports-wrap.woocommerce-reports-wrap{margin-left:300px;padding-top:18px}.woocommerce-reports-wide.halved,.woocommerce-reports-wrap.halved{margin:0;overflow:hidden;zoom:1}.woocommerce-reports-wide .widefat th,.woocommerce-reports-wrap .widefat th{padding:7px}.woocommerce-reports-wide .widefat td,.woocommerce-reports-wrap .widefat td{vertical-align:top;padding:7px}.woocommerce-reports-wide .widefat td .description,.woocommerce-reports-wrap .widefat td .description{margin:4px 0 0}.woocommerce-reports-wide .postbox::after,.woocommerce-reports-wrap .postbox::after{content:".";display:block;height:0;clear:both;visibility:hidden}.woocommerce-reports-wide .postbox h3,.woocommerce-reports-wrap .postbox h3{cursor:default!important}.woocommerce-reports-wide .postbox .inside,.woocommerce-reports-wrap .postbox .inside{padding:10px;margin:0!important}.woocommerce-reports-wide .postbox div.stats_range,.woocommerce-reports-wide .postbox h3.stats_range,.woocommerce-reports-wrap .postbox div.stats_range,.woocommerce-reports-wrap .postbox h3.stats_range{border-bottom-color:#dfdfdf;margin:0;padding:0!important}.woocommerce-reports-wide .postbox div.stats_range .export_csv,.woocommerce-reports-wide .postbox h3.stats_range .export_csv,.woocommerce-reports-wrap .postbox div.stats_range .export_csv,.woocommerce-reports-wrap .postbox h3.stats_range .export_csv{float:right;line-height:26px;border-left:1px solid #dfdfdf;padding:10px;display:block;text-decoration:none}.woocommerce-reports-wide .postbox div.stats_range .export_csv::before,.woocommerce-reports-wide .postbox h3.stats_range .export_csv::before,.woocommerce-reports-wrap .postbox div.stats_range .export_csv::before,.woocommerce-reports-wrap .postbox h3.stats_range .export_csv::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;content:"";text-decoration:none;margin-right:4px}.woocommerce-reports-wide .postbox div.stats_range ul,.woocommerce-reports-wide .postbox h3.stats_range ul,.woocommerce-reports-wrap .postbox div.stats_range ul,.woocommerce-reports-wrap .postbox h3.stats_range ul{list-style:none outside;margin:0;padding:0;zoom:1;background:#f5f5f5;border-bottom:1px solid #ccc}.woocommerce-reports-wide .postbox div.stats_range ul::after,.woocommerce-reports-wide .postbox div.stats_range ul::before,.woocommerce-reports-wide .postbox h3.stats_range ul::after,.woocommerce-reports-wide .postbox h3.stats_range ul::before,.woocommerce-reports-wrap .postbox div.stats_range ul::after,.woocommerce-reports-wrap .postbox div.stats_range ul::before,.woocommerce-reports-wrap .postbox h3.stats_range ul::after,.woocommerce-reports-wrap .postbox h3.stats_range ul::before{content:" ";display:table}.woocommerce-reports-wide .postbox div.stats_range ul::after,.woocommerce-reports-wide .postbox h3.stats_range ul::after,.woocommerce-reports-wrap .postbox div.stats_range ul::after,.woocommerce-reports-wrap .postbox h3.stats_range ul::after{clear:both}.woocommerce-reports-wide .postbox div.stats_range ul li,.woocommerce-reports-wide .postbox h3.stats_range ul li,.woocommerce-reports-wrap .postbox div.stats_range ul li,.woocommerce-reports-wrap .postbox h3.stats_range ul li{float:left;margin:0;padding:0;line-height:26px;font-weight:700;font-size:14px}.woocommerce-reports-wide .postbox div.stats_range ul li a,.woocommerce-reports-wide .postbox h3.stats_range ul li a,.woocommerce-reports-wrap .postbox div.stats_range ul li a,.woocommerce-reports-wrap .postbox h3.stats_range ul li a{border-right:1px solid #dfdfdf;padding:10px;display:block;text-decoration:none}.woocommerce-reports-wide .postbox div.stats_range ul li.active,.woocommerce-reports-wide .postbox h3.stats_range ul li.active,.woocommerce-reports-wrap .postbox div.stats_range ul li.active,.woocommerce-reports-wrap .postbox h3.stats_range ul li.active{background:#fff;box-shadow:0 4px 0 0 #fff}.woocommerce-reports-wide .postbox div.stats_range ul li.active a,.woocommerce-reports-wide .postbox h3.stats_range ul li.active a,.woocommerce-reports-wrap .postbox div.stats_range ul li.active a,.woocommerce-reports-wrap .postbox h3.stats_range ul li.active a{color:#777}.woocommerce-reports-wide .postbox div.stats_range ul li.custom,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom{padding:9px 10px;vertical-align:middle}.woocommerce-reports-wide .postbox div.stats_range ul li.custom div,.woocommerce-reports-wide .postbox div.stats_range ul li.custom form,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom div,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom form,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom div,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom form,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom div,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom form{display:inline;margin:0}.woocommerce-reports-wide .postbox div.stats_range ul li.custom div input.range_datepicker,.woocommerce-reports-wide .postbox div.stats_range ul li.custom form input.range_datepicker,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom div input.range_datepicker,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom form input.range_datepicker,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom div input.range_datepicker,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom form input.range_datepicker,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom div input.range_datepicker,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom form input.range_datepicker{padding:0;margin:0 10px 0 0;background:0 0;border:0;color:#777;text-align:center;box-shadow:none}.woocommerce-reports-wide .postbox div.stats_range ul li.custom div input.range_datepicker.from,.woocommerce-reports-wide .postbox div.stats_range ul li.custom form input.range_datepicker.from,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom div input.range_datepicker.from,.woocommerce-reports-wide .postbox h3.stats_range ul li.custom form input.range_datepicker.from,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom div input.range_datepicker.from,.woocommerce-reports-wrap .postbox div.stats_range ul li.custom form input.range_datepicker.from,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom div input.range_datepicker.from,.woocommerce-reports-wrap .postbox h3.stats_range ul li.custom form input.range_datepicker.from{margin-right:0}.woocommerce-reports-wide .postbox .chart-with-sidebar,.woocommerce-reports-wrap .postbox .chart-with-sidebar{padding:12px 12px 12px 249px;margin:0!important}.woocommerce-reports-wide .postbox .chart-with-sidebar .chart-sidebar,.woocommerce-reports-wrap .postbox .chart-with-sidebar .chart-sidebar{width:225px;margin-left:-237px;float:left}.woocommerce-reports-wide .postbox .chart-widgets,.woocommerce-reports-wrap .postbox .chart-widgets{margin:0;padding:0}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget{margin:0 0 1em;background:#fafafa;border:1px solid #dfdfdf}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget::after,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget::after{content:".";display:block;height:0;clear:both;visibility:hidden}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget h4,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget h4{background:#fff;border:1px solid #dfdfdf;border-left-width:0;border-right-width:0;padding:10px;margin:0;color:#2ea2cc;border-top-width:0;background-image:-webkit-gradient(linear,left bottom,left top,from(#ececec),to(#f9f9f9));background-image:linear-gradient(to top,#ececec,#f9f9f9)}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget h4.section_title:hover,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget h4.section_title:hover{color:#a00}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title{cursor:pointer}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title span,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title span{display:block}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title span::after,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title span::after{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;margin-left:.618em;content:"";text-decoration:none;float:right;font-size:.9em;line-height:1.618}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title.open,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title.open{color:#333}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section_title.open span::after,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section_title.open span::after{display:none}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section{border-bottom:1px solid #dfdfdf}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section .select2-container,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section .select2-container{width:100%!important}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .section:last-of-type,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .section:last-of-type{border-radius:0 0 3px 3px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table{width:100%}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td{padding:7px 10px;vertical-align:top;border-top:1px solid #e5e5e5;line-height:1.4em}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table tr:first-child td,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table tr:first-child td{border-top:0}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td.count,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td.count{background:#f5f5f5}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td.name,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td.name{max-width:175px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td.name a,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td.name a{word-wrap:break-word}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table td.sparkline,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table td.sparkline{vertical-align:middle}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table .wc_sparkline,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table .wc_sparkline{width:32px;height:1em;display:block;float:right}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget table tr.active td,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget table tr.active td{background:#f5f5f5}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget form,.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget p,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget form,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget p{margin:0;padding:10px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget form .submit,.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget p .submit,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget form .submit,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget p .submit{margin-top:10px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget #product_ids,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget #product_ids{width:100%}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .select_all,.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .select_none,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .select_all,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .select_none{float:right;color:#999;margin-left:4px;margin-top:10px}.woocommerce-reports-wide .postbox .chart-widgets li.chart-widget .description,.woocommerce-reports-wrap .postbox .chart-widgets li.chart-widget .description{margin-left:.5em;font-weight:400;opacity:.8}.woocommerce-reports-wide .postbox .chart-legend,.woocommerce-reports-wrap .postbox .chart-legend{list-style:none outside;margin:0 0 1em;padding:0;border:1px solid #dfdfdf;border-right-width:0;border-bottom-width:0;background:#fff}.woocommerce-reports-wide .postbox .chart-legend li,.woocommerce-reports-wrap .postbox .chart-legend li{border-right:5px solid #aaa;color:#aaa;padding:1em;display:block;margin:0;-webkit-transition:all ease .5s;transition:all ease .5s;box-shadow:inset 0 -1px 0 0 #dfdfdf}.woocommerce-reports-wide .postbox .chart-legend li strong,.woocommerce-reports-wrap .postbox .chart-legend li strong{font-size:1.618em;line-height:1.2em;color:#464646;font-weight:400;display:block;font-family:HelveticaNeue-Light,"Helvetica Neue Light","Helvetica Neue",sans-serif}.woocommerce-reports-wide .postbox .chart-legend li strong del,.woocommerce-reports-wrap .postbox .chart-legend li strong del{color:#e74c3c;font-weight:400}.woocommerce-reports-wide .postbox .chart-legend li:hover,.woocommerce-reports-wrap .postbox .chart-legend li:hover{box-shadow:inset 0 -1px 0 0 #dfdfdf,inset 300px 0 0 rgba(156,93,144,.1);border-right:5px solid #9c5d90!important;padding-left:1.5em;color:#9c5d90}.woocommerce-reports-wide .postbox .pie-chart-legend,.woocommerce-reports-wrap .postbox .pie-chart-legend{margin:12px 0 0;overflow:hidden}.woocommerce-reports-wide .postbox .pie-chart-legend li,.woocommerce-reports-wrap .postbox .pie-chart-legend li{float:left;margin:0;padding:6px 0 0;border-top:4px solid #999;text-align:center;box-sizing:border-box;width:50%}.woocommerce-reports-wide .postbox .stat,.woocommerce-reports-wrap .postbox .stat{font-size:1.5em!important;font-weight:700;text-align:center}.woocommerce-reports-wide .postbox .chart-placeholder,.woocommerce-reports-wrap .postbox .chart-placeholder{width:100%;height:650px;overflow:hidden;position:relative}.woocommerce-reports-wide .postbox .chart-prompt,.woocommerce-reports-wrap .postbox .chart-prompt{line-height:650px;margin:0;color:#999;font-size:1.2em;font-style:italic;text-align:center}.woocommerce-reports-wide .postbox .chart-container,.woocommerce-reports-wrap .postbox .chart-container{background:#fff;padding:12px;position:relative;border:1px solid #dfdfdf;border-radius:3px}.woocommerce-reports-wide .postbox .main .chart-legend,.woocommerce-reports-wrap .postbox .main .chart-legend{margin-top:12px}.woocommerce-reports-wide .postbox .main .chart-legend li,.woocommerce-reports-wrap .postbox .main .chart-legend li{border-right:0;margin:0 8px 0 0;float:left;border-top:4px solid #aaa}.woocommerce-reports-wide .woocommerce-reports-main,.woocommerce-reports-wrap .woocommerce-reports-main{float:left;min-width:100%}.woocommerce-reports-wide .woocommerce-reports-main table td,.woocommerce-reports-wrap .woocommerce-reports-main table td{padding:9px}.woocommerce-reports-wide .woocommerce-reports-sidebar,.woocommerce-reports-wrap .woocommerce-reports-sidebar{display:inline;width:281px;margin-left:-300px;clear:both;float:left}.woocommerce-reports-wide .woocommerce-reports-left,.woocommerce-reports-wrap .woocommerce-reports-left{width:49.5%;float:left}.woocommerce-reports-wide .woocommerce-reports-right,.woocommerce-reports-wrap .woocommerce-reports-right{width:49.5%;float:right}.woocommerce-wide-reports-wrap{padding-bottom:11px}.woocommerce-wide-reports-wrap .widefat .export-data{float:right}.woocommerce-wide-reports-wrap .widefat td,.woocommerce-wide-reports-wrap .widefat th{vertical-align:middle;padding:7px}form.report_filters p{vertical-align:middle}form.report_filters div,form.report_filters input,form.report_filters label{vertical-align:middle}.chart-tooltip{position:absolute;display:none;line-height:1}table.bar_chart{width:100%}table.bar_chart thead th{text-align:left;color:#ccc;padding:6px 0}table.bar_chart tbody th{padding:6px 0;width:25%;text-align:left!important;font-weight:400!important;border-bottom:1px solid #fee}table.bar_chart tbody td{text-align:right;line-height:24px;padding:6px 6px 6px 0;border-bottom:1px solid #fee}table.bar_chart tbody td span{color:#8a4b75;display:block}table.bar_chart tbody td span.alt{color:#47a03e;margin-top:6px}table.bar_chart tbody td.bars{position:relative;text-align:left;padding:6px 6px 6px 0;border-bottom:1px solid #fee}table.bar_chart tbody td.bars a,table.bar_chart tbody td.bars span{text-decoration:none;clear:both;background:#8a4b75;float:left;display:block;line-height:24px;height:24px;border-radius:3px}table.bar_chart tbody td.bars span.alt{clear:both;background:#47a03e}table.bar_chart tbody td.bars span.alt span{margin:0;color:#c5dec2!important;text-shadow:0 1px 0 #47a03e;background:0 0}.post-type-shop_order .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}.post-type-shop_coupon .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}.post-type-product .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}.woocommerce-BlankState--api .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}.woocommerce-BlankState--webhooks .woocommerce-BlankState-message::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:""}.woocommerce-BlankState{text-align:center;padding:5em 0 0}.woocommerce-BlankState .woocommerce-BlankState-message{color:#aaa;margin:0 auto 1.5em;line-height:1.5em;font-size:1.2em;max-width:500px}.woocommerce-BlankState .woocommerce-BlankState-message::before{color:#ddd;text-shadow:0 -1px 1px rgba(0,0,0,.2),0 1px 0 rgba(255,255,255,.8);font-size:8em;display:block;position:relative!important;top:auto;left:auto;line-height:1em;margin:0 0 .1875em}.woocommerce-BlankState .woocommerce-BlankState-cta{font-size:1.2em;padding:.75em 1.5em;margin:0 .25em;height:auto;display:inline-block!important}.post-type-product .woocommerce-BlankState,.post-type-shop_order .woocommerce-BlankState{max-width:764px;text-align:center;margin:auto}.post-type-product .woocommerce-BlankState .woocommerce-BlankState-message,.post-type-shop_order .woocommerce-BlankState .woocommerce-BlankState-message{color:#444;font-size:1.5em;margin:0 auto 1em}.post-type-product .woocommerce-BlankState .woocommerce-BlankState-message::before,.post-type-shop_order .woocommerce-BlankState .woocommerce-BlankState-message::before{font-size:120px}.post-type-product .woocommerce-BlankState .woocommerce-BlankState-buttons,.post-type-shop_order .woocommerce-BlankState .woocommerce-BlankState-buttons{margin-bottom:4em}.post-type-product #wp-pointer-2 .wp-pointer-arrow{left:240px}.post-type-product #wp-pointer-3 .wp-pointer-arrow,.post-type-product #wp-pointer-4 .wp-pointer-arrow{left:46%}@media only screen and (max-width:1280px){#order_data .order_data_column{width:48%}#order_data .order_data_column:first-child{width:100%}.woocommerce_options_panel .description{display:block;clear:both;margin-left:0}.woocommerce_options_panel .dimensions_field .wrap,.woocommerce_options_panel .short,.woocommerce_options_panel input[type=email].short,.woocommerce_options_panel input[type=number].short,.woocommerce_options_panel input[type=password].short,.woocommerce_options_panel input[type=text].short{width:80%}.woocommerce_options_panel .downloadable_files,.woocommerce_variations .downloadable_files{padding:0;clear:both}.woocommerce_options_panel .downloadable_files label,.woocommerce_variations .downloadable_files label{position:static}.woocommerce_options_panel .downloadable_files table,.woocommerce_variations .downloadable_files table{margin:0 12px 24px;width:94%}.woocommerce_options_panel .downloadable_files table .sort,.woocommerce_variations .downloadable_files table .sort{visibility:hidden}.woocommerce_options_panel .woocommerce_variable_attributes .downloadable_files table,.woocommerce_variations .woocommerce_variable_attributes .downloadable_files table{margin:0 0 1em;width:100%}}@media only screen and (max-width:900px){#woocommerce-coupon-data ul.coupon_data_tabs,#woocommerce-product-data .wc-tabs-back,#woocommerce-product-data ul.product_data_tabs{width:10%}#woocommerce-coupon-data .wc-metaboxes-wrapper,#woocommerce-coupon-data .woocommerce_options_panel,#woocommerce-product-data .wc-metaboxes-wrapper,#woocommerce-product-data .woocommerce_options_panel{width:90%}#woocommerce-coupon-data ul.coupon_data_tabs li a,#woocommerce-product-data ul.product_data_tabs li a{position:relative;text-indent:-999px;padding:10px}#woocommerce-coupon-data ul.coupon_data_tabs li a::before,#woocommerce-product-data ul.product_data_tabs li a::before{position:absolute;top:0;right:0;bottom:0;left:0;text-indent:0;text-align:center;line-height:40px;width:100%;height:40px}}@media only screen and (max-width:782px){#wp-excerpt-media-buttons a{font-size:16px;line-height:37px;height:39px;padding:0 20px 0 15px}#wp-excerpt-editor-tools{padding-top:20px;padding-right:15px;overflow:hidden;margin-bottom:-1px}#woocommerce-product-data .checkbox{width:25px}.variations-pagenav{float:none;text-align:center;font-size:18px}.variations-pagenav .displaying-num{font-size:16px}.variations-pagenav a{padding:8px 20px 11px;font-size:18px}.variations-pagenav select{padding:0 20px}.variations-defaults{float:none;text-align:center;margin-top:10px}.post-type-product .wp-list-table .column-thumb{display:none;text-align:left;padding-bottom:0}.post-type-product .wp-list-table .column-thumb::before{display:none!important}.post-type-product .wp-list-table .column-thumb img{max-width:32px}.post-type-product .wp-list-table .is-expanded td:not(.hidden){overflow:visible}.post-type-product .wp-list-table .toggle-row{top:-28px}.post-type-shop_order .wp-list-table .column-customer_message,.post-type-shop_order .wp-list-table .column-order_notes{text-align:inherit}.post-type-shop_order .wp-list-table .column-order_notes .note-on{font-size:1.3em;margin:0}.post-type-shop_order .wp-list-table .is-expanded td:not(.hidden){overflow:visible}.post-type-shop_order .wp-list-table .toggle-row{top:-15px}}@media only screen and (max-width:500px){.woocommerce_options_panel label,.woocommerce_options_panel legend{float:none;width:auto;display:block;margin:0}.woocommerce_options_panel fieldset.form-field,.woocommerce_options_panel p.form-field{padding:5px 20px!important}.addons-wcs-banner-block{-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column}.wc-addons-wrap .addons-wcs-banner-block{padding:40px}.wc-addons-wrap .addons-wcs-banner-block-image{padding:1em;text-align:center;width:100%;padding:2em 0;margin:0}.wc-addons-wrap .addons-wcs-banner-block-image .addons-img{margin:0}}.wc-backbone-modal *{box-sizing:border-box}.wc-backbone-modal .wc-backbone-modal-content{position:fixed;background:#fff;z-index:100000;left:50%;top:50%;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);max-width:100%;min-width:500px}.wc-backbone-modal .wc-backbone-modal-content article{overflow:auto}.wc-backbone-modal.wc-backbone-modal-shipping-method-settings .wc-backbone-modal-content{width:75%;min-width:500px}.wc-backbone-modal .select2-container{width:100%!important}@media screen and (max-width:782px){.wc-backbone-modal .wc-backbone-modal-content{width:100%;height:100%;min-width:100%}}.wc-backbone-modal-backdrop{position:fixed;top:0;left:0;right:0;bottom:0;min-height:360px;background:#000;opacity:.7;z-index:99900}.wc-backbone-modal-main{padding-bottom:55px}.wc-backbone-modal-main article,.wc-backbone-modal-main header{display:block;position:relative}.wc-backbone-modal-main .wc-backbone-modal-header{height:auto;background:#fcfcfc;padding:1em 1.5em;border-bottom:1px solid #ddd}.wc-backbone-modal-main .wc-backbone-modal-header h1{margin:0;font-size:18px;font-weight:700;line-height:1.5em}.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link{cursor:pointer;color:#777;height:54px;width:54px;padding:0;position:absolute;top:0;right:0;text-align:center;border:0;border-left:1px solid #ddd;background-color:transparent;-webkit-transition:color .1s ease-in-out,background .1s ease-in-out;transition:color .1s ease-in-out,background .1s ease-in-out}.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link::before{font:normal 22px/50px dashicons!important;color:#666;display:block;content:"\f335";font-weight:300}.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link:focus,.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link:hover{background:#ddd;border-color:#ccc;color:#000}.wc-backbone-modal-main .wc-backbone-modal-header .modal-close-link:focus{outline:0}.wc-backbone-modal-main article{padding:1.5em}.wc-backbone-modal-main article p{margin:1.5em 0}.wc-backbone-modal-main article p:first-child{margin-top:0}.wc-backbone-modal-main article p:last-child{margin-bottom:0}.wc-backbone-modal-main article .pagination{padding:10px 0 0;text-align:center}.wc-backbone-modal-main article table.widefat{margin:0;width:100%;border:0;box-shadow:none}.wc-backbone-modal-main article table.widefat thead th{padding:0 1em 1em 1em;text-align:left}.wc-backbone-modal-main article table.widefat thead th:first-child{padding-left:0}.wc-backbone-modal-main article table.widefat thead th:last-child{padding-right:0;text-align:right}.wc-backbone-modal-main article table.widefat tbody td,.wc-backbone-modal-main article table.widefat tbody th{padding:1em;text-align:left;vertical-align:middle}.wc-backbone-modal-main article table.widefat tbody td:first-child,.wc-backbone-modal-main article table.widefat tbody th:first-child{padding-left:0}.wc-backbone-modal-main article table.widefat tbody td:last-child,.wc-backbone-modal-main article table.widefat tbody th:last-child{padding-right:0;text-align:right}.wc-backbone-modal-main article table.widefat tbody td .select2-container,.wc-backbone-modal-main article table.widefat tbody td select,.wc-backbone-modal-main article table.widefat tbody th .select2-container,.wc-backbone-modal-main article table.widefat tbody th select{width:100%}.wc-backbone-modal-main footer{position:absolute;left:0;right:0;bottom:0;z-index:100;padding:1em 1.5em;background:#fcfcfc;border-top:1px solid #dfdfdf;box-shadow:0 -4px 4px -4px rgba(0,0,0,.1)}.wc-backbone-modal-main footer .inner{text-align:right;line-height:23px}.wc-backbone-modal-main footer .inner .button{margin-bottom:0}.select2-drop,.select2-dropdown{z-index:999999!important}.select2-results{line-height:1.5em}.select2-results .select2-results__group,.select2-results .select2-results__option{margin:0;padding:8px}.select2-results .description{display:block;color:#999;padding-top:4px}.select2-dropdown{border-color:#ddd}.select2-dropdown--below{box-shadow:0 1px 1px rgba(0,0,0,.1)}.select2-dropdown--above{box-shadow:0 -1px 1px rgba(0,0,0,.1)}.select2-container .select2-selection__rendered.ui-sortable li{cursor:move}.select2-container .select2-selection{border-color:#ddd}.select2-container .select2-search__field{min-width:150px}.select2-container .select2-selection--single{height:40px}.select2-container .select2-selection--single .select2-selection__rendered{line-height:40px;padding-right:24px}.select2-container .select2-selection--single .select2-selection__arrow{right:3px;height:36px}.select2-container .select2-selection--multiple{min-height:28px;border-radius:0;line-height:1.5}.select2-container .select2-selection--multiple li{margin:0}.select2-container .select2-selection--multiple .select2-selection__choice{padding:2px 6px}.select2-container .select2-selection--multiple .select2-selection__choice .description{display:none}.select2-container .select2-selection__clear{color:#999;margin-top:-1px;z-index:1}.select2-container .select2-search--inline .select2-search__field{font-family:inherit;font-size:inherit;font-weight:inherit;padding:3px 0}.woocommerce table.form-table .select2-container{min-width:400px!important}.wc-wp-version-gte-53 .select2-results .select2-results__group:focus,.wc-wp-version-gte-53 .select2-results .select2-results__option:focus{outline:0}.wc-wp-version-gte-53 .select2-dropdown{border-color:#007cba}.wc-wp-version-gte-53 .select2-dropdown::after{position:absolute;left:0;right:0;height:1px;background:#fff;content:""}.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #007cba,0 2px 1px rgba(0,0,0,.1)}.wc-wp-version-gte-53 .select2-dropdown--below::after{top:-1px}.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #007cba,0 -2px 1px rgba(0,0,0,.1)}.wc-wp-version-gte-53 .select2-dropdown--above::after{bottom:-1px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .select2-container{font-size:16px}}.wc-wp-version-gte-53 .select2-container:focus{outline:0}.wc-wp-version-gte-53 .select2-container .select2-selection--single{height:30px;border-color:#7e8993}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .select2-container .select2-selection--single{height:40px}}.wc-wp-version-gte-53 .select2-container .select2-selection--single:focus{outline:0}.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__rendered{line-height:28px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__rendered{line-height:38px}}.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__rendered:hover{color:#007cba}.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__arrow{right:1px;height:28px;width:23px;background:url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E") no-repeat right 5px top 55%;background-size:16px 16px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__arrow{height:38px}}.wc-wp-version-gte-53 .select2-container .select2-selection--single .select2-selection__arrow b{display:none}.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#007cba;box-shadow:0 0 0 1px #007cba}.wc-wp-version-gte-53 .select2-container .select2-selection--multiple{min-height:30px;border-color:#7e8993;border-radius:4px}.wc-wp-version-gte-53 .select2-container .select2-search--inline .select2-search__field{padding:0 0 0 3px;min-height:28px}@media only screen and (max-width:782px){.wc-wp-version-gte-53 .woocommerce table.form-table .select2-container{min-width:100%!important}}.wc-wp-version-gte-55 #woocommerce-product-data .hndle{display:block;line-height:24px}.wc-wp-version-gte-55 #woocommerce-product-data .hndle .type_box{display:inline;line-height:inherit;vertical-align:baseline}.admin-color-blue.wc-wp-version-gte-53 .select2-dropdown{border-color:#096484}.admin-color-blue.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #096484,0 2px 1px rgba(0,0,0,.1)}.admin-color-blue.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #096484,0 -2px 1px rgba(0,0,0,.1)}.admin-color-blue.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#096484}.admin-color-blue.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-blue.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-blue.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#096484;box-shadow:0 0 0 1px #096484}.admin-color-blue.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-blue.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#096484}.admin-color-coffee.wc-wp-version-gte-53 .select2-dropdown{border-color:#c7a589}.admin-color-coffee.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #c7a589,0 2px 1px rgba(0,0,0,.1)}.admin-color-coffee.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #c7a589,0 -2px 1px rgba(0,0,0,.1)}.admin-color-coffee.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#c7a589}.admin-color-coffee.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-coffee.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-coffee.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#c7a589;box-shadow:0 0 0 1px #c7a589}.admin-color-coffee.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-coffee.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#c7a589}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-dropdown{border-color:#a3b745}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #a3b745,0 2px 1px rgba(0,0,0,.1)}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #a3b745,0 -2px 1px rgba(0,0,0,.1)}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#a3b745}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#a3b745;box-shadow:0 0 0 1px #a3b745}.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-ectoplasm.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#a3b745}.admin-color-midnight.wc-wp-version-gte-53 .select2-dropdown{border-color:#e14d43}.admin-color-midnight.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #e14d43,0 2px 1px rgba(0,0,0,.1)}.admin-color-midnight.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #e14d43,0 -2px 1px rgba(0,0,0,.1)}.admin-color-midnight.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#e14d43}.admin-color-midnight.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-midnight.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-midnight.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#e14d43;box-shadow:0 0 0 1px #e14d43}.admin-color-midnight.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-midnight.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#e14d43}.admin-color-ocean.wc-wp-version-gte-53 .select2-dropdown{border-color:#9ebaa0}.admin-color-ocean.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #9ebaa0,0 2px 1px rgba(0,0,0,.1)}.admin-color-ocean.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #9ebaa0,0 -2px 1px rgba(0,0,0,.1)}.admin-color-ocean.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#9ebaa0}.admin-color-ocean.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-ocean.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-ocean.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#9ebaa0;box-shadow:0 0 0 1px #9ebaa0}.admin-color-ocean.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-ocean.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#9ebaa0}.admin-color-sunrise.wc-wp-version-gte-53 .select2-dropdown{border-color:#dd823b}.admin-color-sunrise.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #dd823b,0 2px 1px rgba(0,0,0,.1)}.admin-color-sunrise.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #dd823b,0 -2px 1px rgba(0,0,0,.1)}.admin-color-sunrise.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#dd823b}.admin-color-sunrise.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-sunrise.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-sunrise.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#dd823b;box-shadow:0 0 0 1px #dd823b}.admin-color-sunrise.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-sunrise.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#dd823b}.admin-color-light.wc-wp-version-gte-53 .select2-dropdown{border-color:#04a4cc}.admin-color-light.wc-wp-version-gte-53 .select2-dropdown--below{box-shadow:0 0 0 1px #04a4cc,0 2px 1px rgba(0,0,0,.1)}.admin-color-light.wc-wp-version-gte-53 .select2-dropdown--above{box-shadow:0 0 0 1px #04a4cc,0 -2px 1px rgba(0,0,0,.1)}.admin-color-light.wc-wp-version-gte-53 .select2-selection--single .select2-selection__rendered:hover{color:#04a4cc}.admin-color-light.wc-wp-version-gte-53 .select2-container.select2-container--focus .select2-selection--single,.admin-color-light.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--multiple,.admin-color-light.wc-wp-version-gte-53 .select2-container.select2-container--open .select2-selection--single{border-color:#04a4cc;box-shadow:0 0 0 1px #04a4cc}.admin-color-light.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[aria-selected],.admin-color-light.wc-wp-version-gte-53 .select2-container--default .select2-results__option--highlighted[data-selected]{background-color:#04a4cc}.post-type-product .tablenav .actions,.post-type-shop_order .tablenav .actions{overflow:visible}.post-type-product .tablenav input,.post-type-product .tablenav select,.post-type-shop_order .tablenav input,.post-type-shop_order .tablenav select{height:32px}.post-type-product .tablenav .select2-container,.post-type-shop_order .tablenav .select2-container{float:left;width:240px!important;font-size:14px;vertical-align:middle;margin:1px 6px 4px 1px}.woocommerce-exporter-wrapper,.woocommerce-importer-wrapper,.woocommerce-progress-form-wrapper{text-align:center;max-width:700px;margin:40px auto}.woocommerce-exporter-wrapper .error,.woocommerce-importer-wrapper .error,.woocommerce-progress-form-wrapper .error{text-align:left}.woocommerce-exporter-wrapper .wc-progress-steps,.woocommerce-importer-wrapper .wc-progress-steps,.woocommerce-progress-form-wrapper .wc-progress-steps{padding:0 0 24px;margin:0;list-style:none outside;overflow:hidden;color:#ccc;width:100%;display:-webkit-inline-box;display:inline-flex}.woocommerce-exporter-wrapper .wc-progress-steps li,.woocommerce-importer-wrapper .wc-progress-steps li,.woocommerce-progress-form-wrapper .wc-progress-steps li{width:25%;float:left;padding:0 0 .8em;margin:0;text-align:center;position:relative;border-bottom:4px solid #ccc;line-height:1.4em}.woocommerce-exporter-wrapper .wc-progress-steps li::before,.woocommerce-importer-wrapper .wc-progress-steps li::before,.woocommerce-progress-form-wrapper .wc-progress-steps li::before{content:"";border:4px solid #ccc;border-radius:100%;width:4px;height:4px;position:absolute;bottom:0;left:50%;margin-left:-6px;margin-bottom:-8px;background:#fff}.woocommerce-exporter-wrapper .wc-progress-steps li.active,.woocommerce-importer-wrapper .wc-progress-steps li.active,.woocommerce-progress-form-wrapper .wc-progress-steps li.active{border-color:#a16696;color:#a16696}.woocommerce-exporter-wrapper .wc-progress-steps li.active::before,.woocommerce-importer-wrapper .wc-progress-steps li.active::before,.woocommerce-progress-form-wrapper .wc-progress-steps li.active::before{border-color:#a16696}.woocommerce-exporter-wrapper .wc-progress-steps li.done,.woocommerce-importer-wrapper .wc-progress-steps li.done,.woocommerce-progress-form-wrapper .wc-progress-steps li.done{border-color:#a16696;color:#a16696}.woocommerce-exporter-wrapper .wc-progress-steps li.done::before,.woocommerce-importer-wrapper .wc-progress-steps li.done::before,.woocommerce-progress-form-wrapper .wc-progress-steps li.done::before{border-color:#a16696;background:#a16696}.woocommerce-exporter-wrapper .button,.woocommerce-importer-wrapper .button,.woocommerce-progress-form-wrapper .button{font-size:1.25em;padding:.5em 1em!important;line-height:1.5em!important;margin-right:.5em;margin-bottom:2px;height:auto!important;border-radius:4px;background-color:#bb77ae;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597;text-shadow:0 -1px 1px #a36597,1px 0 1px #a36597,0 1px 1px #a36597,-1px 0 1px #a36597;margin:0;opacity:1}.woocommerce-exporter-wrapper .button:active,.woocommerce-exporter-wrapper .button:focus,.woocommerce-exporter-wrapper .button:hover,.woocommerce-importer-wrapper .button:active,.woocommerce-importer-wrapper .button:focus,.woocommerce-importer-wrapper .button:hover,.woocommerce-progress-form-wrapper .button:active,.woocommerce-progress-form-wrapper .button:focus,.woocommerce-progress-form-wrapper .button:hover{background:#a36597;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597}.woocommerce-exporter-wrapper .error .button,.woocommerce-importer-wrapper .error .button,.woocommerce-progress-form-wrapper .error .button{font-size:1em}.woocommerce-exporter-wrapper .wc-actions,.woocommerce-importer-wrapper .wc-actions,.woocommerce-progress-form-wrapper .wc-actions{overflow:hidden;border-top:1px solid #eee;margin:0;padding:23px 24px 24px;line-height:3em}.woocommerce-exporter-wrapper .wc-actions .button,.woocommerce-importer-wrapper .wc-actions .button,.woocommerce-progress-form-wrapper .wc-actions .button{float:right}.woocommerce-exporter-wrapper .wc-actions .woocommerce-importer-toggle-advanced-options,.woocommerce-importer-wrapper .wc-actions .woocommerce-importer-toggle-advanced-options,.woocommerce-progress-form-wrapper .wc-actions .woocommerce-importer-toggle-advanced-options{color:#999}.woocommerce-exporter-wrapper .wc-progress-form-content,.woocommerce-exporter-wrapper .woocommerce-exporter,.woocommerce-exporter-wrapper .woocommerce-importer,.woocommerce-importer-wrapper .wc-progress-form-content,.woocommerce-importer-wrapper .woocommerce-exporter,.woocommerce-importer-wrapper .woocommerce-importer,.woocommerce-progress-form-wrapper .wc-progress-form-content,.woocommerce-progress-form-wrapper .woocommerce-exporter,.woocommerce-progress-form-wrapper .woocommerce-importer{background:#fff;overflow:hidden;padding:0;margin:0 0 16px;box-shadow:0 1px 3px rgba(0,0,0,.13);color:#555;text-align:left}.woocommerce-exporter-wrapper .wc-progress-form-content header,.woocommerce-exporter-wrapper .woocommerce-exporter header,.woocommerce-exporter-wrapper .woocommerce-importer header,.woocommerce-importer-wrapper .wc-progress-form-content header,.woocommerce-importer-wrapper .woocommerce-exporter header,.woocommerce-importer-wrapper .woocommerce-importer header,.woocommerce-progress-form-wrapper .wc-progress-form-content header,.woocommerce-progress-form-wrapper .woocommerce-exporter header,.woocommerce-progress-form-wrapper .woocommerce-importer header{border-bottom:1px solid #eee;margin:0;padding:24px 24px 0}.woocommerce-exporter-wrapper .wc-progress-form-content section,.woocommerce-exporter-wrapper .woocommerce-exporter section,.woocommerce-exporter-wrapper .woocommerce-importer section,.woocommerce-importer-wrapper .wc-progress-form-content section,.woocommerce-importer-wrapper .woocommerce-exporter section,.woocommerce-importer-wrapper .woocommerce-importer section,.woocommerce-progress-form-wrapper .wc-progress-form-content section,.woocommerce-progress-form-wrapper .woocommerce-exporter section,.woocommerce-progress-form-wrapper .woocommerce-importer section{padding:24px 24px 0}.woocommerce-exporter-wrapper .wc-progress-form-content h2,.woocommerce-exporter-wrapper .woocommerce-exporter h2,.woocommerce-exporter-wrapper .woocommerce-importer h2,.woocommerce-importer-wrapper .wc-progress-form-content h2,.woocommerce-importer-wrapper .woocommerce-exporter h2,.woocommerce-importer-wrapper .woocommerce-importer h2,.woocommerce-progress-form-wrapper .wc-progress-form-content h2,.woocommerce-progress-form-wrapper .woocommerce-exporter h2,.woocommerce-progress-form-wrapper .woocommerce-importer h2{margin:0 0 24px;color:#555;font-size:24px;font-weight:400;line-height:1em}.woocommerce-exporter-wrapper .wc-progress-form-content p,.woocommerce-exporter-wrapper .woocommerce-exporter p,.woocommerce-exporter-wrapper .woocommerce-importer p,.woocommerce-importer-wrapper .wc-progress-form-content p,.woocommerce-importer-wrapper .woocommerce-exporter p,.woocommerce-importer-wrapper .woocommerce-importer p,.woocommerce-progress-form-wrapper .wc-progress-form-content p,.woocommerce-progress-form-wrapper .woocommerce-exporter p,.woocommerce-progress-form-wrapper .woocommerce-importer p{font-size:1em;line-height:1.75em;font-size:16px;color:#555;margin:0 0 24px}.woocommerce-exporter-wrapper .wc-progress-form-content .form-row,.woocommerce-exporter-wrapper .woocommerce-exporter .form-row,.woocommerce-exporter-wrapper .woocommerce-importer .form-row,.woocommerce-importer-wrapper .wc-progress-form-content .form-row,.woocommerce-importer-wrapper .woocommerce-exporter .form-row,.woocommerce-importer-wrapper .woocommerce-importer .form-row,.woocommerce-progress-form-wrapper .wc-progress-form-content .form-row,.woocommerce-progress-form-wrapper .woocommerce-exporter .form-row,.woocommerce-progress-form-wrapper .woocommerce-importer .form-row{margin-top:24px}.woocommerce-exporter-wrapper .wc-progress-form-content .spinner,.woocommerce-exporter-wrapper .woocommerce-exporter .spinner,.woocommerce-exporter-wrapper .woocommerce-importer .spinner,.woocommerce-importer-wrapper .wc-progress-form-content .spinner,.woocommerce-importer-wrapper .woocommerce-exporter .spinner,.woocommerce-importer-wrapper .woocommerce-importer .spinner,.woocommerce-progress-form-wrapper .wc-progress-form-content .spinner,.woocommerce-progress-form-wrapper .woocommerce-exporter .spinner,.woocommerce-progress-form-wrapper .woocommerce-importer .spinner{display:none}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th{vertical-align:top;line-height:1.75em;padding:0 0 24px 0}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td label,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th label,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td label,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th label,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td label,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th label,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td label,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th label,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td label,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th label,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td label,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th label,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td label,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th label,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td label,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th label,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td label,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th label,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td label,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th label,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td label,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th label,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td label,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th label,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td label,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th label,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td label,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th label,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td label,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th label,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td label,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th label,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td label,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th label,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td label,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th label{color:#555;font-weight:400}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=checkbox],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=checkbox],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=checkbox],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=checkbox],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=checkbox],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=checkbox],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=checkbox],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=checkbox],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=checkbox],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=checkbox],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=checkbox],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=checkbox],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=checkbox],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=checkbox],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=checkbox]{margin:0 4px 0 0;padding:7px}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=number],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=text],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=number],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=text],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=number],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=text],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=number],.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=text],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=number],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=text],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=number],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=text],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=number],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=text],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=number],.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=text],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=number],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=text],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=number],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=text],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=number],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=text],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=number],.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=text],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=number],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=text],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=number],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=text],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=number],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=text],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=number],.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=text],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=number],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=text],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=number],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=text],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=number],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=text],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=number],.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=text],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=number],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=text],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=number],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=text],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=number],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=text],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=number],.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=text],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=number],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td input[type=text],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=number],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th input[type=text],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=number],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td input[type=text],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=number],.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th input[type=text],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=number],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td input[type=text],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=number],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th input[type=text],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=number],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td input[type=text],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=number],.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th input[type=text],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=number],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td input[type=text],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=number],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th input[type=text],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=number],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td input[type=text],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=number],.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th input[type=text]{padding:7px;height:auto;margin:0}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper{border:1px solid #ddd;box-shadow:inset 0 1px 2px rgba(0,0,0,.07);background-color:#fff;color:#32373c;outline:0;line-height:1;display:block}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper code,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper code{background:0 0;font-size:smaller;padding:0;margin:0;color:#999;padding:7px 0 0 7px;display:inline-block}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options td .woocommerce-importer-file-url-field-wrapper input,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th .woocommerce-importer-file-url-field-wrapper input{font-family:Consolas,Monaco,monospace;border:0;margin:0;outline:0;box-shadow:none;display:inline-block;min-width:100%}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-options th,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-options th,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-exporter-options th,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-options th{width:35%;padding-right:20px}.woocommerce-exporter-wrapper .wc-progress-form-content progress,.woocommerce-exporter-wrapper .woocommerce-exporter progress,.woocommerce-exporter-wrapper .woocommerce-importer progress,.woocommerce-importer-wrapper .wc-progress-form-content progress,.woocommerce-importer-wrapper .woocommerce-exporter progress,.woocommerce-importer-wrapper .woocommerce-importer progress,.woocommerce-progress-form-wrapper .wc-progress-form-content progress,.woocommerce-progress-form-wrapper .woocommerce-exporter progress,.woocommerce-progress-form-wrapper .woocommerce-importer progress{width:100%;height:42px;margin:0 auto 24px;display:block;-webkit-appearance:none;border:none;display:none;background:#f5f5f5;border:2px solid #eee;border-radius:4px;padding:0;box-shadow:0 1px 0 0 rgba(255,255,255,.2)}.woocommerce-exporter-wrapper .wc-progress-form-content progress::-webkit-progress-bar,.woocommerce-exporter-wrapper .woocommerce-exporter progress::-webkit-progress-bar,.woocommerce-exporter-wrapper .woocommerce-importer progress::-webkit-progress-bar,.woocommerce-importer-wrapper .wc-progress-form-content progress::-webkit-progress-bar,.woocommerce-importer-wrapper .woocommerce-exporter progress::-webkit-progress-bar,.woocommerce-importer-wrapper .woocommerce-importer progress::-webkit-progress-bar,.woocommerce-progress-form-wrapper .wc-progress-form-content progress::-webkit-progress-bar,.woocommerce-progress-form-wrapper .woocommerce-exporter progress::-webkit-progress-bar,.woocommerce-progress-form-wrapper .woocommerce-importer progress::-webkit-progress-bar{background:transparent none;border:0;border-radius:4px;padding:0;box-shadow:none}.woocommerce-exporter-wrapper .wc-progress-form-content progress::-webkit-progress-value,.woocommerce-exporter-wrapper .woocommerce-exporter progress::-webkit-progress-value,.woocommerce-exporter-wrapper .woocommerce-importer progress::-webkit-progress-value,.woocommerce-importer-wrapper .wc-progress-form-content progress::-webkit-progress-value,.woocommerce-importer-wrapper .woocommerce-exporter progress::-webkit-progress-value,.woocommerce-importer-wrapper .woocommerce-importer progress::-webkit-progress-value,.woocommerce-progress-form-wrapper .wc-progress-form-content progress::-webkit-progress-value,.woocommerce-progress-form-wrapper .woocommerce-exporter progress::-webkit-progress-value,.woocommerce-progress-form-wrapper .woocommerce-importer progress::-webkit-progress-value{border-radius:3px;box-shadow:inset 0 1px 1px 0 rgba(255,255,255,.4);background:#a46497;background:-webkit-gradient(linear,left top,left bottom,from(#a46497),to(#66405f)),#a46497;background:linear-gradient(to bottom,#a46497,#66405f),#a46497;-webkit-transition:width 1s ease;transition:width 1s ease}.woocommerce-exporter-wrapper .wc-progress-form-content progress::-moz-progress-bar,.woocommerce-exporter-wrapper .woocommerce-exporter progress::-moz-progress-bar,.woocommerce-exporter-wrapper .woocommerce-importer progress::-moz-progress-bar,.woocommerce-importer-wrapper .wc-progress-form-content progress::-moz-progress-bar,.woocommerce-importer-wrapper .woocommerce-exporter progress::-moz-progress-bar,.woocommerce-importer-wrapper .woocommerce-importer progress::-moz-progress-bar,.woocommerce-progress-form-wrapper .wc-progress-form-content progress::-moz-progress-bar,.woocommerce-progress-form-wrapper .woocommerce-exporter progress::-moz-progress-bar,.woocommerce-progress-form-wrapper .woocommerce-importer progress::-moz-progress-bar{border-radius:3px;box-shadow:inset 0 1px 1px 0 rgba(255,255,255,.4);background:#a46497;background:linear-gradient(to bottom,#a46497,#66405f),#a46497;-moz-transition:width 1s ease;transition:width 1s ease}.woocommerce-exporter-wrapper .wc-progress-form-content progress::-ms-fill,.woocommerce-exporter-wrapper .woocommerce-exporter progress::-ms-fill,.woocommerce-exporter-wrapper .woocommerce-importer progress::-ms-fill,.woocommerce-importer-wrapper .wc-progress-form-content progress::-ms-fill,.woocommerce-importer-wrapper .woocommerce-exporter progress::-ms-fill,.woocommerce-importer-wrapper .woocommerce-importer progress::-ms-fill,.woocommerce-progress-form-wrapper .wc-progress-form-content progress::-ms-fill,.woocommerce-progress-form-wrapper .woocommerce-exporter progress::-ms-fill,.woocommerce-progress-form-wrapper .woocommerce-importer progress::-ms-fill{border-radius:3px;box-shadow:inset 0 1px 1px 0 rgba(255,255,255,.4);background:#a46497;background:linear-gradient(to bottom,#a46497,#66405f),#a46497;-ms-transition:width 1s ease;transition:width 1s ease}.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .spinner,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-importer__importing .spinner,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .spinner,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-importer__importing .spinner,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-exporter__exporting .spinner,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-importer__importing .spinner,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .spinner,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-importer__importing .spinner,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .spinner,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-importer__importing .spinner,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-exporter__exporting .spinner,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-importer__importing .spinner,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .spinner,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-importer__importing .spinner,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .spinner,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-importer__importing .spinner,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-exporter__exporting .spinner,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-importer__importing .spinner{display:block}.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-exporter__exporting progress,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-importer__importing progress,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-exporter__exporting progress,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-importer__importing progress,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-exporter__exporting progress,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-importer__importing progress,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-exporter__exporting progress,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-importer__importing progress,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-exporter__exporting progress,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-importer__importing progress,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-exporter__exporting progress,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-importer__importing progress,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-exporter__exporting progress,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-importer__importing progress,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-exporter__exporting progress,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-importer__importing progress,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-exporter__exporting progress,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-importer__importing progress{display:block}.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .wc-actions,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-importer__importing .wc-actions,.woocommerce-exporter-wrapper .wc-progress-form-content.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .wc-actions,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-importer__importing .wc-actions,.woocommerce-exporter-wrapper .woocommerce-exporter.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-exporter__exporting .wc-actions,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-importer__importing .wc-actions,.woocommerce-exporter-wrapper .woocommerce-importer.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .wc-actions,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-importer__importing .wc-actions,.woocommerce-importer-wrapper .wc-progress-form-content.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .wc-actions,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-importer__importing .wc-actions,.woocommerce-importer-wrapper .woocommerce-exporter.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-exporter__exporting .wc-actions,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-importer__importing .wc-actions,.woocommerce-importer-wrapper .woocommerce-importer.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .wc-actions,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-importer__importing .wc-actions,.woocommerce-progress-form-wrapper .wc-progress-form-content.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .wc-actions,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-importer__importing .wc-actions,.woocommerce-progress-form-wrapper .woocommerce-exporter.woocommerce-importer__importing .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-exporter__exporting .wc-actions,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-exporter__exporting .woocommerce-exporter-options,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-importer__importing .wc-actions,.woocommerce-progress-form-wrapper .woocommerce-importer.woocommerce-importer__importing .woocommerce-exporter-options{display:none}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table-wrapper,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table-wrapper,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table-wrapper,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table-wrapper,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table-wrapper,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table-wrapper,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table-wrapper,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table-wrapper,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table-wrapper{padding:0}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table{margin:0;border:0;box-shadow:none;width:100%;table-layout:fixed}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table td,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table td,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table td,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table td,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table td,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table td,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table td,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table td,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table td,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table td,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table td,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table td,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table td,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table td,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table td,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table td,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table td,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table td,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th{border:0;padding:12px;vertical-align:middle;word-wrap:break-word}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table td select,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th select,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table td select,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th select,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table td select,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th select,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table td select,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th select,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table td select,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th select,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table td select,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th select,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table td select,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th select,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table td select,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th select,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table td select,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th select,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table td select,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th select,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table td select,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th select,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table td select,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th select,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table td select,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th select,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table td select,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th select,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table td select,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th select,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table td select,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th select,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table td select,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th select,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table td select,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th select{width:100%}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table tbody tr:nth-child(odd) th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) td,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table tbody tr:nth-child(odd) th{background:#fbfbfb}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th{font-weight:700}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table td:first-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th:first-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table td:first-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th:first-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table td:first-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th:first-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table td:first-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th:first-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table td:first-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th:first-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table td:first-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th:first-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table td:first-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th:first-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table td:first-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th:first-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table td:first-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th:first-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table td:first-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th:first-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table td:first-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th:first-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table td:first-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th:first-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table td:first-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th:first-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table td:first-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th:first-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table td:first-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th:first-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table td:first-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th:first-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table td:first-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th:first-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table td:first-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th:first-child{padding-left:24px}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table td:last-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table th:last-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table td:last-child,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table th:last-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table td:last-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table th:last-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table td:last-child,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table th:last-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table td:last-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table th:last-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table td:last-child,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table th:last-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table td:last-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table th:last-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table td:last-child,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table th:last-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table td:last-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table th:last-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table td:last-child,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table th:last-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table td:last-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table th:last-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table td:last-child,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table th:last-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table td:last-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table th:last-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table td:last-child,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table th:last-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table td:last-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table th:last-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table td:last-child,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table th:last-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table td:last-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table th:last-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table td:last-child,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table th:last-child{padding-right:24px}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name{width:50%}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description{color:#999;margin-top:4px;display:block}.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-exporter-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-importer-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .wc-progress-form-content .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .woocommerce-exporter .wc-importer-mapping-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-error-log-table .wc-importer-mapping-table-name .description code,.woocommerce-progress-form-wrapper .woocommerce-importer .wc-importer-mapping-table .wc-importer-mapping-table-name .description code{background:0 0;padding:0;white-space:pre-line;word-wrap:break-word;word-break:break-all}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-done,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-done,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-done,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-done,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-done,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-done,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-done,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-done,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-done{text-align:center;padding:48px 24px;font-size:1.5em;line-height:1.75em}.woocommerce-exporter-wrapper .wc-progress-form-content .woocommerce-importer-done::before,.woocommerce-exporter-wrapper .woocommerce-exporter .woocommerce-importer-done::before,.woocommerce-exporter-wrapper .woocommerce-importer .woocommerce-importer-done::before,.woocommerce-importer-wrapper .wc-progress-form-content .woocommerce-importer-done::before,.woocommerce-importer-wrapper .woocommerce-exporter .woocommerce-importer-done::before,.woocommerce-importer-wrapper .woocommerce-importer .woocommerce-importer-done::before,.woocommerce-progress-form-wrapper .wc-progress-form-content .woocommerce-importer-done::before,.woocommerce-progress-form-wrapper .woocommerce-exporter .woocommerce-importer-done::before,.woocommerce-progress-form-wrapper .woocommerce-importer .woocommerce-importer-done::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";color:#a16696;position:static;font-size:100px;display:block;float:none;margin:0 0 24px}.wc-pointer .wc-pointer-buttons .close{float:left;margin:6px 0 0 15px}.wc-quick-edit-warning{color:#8b0000;font-weight:700}@media screen and (min-width:600px){.wc-addons-wrap .marketplace-header{padding-left:84px}.wc-addons-wrap .storefront h2{margin-top:0}.wc-addons-wrap .storefront img{float:left;margin:0 16px 0 auto;width:278px}} \ No newline at end of file diff --git a/assets/css/admin.scss b/assets/css/admin.scss new file mode 100644 index 0000000..c31e4ef --- /dev/null +++ b/assets/css/admin.scss @@ -0,0 +1,7703 @@ +/** + * admin.scss + * General WooCommerce admin styles. Settings, product data tabs, reports, etc. + */ + +/** + * Imports + */ +@import "mixins"; +@import "variables"; +@import "animation"; +@import "fonts"; + +/** + * Styling begins + */ +.blockUI.blockOverlay { + + @include loader(); +} + +.wc-addons-wrap { + + .marketplace-header { + background-image: url(../images/marketplace-header-bg@2x.png); + background-position: right; + background-size: cover; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 216px; + padding: 24px 16px; + width: 100%; + + &__title { + color: #fff; + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: 1.15; + margin-bottom: 8px; + padding: 0; + } + + &__description { + color: #fff; + font-size: 16px; + line-height: 24px; + margin-bottom: 24px; + margin-top: 0; + } + + &__search-form { + clear: both; + display: block; + max-width: 318px; + position: relative; + + input { + border: 1px solid #ddd; + box-shadow: none; + font-size: 13px; + height: 48px; + padding-left: 16px; + padding-right: 50px; + width: 100%; + margin: 0; + } + + button { + background: none; + border: none; + cursor: pointer; + height: 48px; + position: absolute; + right: 0; + width: 53px; + } + } + } + + .top-bar { + background: #fff; + box-shadow: inset 0 -1px 0 #ccc; + display: block; + height: 60px; + margin: 0 0 16px; + + @media only screen and ( min-width: 768px ) { + margin-bottom: 24px; + } + + .current-section-dropdown { + position: relative; + width: 100%; + + @media only screen and ( min-width: 600px ) { + margin-left: 70px; + width: 288px; + } + } + + .current-section-name { + cursor: pointer; + font-weight: 600; + font-size: 14px; + line-height: 20px; + padding: 20px 16px; + position: relative; + } + + .current-section-name::after { + background-image: url(../images/icons/gridicons-chevron-down.svg); + background-size: contain; + content: ""; + display: block; + height: 20px; + position: absolute; + right: 20px; + top: 20px; + width: 20px; + } + + ul { + background: #fff; + border-radius: 2px; + display: none; + flex-direction: column; + justify-content: left; + left: 0; + margin: 0; + padding: 14px 0; + position: absolute; + top: 50px; + width: 100%; + z-index: 10; + + @media only screen and ( min-width: 600px ) { + border: 1px solid #1e1e1e; + } + + @media only screen and ( min-width: 1100px ) { + justify-content: center; + } + + li { + font-size: 13px; + line-height: 16px; + margin: 0; + } + + a, + a:visited, + a:hover, + a:focus { + border: none; + box-shadow: none; + box-sizing: border-box; + color: #1e1e1e; + display: inline-block; + text-decoration: none; + outline: none; + padding: 14px 18px; + position: relative; + width: 100%; + + @media only screen and ( min-width: 600px ) { + padding: 10px 18px; + } + } + + a.current::after { + background-image: url(../images/icons/gridicons-checkmark.svg); + content: ""; + display: block; + height: 20px; + position: absolute; + right: 20px; + top: 7px; + width: 20px; + } + } + + .current-section-dropdown.is-open { + + ul { + display: flex; + } + + .current-section-name::after { + transform: rotate(0.5turn); + } + } + } + + .update-plugins .update-count { + background-color: #d54e21; + border-radius: 10px; + color: #fff; + display: inline-block; + font-size: 9px; + font-weight: 600; + line-height: 17px; + margin: 1px 0 0 2px; + padding: 0 6px; + vertical-align: text-top; + } + + /** + * Marketplace related variables + */ + $font-sf-pro-text: + helveticaneue-light, + "Helvetica Neue Light", + "Helvetica Neue", + sans-serif; + + $font-sf-pro-display: sans-serif; + + h1.search-form-title { + clear: left; + font-size: 20px; + font-family: $font-sf-pro-display; + line-height: 1.2; + margin: 48px 0 16px; + padding: 0; + } + + .addons-featured { + margin: 0; + } + + ul.subsubsub.subsubsub { + margin: -2px 0 12px; + } + + .subsubsub li::after { + content: "|"; + } + + .subsubsub li:last-child::after { + content: ""; + } + + .addons-button { + border-radius: 3px; + cursor: pointer; + display: block; + height: 37px; + line-height: 37px; + margin-top: 16px; + text-align: center; + text-decoration: none; + width: 124px; + } + + .addons-wcs-banner-block { + align-items: center; + background: #fff; + border: 1px solid #ddd; + display: flex; + margin: 0 0 1em 0; + padding: 2em 2em 1em; + } + + .addons-wcs-banner-block-image { + background: #f7f7f7; + border: 1px solid #e6e6e6; + margin-right: 2em; + padding: 4em; + max-width: 200px; + + .addons-img { + max-height: 86px; + max-width: 97px; + } + + &.is-full-image { + padding: 0; + background: none; + border: none; + + .addons-img { + max-height: 100%; + max-width: 100%; + } + } + } + + .addons-shipping-methods .addons-wcs-banner-block { + margin-left: 0; + margin-right: 0; + margin-top: 1em; + } + + .addons-wcs-banner-block-content { + display: flex; + flex-direction: column; + justify-content: space-around; + align-self: stretch; + padding: 1em 0; + + h1 { + padding-bottom: 0; + } + + p { + margin-bottom: 0; + } + + .wcs-logos-container { + display: flex; + align-items: center; + flex-direction: row; + justify-content: center; + + @media screen and (min-width: 500px) { + justify-content: left; + } + + li { + margin-right: 8px; + + &:last-child { + margin-right: 0; + } + } + } + + .wcs-service-logo { + max-width: 45px; + } + } + + .addons-column { + flex: 1; + width: 50%; + padding: 0 0.5em; + } + + .addons-column:nth-child(2) { + margin-right: 0; + } + + .addons-small-dark-items { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + } + + .addons-small-dark-item { + margin: 0 0 20px; + } + + .addons-small-dark-item-icon img { + height: 30px; + } + + .addons-small-dark-item a { + margin: 28px auto 0; + } + + .addons-button-solid { + background-color: #674399; + color: #fff; + } + + .addons-button-promoted { + float: right; + width: auto; + padding: 0 20px; + margin-top: 0; + } + + .addons-button-promoted:hover { + opacity: 0.8; + } + + .addons-button-expandable { + display: inline-block; + padding: 0 16px; + width: auto; + } + + .addons-button-solid:hover { + color: #fff; + opacity: 0.8; + } + + .addons-button-outline-green { + border: 1px solid #73ae39; + color: #73ae39; + } + + .addons-button-outline-green:hover { + color: #73ae39; + opacity: 0.8; + } + + .addons-button-outline-purple { + border: 1px solid #674399; + color: #674399; + } + + .addons-button-outline-purple:hover { + color: #674399; + opacity: 0.8; + } + + .addons-button-outline-white { + border: 1px solid #fff; + color: #fff; + } + + .addons-button-outline-white:hover { + color: #fff; + opacity: 0.8; + } + + .addons-button-installed { + background: #e6e6e6; + color: #3c3c3c; + } + + .addons-button-installed:hover { + color: #3c3c3c; + opacity: 0.8; + } + + @media only screen and (max-width: 400px) { + + .addons-featured { + margin: -1% -5%; + } + + .addons-button { + width: 100%; + } + + .addons-small-dark-item { + width: 100%; + } + } + + .marketplace-content-wrapper { + font-family: $font-sf-pro-text; + margin: 0 auto; + max-width: 1032px; + width: 100%; + } + + .addon-product-group-title { + font-family: $font-sf-pro-display; + letter-spacing: 0.38px; + } + + .addon-product-group-description-container { + align-items: center; + display: flex; + flex-direction: row; + font-size: 14px; + justify-content: space-between; + line-height: 20px; + + .addon-product-group-see-more, + .addon-product-group-see-more:visited { + color: #007cba; /* Primary / Blue */ + display: block; + font-size: 13px; + text-decoration: none; + } + } + + .products { + display: flex; + flex-flow: row; + flex-wrap: wrap; + font-weight: normal; + justify-content: space-between; + margin: 0; + max-width: 1032px; + overflow: hidden; + + .product.addons-product-banner, + .product.addons-buttons-banner { + max-width: calc(100% - 2px); + } + + @media screen and (min-width: 960px) { + // Adjust heading titles font for three-column product groups + &.addons-products-three-column li.product { + max-width: calc(33.33% - 12px); + + h2, + h3 { + font-size: 16px; + } + } + } + + li { + background: #fff; + border: 1px solid #dcdcde; + border-radius: 2px; + display: flex; + flex: 1 0 auto; + flex-direction: column; + justify-content: space-between; + margin: 12px 0; + max-width: calc(50% - 12px); + min-width: 280px; + min-height: 220px; + overflow: hidden; + padding: 0; + vertical-align: top; + + &.addons-full-width { + max-width: 100%; + } + + @media only screen and ( max-width: 768px ) { + max-width: none; + width: 100%; + } + + a { + text-decoration: none; + } + + .product-details { + padding: 24px; + position: relative; + + /* Display an image (product's icon) top right */ + .product-img-wrap { + display: block; + margin-left: 24px; + position: absolute; + right: 24px; + top: 24px; + + img { + border-radius: 3px; + display: block; + margin: 0; + max-width: 48px; + max-height: 48px; + } + } + + /* Align aproduct-related banner image vertically centered */ + &.addon-product-banner-details { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-between; + + .product-img-wrap { + position: unset; + + img { + max-width: 150px; + max-height: 150px; + } + } + } + + h2, + h3 { + color: #007cba; + font-size: 20px; + font-weight: 400; + letter-spacing: -0.32px; + line-height: 28px; + margin: 0 !important; + // Don't cover a product icon + max-width: calc(100% - 48px); + } + + .addons-buttons-banner-details h2 { + color: #1d2327; // Gray / Gray 90 + } + + &.featured, + &.promoted { + + .label { + align-items: center; + border-radius: 2px; + background: #dcdcde; + display: flex; + flex-direction: row; + height: 20px; + justify-content: flex-end; + margin-bottom: 8px; + max-width: 52px; + padding: 3px 12px; + top: 28px; + right: 24px; + text-align: center; + + &.promoted { + float: right; + max-width: 58px; + } + } + + h2 { + color: #2c3338; + } + } + + p { + color: #2c3338; + font-size: 14px; + line-height: 20px; + margin: 14px 64px 0 0; + width: 100%; + } + + .addons-buttons-banner-details p { + font-size: 14px; + margin-bottom: 14px; + max-width: none; + } + + .product-developed-by { + color: #50575e; /* Gray 60 */ + font-size: 12px; + line-height: 20px; + margin-top: 4px; + + .product-vendor-link { + color: #50575e; /* Gray 60 */ + } + } + + .product-developed-by { + color: #50575e; // Gray 60 + font-size: 12px; + font-family: sans-serif; + line-height: 20px; + margin-top: 4px; + + .product-vendor-link { + color: #50575e; // Gray 60 + } + } + } + + + .product-footer { + align-items: center; + border-top: 1px solid #dcdcde; + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 24px; + + .price { + font-size: 16px; + color: #1d2327; + } + + .price-suffix { + color: #646970; // Gray 50 + } + + .product-reviews-block { + display: flex; + flex-direction: row; + margin-top: 4px; + + .product-rating-star { + background-repeat: no-repeat; + background-size: contain; + height: 16px; + margin: 4px 4px 4px 0; + width: 17px; + + &__fill { + background-image: url(../images/icons/star-golden.svg); + } + + &__half-fill { + background-image: url(../images/icons/star-half-filled.svg); + } + + &__no-fill { + background-image: url(../images/icons/star-gray.svg); + } + } + + .product-reviews-count { + color: #646970; // Gray 50 + font-size: 12px; + font-family: sans-serif; + line-height: 24px; + letter-spacing: -0.154px; + margin-left: 4px; + } + } + + .button { + background-color: #fff; + border-color: #007cba; + color: #007cba; + float: right; + font-size: 13px; + height: 36px; + line-height: 30px; + padding: 2px 14px; + } + } + } + + .product-footer-promoted { + align-items: flex-end; + display: flex; + justify-content: space-between; + padding: 24px; + + .icon img { + border-radius: 4px; + width: 80px; + } + } + + .addons-buttons-banner { + display: flex; + flex-direction: row; + + .addons-buttons-banner-image { + background-repeat: no-repeat; + background-size: cover; + height: 190px; + margin: 24px; + width: 200px; + } + + .addons-buttons-banner-details-container { + padding-left: 0; + width: calc(100% - 198px - 24px - 24px); + } + + .addons-buttons-banner-details-container { + display: flex; + flex-direction: column; + justify-content: space-between; + } + + .button.addons-buttons-banner-button, + .button.addons-buttons-banner-button:hover { + background: #fff; + border: 1.5px solid #624594; + color: #624594; + padding: 4px 12px; + margin-right: 16px; + + &.addons-buttons-banner-button-primary { + background-color: #624594; + color: #fff; + } + } + } + } + + .storefront { + max-width: 990px; + background: url(../images/storefront-bg.jpg) bottom right #f6f6f6; + border: 1px solid #ddd; + margin: 1em auto; + padding: 24px; + overflow: hidden; + zoom: 1; + + img { + display: block; + width: 100%; + max-width: 400px; + height: auto; + margin: 0 auto 16px; + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1); + } + + p:last-of-type { + margin-bottom: 0; + } + + p { + max-width: 750px; + } + } +} + +.no-touch, +.no-js { + + .wc-addons-wrap { + + .current-section-dropdown:hover { + + ul { + display: flex; + } + + .current-section-name::after { + transform: rotate(0.5turn); + } + } + } +} + +.wc-subscriptions-wrap { + max-width: 1200px; +} + +.woocommerce-page-wc-marketplace { + + .notice { + margin-left: 20px; + margin-right: 20px; + } + + &.woocommerce-page { + + .wrap { + margin-top: 32px; + } + } +} + +.woocommerce-page-wc-subscriptions { + + #wpbody-content { + + .screen-reader-text + .notice { + margin-top: 32px; + } + } +} + +.woocommerce-embed-page.woocommerce-page-wc-marketplace { + + #screen-meta-links { + position: absolute; + right: 0; + } +} + +.woocommerce-message, +.woocommerce-BlankState { + + a.button-primary, + button.button-primary { + background: #bb77ae; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + color: #fff; + text-shadow: + 0 -1px 1px #a36597, + 1px 0 1px #a36597, + 0 1px 1px #a36597, + -1px 0 1px #a36597; + display: inline-block; + + &:hover, + &:focus, + &:active { + background: #a36597; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + } + } +} + +.woocommerce-message { + position: relative; + overflow: hidden; + + &.updated { + border-left-color: #cc99c2 !important; + } + + a.skip, + a.docs { + text-decoration: none !important; + } + + a.woocommerce-message-close { + position: static; + float: right; + padding: 0 15px 10px 28px; + margin-top: -10px; + font-size: 13px; + line-height: 1.23076923; + text-decoration: none; + + &::before { + position: relative; + top: 18px; + left: -20px; + transition: all 0.1s ease-in-out; + } + } + + .twitter-share-button { + margin-top: -3px; + margin-left: 3px; + vertical-align: middle; + } +} + +#variable_product_options #message, +#variable_product_options .notice { + margin: 10px; +} + +#variable_product_options { + + .form-row select { + max-width: 100%; + } + + .toolbar-top { + + .button { + margin: 1px; + } + } +} + +#product_attributes { + + .toolbar-top { + + .button { + margin: 1px; + } + } +} + +.clear { + clear: both; +} + +.wrap.woocommerce div.updated, +.wrap.woocommerce div.error { + margin-top: 10px; +} + +mark.amount { + background: transparent none; + color: inherit; +} + +/** + * Help Tip + */ +.woocommerce-help-tip { + color: #666; + display: inline-block; + font-size: 1.1em; + font-style: normal; + height: 16px; + line-height: 16px; + position: relative; + vertical-align: middle; + width: 16px; + + &::after { + + @include icon_dashicons("\f223"); + cursor: help; + } +} + +.wc-wp-version-gte-53 { + + .woocommerce-help-tip { + font-size: 1.2em; + cursor: help; + } +} + +h2 .woocommerce-help-tip { + margin-top: -5px; + margin-left: 0.25em; +} + +table.wc_status_table { + margin-bottom: 1em; + + h2 { + font-size: 14px; + margin: 0; + } + + tr:nth-child(2n) { + + th, + td { + background: #fcfcfc; + } + } + + th { + font-weight: 700; + padding: 9px; + } + + td:first-child { + width: 33%; + } + + td.help { + width: 1em; + } + + td, + th { + font-size: 1.1em; + font-weight: normal; + + &.run-tool { + text-align: right; + } + + strong.name { + display: block; + margin-bottom: 0.5em; + } + + mark { + background: transparent none; + } + + mark.yes { + color: $green; + } + + mark.no { + color: #999; + } + + mark.error, + .red { + color: $red; + } + + ul { + margin: 0; + } + } + + .help_tip { + cursor: help; + } +} + +table.wc_status_table--tools { + + td, + th { + padding: 2em; + } +} + +.taxonomy-product_cat { + + .check-column .woocommerce-help-tip { + font-size: 1.5em; + margin: -3px 0 0 5px; + display: block; + position: absolute; + } +} + +#debug-report { + display: none; + margin: 10px 0; + padding: 0; + position: relative; + + textarea { + font-family: monospace; + width: 100%; + margin: 0; + height: 300px; + padding: 20px; + border-radius: 0; + resize: none; + font-size: 12px; + line-height: 20px; + outline: 0; + } +} + +/** + * DB log viewer + */ +.wp-list-table.logs { + + .log-level { + display: inline; + padding: 0.2em 0.6em 0.3em; + font-size: 80%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.2em; + + &:empty { + display: none; + } + } + + /** + * Add color to levels + * + * Descending severity: + * emergency, alert -> red + * critical, error -> orange + * warning, notice -> yellow + * info -> blue + * debug -> gree + */ + + .log-level--emergency, + .log-level--alert { + background-color: #ff4136; + } + + .log-level--critical, + .log-level--error { + background-color: #ff851b; + } + + .log-level--warning, + .log-level--notice { + color: #222; + background-color: #ffdc00; + } + + .log-level--info { + background-color: #0074d9; + } + + .log-level--debug { + background-color: #3d9970; + } + + // Adjust log table columns only when table is not collapsed + @media screen and (min-width: 783px) { + + .column-timestamp { + width: 18%; + } + + .column-level { + width: 14%; + } + + .column-source { + width: 15%; + } + } +} + +#log-viewer-select { + padding: 10px 0 8px; + line-height: 28px; + + h2 a { + vertical-align: middle; + } +} + +#log-viewer { + background: #fff; + border: 1px solid #e5e5e5; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + padding: 5px 20px; + + pre { + font-family: monospace; + white-space: pre-wrap; + word-wrap: break-word; + } +} + +.inline-edit-product.quick-edit-row { + + .inline-edit-col-center, + .inline-edit-col-right { + float: right !important; + } +} + +#woocommerce-fields.inline-edit-col { + clear: left; + + label.featured, + label.manage_stock { + margin-left: 10px; + } + + label.stock_status_field { + clear: both; + float: left; + } + + .dimensions div { + display: block; + margin: 0.2em 0; + + span.title { + display: block; + float: left; + width: 5em; + } + + span.input-text-wrap { + display: block; + margin-left: 5em; + } + } + + .text { + box-sizing: border-box; + width: 99%; + float: left; + margin: 1px 1% 1px 1px; + } + + .length, + .width, + .height { + width: 32.33%; + } + + .height { + margin-right: 0; + } +} + +#woocommerce-fields-bulk.inline-edit-col { + + label { + clear: left; + } + + .inline-edit-group { + + label { + clear: none; + width: 49%; + margin: 0.2em 0; + } + + &.dimensions label { + width: 75%; + max-width: 75%; + } + } + + .regular_price, + .sale_price, + .weight, + .stock, + .length { + box-sizing: border-box; + width: 100%; + margin-left: 4.4em; + } + + .length, + .width, + .height { + box-sizing: border-box; + width: 25%; + } +} + +.column-coupon_code { + line-height: 2.25em; +} + +ul.wc_coupon_list, +.column-coupon_code { + margin: 0; + overflow: hidden; + zoom: 1; + clear: both; +} + +ul.wc_coupon_list { + padding-bottom: 5px; + + li { + margin: 0; + + &.code { + display: inline-block; + position: relative; + padding: 0 0.5em; + background-color: #fff; + border: 1px solid #aaa; + -webkit-box-shadow: 0 1px 0 #dfdfdf; + box-shadow: 0 1px 0 #dfdfdf; + + border-radius: 4px; + margin-right: 5px; + margin-top: 5px; + + &.editable { + padding-right: 2em; + } + + .tips { + cursor: pointer; + + span { + color: #888; + + &:hover { + color: #000; + } + } + } + + .remove-coupon { + text-decoration: none; + color: #888; + position: absolute; + top: 7px; + right: 20px; + + /*rtl:raw: + left: 7px; + */ + + &::before { + + @include icon_dashicons("\f158"); + } + + &:hover::before { + color: $red; + } + } + } + } +} + +ul.wc_coupon_list_block { + margin: 0; + padding-bottom: 2px; + + li { + border-top: 1px solid #fff; + border-bottom: 1px solid #ccc; + line-height: 2.5em; + margin: 0; + padding: 0.5em 0; + } + + li:first-child { + border-top: 0; + padding-top: 0; + } + + li:last-child { + border-bottom: 0; + padding-bottom: 0; + } +} + +.button.wc-reload { + + @include ir(); + padding: 0; + height: 28px; + width: 28px !important; + display: inline-block; + + &::after { + + @include icon_dashicons("\f345"); + line-height: 28px; + } +} + +#woocommerce-order-data { + + .postbox-header, + .hndle, + .handlediv { + display: none; + } + + .inside { + display: block !important; + } +} + +#order_data { + padding: 23px 24px 12px; + + h2 { + margin: 0; + font-family: + "HelveticaNeue-Light", + "Helvetica Neue Light", + "Helvetica Neue", + sans-serif; + font-size: 21px; + font-weight: normal; + line-height: 1.2; + text-shadow: 1px 1px 1px white; + padding: 0; + } + + h3 { + font-size: 14px; + } + + h3, + h4 { + color: #333; + margin: 1.33em 0 0; + } + + p { + color: #777; + } + + p.order_number { + margin: 0; + font-family: + "HelveticaNeue-Light", + "Helvetica Neue Light", + "Helvetica Neue", + sans-serif; + font-weight: normal; + line-height: 1.6em; + font-size: 16px; + } + + .order_data_column_container { + clear: both; + + p._billing_email_field { + margin-top: 13px; + } + } + + .order_data_column { + width: 32%; + padding: 0 2% 0 0; + float: left; + + > h3 span { + display: block; + } + + &:last-child { + padding-right: 0; + } + + p { + padding: 0 !important; + } + + .address strong { + display: block; + } + + .form-field { + float: left; + clear: left; + width: 48%; + padding: 0; + margin: 9px 0 0; + + label { + display: block; + padding: 0 0 3px; + } + + input, + textarea { + width: 100%; + } + + select { + width: 100%; + max-width: 100%; + } + + .select2-container { + width: 100% !important; + } + + .date-picker { + width: 50%; + } + + .hour, + .minute { + width: 3.5em; + } + + small { + display: block; + margin: 5px 0 0; + color: #999; + } + } + + .form-field.last, + ._billing_last_name_field, + ._billing_address_2_field, + ._billing_postcode_field, + ._billing_state_field, + ._billing_phone_field, + ._shipping_last_name_field, + ._shipping_address_2_field, + ._shipping_postcode_field, + ._shipping_state_field { + float: right; + clear: right; + } + + .form-field-wide, + ._billing_company_field, + ._shipping_company_field, + ._transaction_id_field { + width: 100%; + clear: both; + + input, + textarea, + select, + .wc-enhanced-select, + .wc-category-search, + .wc-customer-search { + width: 100%; + } + } + + p.none_set { + color: #999; + } + + div.edit_address { + display: none; + zoom: 1; + padding-right: 1px; + + .select2-container { + + .select2-selection--single { + height: 32px; + + .select2-selection__rendered { + line-height: 32px; + } + } + } + } + + .wc-customer-user, + .wc-order-status { + + label a { + float: right; + margin-left: 8px; + } + } + + a.edit_address { + width: 14px; + height: 0; + padding: 14px 0 0; + margin: 0 0 0 6px; + overflow: hidden; + position: relative; + color: #999; + border: 0; + float: right; + + &:hover, + &:focus { + color: #000; + } + + &::after { + font-family: "WooCommerce"; + position: absolute; + top: 0; + left: 0; + text-align: center; + vertical-align: top; + line-height: 14px; + font-size: 14px; + font-weight: 400; + } + } + + a.edit_address::after { + font-family: "Dashicons"; + content: "\f464"; + } + + .billing-same-as-shipping, + .load_customer_shipping, + .load_customer_billing { + font-size: 13px; + display: inline-block; + font-weight: normal; + } + + .load_customer_shipping { + margin-right: 0.3em; + } + } +} + +.order_actions { + margin: 0; + overflow: hidden; + zoom: 1; + + li { + border-top: 1px solid #fff; + border-bottom: 1px solid #ddd; + padding: 6px 0; + margin: 0; + line-height: 1.6em; + float: left; + width: 50%; + text-align: center; + + a { + float: none; + text-align: center; + text-decoration: underline; + } + + &.wide { + width: auto; + float: none; + clear: both; + padding: 6px; + text-align: left; + overflow: hidden; + } + + #delete-action { + line-height: 25px; + vertical-align: middle; + text-align: left; + float: left; + } + + .save_order { + float: right; + } + + &#actions { + overflow: hidden; + + .button { + width: 24px; + box-sizing: border-box; + float: right; + } + + select { + width: 225px; + box-sizing: border-box; + float: left; + } + } + } +} + +#woocommerce-order-items { + + .inside { + margin: 0; + padding: 0; + background: #fefefe; + } + + .wc-order-data-row { + border-bottom: 1px solid #dfdfdf; + padding: 1.5em 2em; + background: #f8f8f8; + + @include clearfix(); + line-height: 2em; + text-align: right; + + p { + margin: 0; + line-height: 2em; + } + + .wc-used-coupons { + text-align: left; + + .tips { + display: inline-block; + } + } + } + + .wc-used-coupons { + float: left; + width: 50%; + } + + .wc-order-totals { + float: right; + width: 50%; + margin: 0; + padding: 0; + text-align: right; + + .amount { + font-weight: 700; + } + + .label { + vertical-align: top; + } + + .total { + font-size: 1em !important; + width: 10em; + margin: 0 0 0 0.5em; + box-sizing: border-box; + + input[type="text"] { + width: 96%; + float: right; + } + } + + .refunded-total { + color: $red; + } + + .label-highlight { + font-weight: bold; + } + } + + .refund-actions { + margin-top: 5px; + padding-top: 12px; + border-top: 1px solid #dfdfdf; + + .button { + float: right; + margin-left: 4px; + } + + .cancel-action { + float: left; + margin-left: 0; + } + } + + .add_meta { + margin-left: 0 !important; + } + + h3 small { + color: #999; + } + + .amount { + white-space: nowrap; + } + + .add-items { + + .description { + margin-right: 10px; + } + + .button { + float: left; + margin-right: 0.25em; + } + + .button-primary { + float: none; + margin-right: 0; + } + } +} + +#woocommerce-order-items { + + .inside { + display: block !important; + } + + .postbox-header, + .hndle, + .handlediv { + display: none; + } + + .woocommerce_order_items_wrapper { + margin: 0; + overflow-x: auto; + + table.woocommerce_order_items { + width: 100%; + background: #fff; + + thead th { + text-align: left; + padding: 1em; + font-weight: normal; + color: #999; + background: #f8f8f8; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &.sortable { + cursor: pointer; + } + + &:last-child { + padding-right: 2em; + } + + &:first-child { + padding-left: 2em; + } + + .wc-arrow { + float: right; + position: relative; + margin-right: -1em; + } + } + + tbody th, + td { + padding: 1.5em 1em 1em; + text-align: left; + line-height: 1.5em; + vertical-align: top; + border-bottom: 1px solid #f8f8f8; + + textarea { + width: 100%; + } + + select { + width: 50%; + } + + input, + textarea { + font-size: 14px; + padding: 4px; + color: #555; + } + + &:last-child { + padding-right: 2em; + } + + &:first-child { + padding-left: 2em; + } + } + + tbody tr:last-child td { + border-bottom: 1px solid #dfdfdf; + } + + tbody tr:first-child td { + border-top: 8px solid #f8f8f8; + } + + tbody#order_line_items tr:first-child td { + border-top: none; + } + + td.thumb { + text-align: left; + width: 38px; + padding-bottom: 1.5em; + + .wc-order-item-thumbnail { + width: 38px; + height: 38px; + border: 2px solid #e8e8e8; + background: #f8f8f8; + color: #ccc; + position: relative; + font-size: 21px; + display: block; + text-align: center; + + &::before { + + @include icon_dashicons("\f128"); + width: 38px; + line-height: 38px; + display: block; + } + + img { + width: 100%; + height: 100%; + margin: 0; + padding: 0; + position: relative; + } + } + } + + td.name { + + .wc-order-item-sku, + .wc-order-item-variation { + display: block; + margin-top: 0.5em; + font-size: 0.92em !important; + color: #888; + } + } + + .item { + min-width: 200px; + } + + .center, + .variation-id { + text-align: center; + } + + .cost, + .tax, + .quantity, + .line_cost, + .line_tax, + .tax_class, + .item_cost { + text-align: right; + + label { + white-space: nowrap; + color: #999; + font-size: 0.833em; + + input { + display: inline; + } + } + + input { + width: 70px; + vertical-align: middle; + text-align: right; + } + + select { + width: 85px; + height: 26px; + vertical-align: middle; + font-size: 1em; + } + + .split-input { + display: inline-block; + background: #fff; + border: 1px solid #ddd; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); + margin: 1px 0; + min-width: 80px; + overflow: hidden; + line-height: 1em; + text-align: right; + + div.input { + width: 100%; + box-sizing: border-box; + + label { + font-size: 0.75em; + padding: 4px 6px 0; + color: #555; + display: block; + } + + input { + width: 100%; + box-sizing: border-box; + border: 0; + box-shadow: none; + margin: 0; + padding: 0 6px 4px; + color: #555; + background: transparent; + + &::-webkit-input-placeholder { + color: #ddd; + } + } + } + + div.input:first-child { + border-bottom: 1px dashed #ddd; + background: #fff; + + label { + color: #ccc; + } + + input { + color: #ccc; + } + } + } + + .view { + white-space: nowrap; + } + + .edit { + text-align: left; + } + + small.times, + del, + .wc-order-item-taxes, + .wc-order-item-discount, + .wc-order-item-refund-fields { + font-size: 0.92em !important; + color: #888; + } + + .wc-order-item-taxes, + .wc-order-item-refund-fields { + margin: 0; + + label { + display: block; + } + } + + .wc-order-item-discount { + display: block; + margin-top: 0.5em; + } + + small.times { + margin-right: 0.25em; + } + } + + .quantity { + text-align: center; + + input { + text-align: center; + width: 50px; + } + } + + span.subtotal { + opacity: 0.5; + } + + td.tax_class, + th.tax_class { + text-align: left; + } + + .calculated { + border-color: #ae8ca2; + border-style: dotted; + } + + table.meta { + width: 100%; + } + + table.meta, + table.display_meta { + margin: 0.5em 0 0; + font-size: 0.92em !important; + color: #888; + + tr { + + th { + border: 0; + padding: 0 4px 0.5em 0; + line-height: 1.5em; + width: 20%; + } + + td { + padding: 0 4px 0.5em 0; + border: 0; + line-height: 1.5em; + + input { + width: 100%; + margin: 0; + position: relative; + border-bottom: 0; + box-shadow: none; + } + + textarea { + width: 100%; + height: 4em; + margin: 0; + box-shadow: none; + } + + input:focus + textarea { + border-top-color: #999; + } + + p { + margin: 0 0 0.5em; + line-height: 1.5em; + } + + p:last-child { + margin: 0; + } + } + } + } + + .refund_by { + border-bottom: 1px dotted #999; + } + + tr.fee .thumb div { + + @include ir(); + font-size: 1.5em; + line-height: 1em; + vertical-align: middle; + margin: 0 auto; + + &::before { + + @include icon("\e007"); + color: #ccc; + } + } + + tr.refund .thumb div { + + @include ir(); + font-size: 1.5em; + line-height: 1em; + vertical-align: middle; + margin: 0 auto; + + &::before { + + @include icon("\e014"); + color: #ccc; + } + } + + tr.shipping { + + .thumb div { + + @include ir(); + font-size: 1.5em; + line-height: 1em; + vertical-align: middle; + margin: 0 auto; + + &::before { + + @include icon("\e01a"); + color: #ccc; + } + } + + .shipping_method_name, + .shipping_method { + width: 100%; + margin: 0 0 0.5em; + } + } + + th.line_tax { + white-space: nowrap; + } + + th.line_tax, + td.line_tax { + + .delete-order-tax { + + @include ir(); + float: right; + font-size: 14px; + visibility: hidden; + margin: 3px -18px 0 0; + + &::before { + + @include icon_dashicons("\f153"); + color: #999; + } + + &:hover::before { + color: $red; + } + } + + &:hover .delete-order-tax { + visibility: visible; + } + } + + small.refunded { + display: block; + color: $red; + white-space: nowrap; + margin-top: 0.5em; + + &::before { + + @include icon_dashicons("\f171"); + position: relative; + top: auto; + left: auto; + margin: -1px 4px 0 0; + vertical-align: middle; + line-height: 1em; + } + } + } + } + + .wc-order-edit-line-item { + padding-left: 0; + } + + .wc-order-edit-line-item-actions { + width: 44px; + text-align: right; + padding-left: 0; + vertical-align: middle; + + a { + color: #ccc; + display: inline-block; + cursor: pointer; + padding: 0 0 0.5em; + margin: 0 0 0 12px; + vertical-align: middle; + text-decoration: none; + line-height: 16px; + width: 16px; + overflow: hidden; + + &::before { + margin: 0; + padding: 0; + font-size: 16px; + width: 16px; + height: 16px; + } + + &:hover { + + &::before { + color: #999; + } + } + + &:first-child { + margin-left: 0; + } + } + + .edit-order-item::before { + + @include icon_dashicons("\f464"); + position: relative; + } + + .delete-order-item, + .delete_refund { + + &::before { + + @include icon_dashicons("\f158"); + position: relative; + } + + &:hover::before { + color: $red; + } + } + } + + tbody tr .wc-order-edit-line-item-actions { + visibility: hidden; + } + + tbody tr:hover .wc-order-edit-line-item-actions { + visibility: visible; + } + + .wc-order-totals .wc-order-edit-line-item-actions { + width: 1.5em; + visibility: visible !important; + + a { + padding: 0; + } + } +} + +#woocommerce-order-downloads { + + .buttons { + float: left; + padding: 0; + margin: 0; + vertical-align: top; + + .add_item_id, + .select2-container { + width: 400px !important; + margin-right: 9px; + vertical-align: top; + float: left; + } + + button { + margin: 2px 0 0; + } + } + + h3 small { + color: #999; + } +} + +#poststuff #woocommerce-order-actions .inside { + margin: 0; + padding: 0; + + ul.order_actions li { + padding: 6px 10px; + box-sizing: border-box; + + &:last-child { + border-bottom: 0; + } + } + + button { + margin: 1px; + } +} + +#poststuff #woocommerce-order-notes .inside { + margin: 0; + padding: 0; + + ul.order_notes li { + padding: 0 10px; + } + + button { + margin: 1px; + vertical-align: top; + } +} + +#woocommerce_customers { + + p.search-box { + margin: 6px 0 4px; + float: left; + } + + .tablenav { + float: right; + clear: none; + } +} + +.widefat { + + &.customers td { + vertical-align: middle; + padding: 4px 7px; + } + + .column-order_title { + width: 15%; + + time { + display: block; + color: #999; + margin: 3px 0; + } + } + + .column-orders, + .column-paying, + .column-spent { + text-align: center; + width: 8%; + } + + .column-last_order { + width: 11%; + } + + .column-wc_actions { + width: 110px; + + a.button { + + @include ir(); + display: inline-block; + margin: 2px 4px 2px 0; + padding: 0 !important; + height: 2em !important; + width: 2em; + overflow: hidden; + vertical-align: middle; + + &::after { + font-family: "Dashicons"; + speak: never; + font-weight: normal; + font-variant: normal; + text-transform: none; + margin: 0; + text-indent: 0; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + text-align: center; + line-height: 1.85; + } + + img { + display: block; + width: 12px; + height: auto; + } + } + + a.edit::after { + content: "\f464"; + } + + a.link::after { + font-family: "WooCommerce"; + content: "\e00d"; + } + + a.view::after { + content: "\f177"; + } + + a.refresh::after { + font-family: "WooCommerce"; + content: "\e031"; + } + + a.processing::after { + font-family: "WooCommerce"; + content: "\e00f"; + } + + a.complete::after { + content: "\f147"; + } + } + + small.meta { + display: block; + color: #999; + font-size: inherit; + margin: 3px 0; + } +} + +.wc-wp-version-gte-53 { + + .widefat { + + .column-wc_actions { + + a.button { + + &::after { + margin-top: 2px; + } + } + } + } +} + +.post-type-shop_order { + + .tablenav .one-page .displaying-num { + display: none; + } + + .tablenav { + + .select2-selection--single { + height: 32px; + + .select2-selection__rendered { + line-height: 29px; + } + + .select2-selection__arrow { + height: 30px; + } + } + } + + .wp-list-table { + margin-top: 1em; + + thead, + tfoot { + + th { + padding: 0.75em 1em; + } + + th.sortable a, + th.sorted a { + padding: 0; + } + + th:first-child { + padding-left: 2em; + } + + th:last-child { + padding-right: 2em; + } + } + + tbody { + + td, + th { + padding: 1em; + line-height: 26px; + } + + td:first-child { + padding-left: 2em; + } + + td:last-child { + padding-right: 2em; + } + } + + tbody tr { + border-top: 1px solid #f5f5f5; + } + + tbody tr:hover:not(.status-trash):not(.no-link) td { + cursor: pointer; + } + + .no-link { + cursor: default !important; + } + + // Columns. + td, + th { + width: 12ch; + vertical-align: middle; + + p { + margin: 0; + } + } + + .check-column { + width: 1px; + white-space: nowrap; + padding: 1em 1em 1em 1em !important; + vertical-align: middle; + + input { + vertical-align: text-top; + margin: 1px 0; + } + } + + .column-order_number { + width: 20ch; + } + + .column-order_total { + width: 8ch; + text-align: right; + + a span { + float: right; + } + } + + .column-order_date, + .column-order_status { + width: 10ch; + } + + .column-order_status { + width: 14ch; + } + + .column-shipping_address, + .column-billing_address { + width: 20ch; + line-height: 1.5em; + + .description { + display: block; + color: #999; + } + } + + .column-wc_actions { + text-align: right; + + a.button { + text-indent: 9999px; + margin: 2px 0 2px 4px; + } + } + + .order-preview { + float: right; + width: 16px; + padding: 20px 4px 4px 4px; + height: 0; + overflow: hidden; + position: relative; + border: 2px solid transparent; + border-radius: 4px; + + &::before { + + @include icon("\e010"); + line-height: 16px; + font-size: 14px; + vertical-align: middle; + top: 4px; + } + + &:hover { + border: 2px solid #00a0d2; + } + } + + .order-preview.disabled { + + &::before { + content: ""; + background: url("../images/wpspin-2x.gif") no-repeat center top; + background-size: 71%; + } + } + } +} + +.order-status { + display: inline-flex; + line-height: 2.5em; + color: #777; + background: #e5e5e5; + border-radius: 4px; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + margin: -0.25em 0; + cursor: inherit !important; + white-space: nowrap; + max-width: 100%; + + &.status-completed { + background: #c8d7e1; + color: #2e4453; + } + + &.status-on-hold { + background: #f8dda7; + color: #94660c; + } + + &.status-failed { + background: #eba3a3; + color: #761919; + } + + &.status-processing { + background: #c6e1c6; + color: #5b841b; + } + + &.status-trash { + background: #eba3a3; + color: #761919; + } + + > span { + margin: 0 1em; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.wc-order-preview { + + .order-status { + float: right; + margin-right: 54px; + } + + article { + padding: 0 !important; + } + + .modal-close { + border-radius: 0; + } + + .wc-order-preview-table { + width: 100%; + margin: 0; + + th, + td { + padding: 1em 1.5em; + text-align: left; + border: 0; + border-bottom: 1px solid #eee; + margin: 0; + background: transparent; + box-shadow: none; + text-align: right; + vertical-align: top; + } + + td:first-child, + th:first-child { + text-align: left; + } + + th { + border-color: #ccc; + } + + tr:last-child td { + border: 0; + } + + .wc-order-item-sku { + margin-top: 0.5em; + } + + .wc-order-item-meta { + margin-top: 0.5em; + + th, + td { + padding: 0; + border: 0; + text-align: left; + vertical-align: top; + } + + td:last-child { + padding-left: 0.5em; + } + } + } + + .wc-order-preview-addresses { + overflow: hidden; + padding-bottom: 1.5em; + + .wc-order-preview-address, + .wc-order-preview-note { + width: 50%; + float: left; + padding: 1.5em 1.5em 0; + box-sizing: border-box; + word-wrap: break-word; + + h2 { + margin-top: 0; + } + + strong { + display: block; + margin-top: 1.5em; + } + + strong:first-child { + margin-top: 0; + } + } + } + + footer { + + .wc-action-button-group { + display: inline-block; + float: left; + } + + .button.button-large { + margin-left: 10px; + padding: 0 10px !important; + line-height: 28px; + height: auto; + display: inline-block; + } + } + + .wc-action-button-group label { + display: none; + } +} + +.wc-action-button-group { + vertical-align: middle; + line-height: 26px; + text-align: left; + + label { + margin-right: 6px; + cursor: default; + font-weight: bold; + line-height: 28px; + } + + .wc-action-button-group__items { + display: inline-flex; + flex-flow: row wrap; + align-content: flex-start; + justify-content: flex-start; + } + + .wc-action-button { + margin: 0 0 0 -1px !important; + border: 1px solid #ccc; + padding: 0 10px !important; + border-radius: 0 !important; + float: none; + line-height: 28px; + height: auto; + z-index: 1; + position: relative; + overflow: hidden; + text-overflow: ellipsis; + flex: 1 0 auto; + box-sizing: border-box; + text-align: center; + white-space: nowrap; + } + + .wc-action-button:hover, + .wc-action-button:focus { + border: 1px solid #999; + z-index: 2; + } + + .wc-action-button:first-child { + margin-left: 0 !important; + border-top-left-radius: 3px !important; + border-bottom-left-radius: 3px !important; + } + + .wc-action-button:last-child { + border-top-right-radius: 3px !important; + border-bottom-right-radius: 3px !important; + } +} + +@media screen and (max-width: 782px) { + + .wc-order-preview footer { + + .wc-action-button-group .wc-action-button-group__items { + display: flex; + } + + .wc-action-button-group { + float: none; + display: block; + margin-bottom: 4px; + } + + .button.button-large { + width: 100%; + float: none; + text-align: center; + margin: 0; + display: block; + } + } + + .post-type-shop_order .wp-list-table { + + td.check-column { + width: 1em; + } + + td.column-order_number { + padding-left: 0; + padding-bottom: 0.5em; + } + + td.column-order_status, + td.column-order_date { + display: inline-block !important; + padding: 0 1em 1em 1em !important; + + &::before { + display: none !important; + } + } + + td.column-order_date { + padding-left: 0 !important; + } + + td.column-order_status { + float: right; + } + } +} + +.column-customer_message .note-on { + + @include ir(); + margin: 0 auto; + color: #999; + + &::after { + + @include icon("\e026"); + line-height: 16px; + } +} + +.column-order_notes .note-on { + + @include ir(); + margin: 0 auto; + color: #999; + + &::after { + + @include icon("\e027"); + line-height: 16px; + } +} + +.attributes-table { + + td, + th { + width: 15%; + vertical-align: top; + } + + .attribute-terms { + width: 32%; + } + + .attribute-actions { + width: 2em; + + .configure-terms { + + @include ir(); + padding: 0 !important; + height: 2em !important; + width: 2em; + + &::after { + + @include icon("\f111"); + font-family: "Dashicons"; + line-height: 1.85; + } + } + } +} + +/* Order notes */ +ul.order_notes { + padding: 2px 0 0; + + li { + + .note_content { + padding: 10px; + background: #efefef; + position: relative; + + p { + margin: 0; + padding: 0; + word-wrap: break-word; + } + } + + p.meta { + padding: 10px; + color: #999; + margin: 0; + font-size: 11px; + + .exact-date { + border-bottom: 1px dotted #999; + } + } + + a.delete_note { + color: $red; + } + + .note_content::after { + content: ""; + display: block; + position: absolute; + bottom: -10px; + left: 20px; + width: 0; + height: 0; + border-width: 10px 10px 0 0; + border-style: solid; + border-color: #efefef transparent; + } + } + + li.system-note { + + .note_content { + background: #d7cad2; + } + + .note_content::after { + border-color: #d7cad2 transparent; + } + } + + li.customer-note { + + .note_content { + background: #a7cedc; + } + + .note_content::after { + border-color: #a7cedc transparent; + } + } +} + +.add_note { + border-top: 1px solid #ddd; + padding: 10px 10px 0; + + h4 { + margin-top: 5px !important; + } + + #add_order_note { + width: 100%; + height: 50px; + } +} + +table.wp-list-table { + + .column-thumb { + width: 52px; + text-align: center; + white-space: nowrap; + } + + .column-handle { + width: 17px; + display: none; + } + + tbody { + + td.column-handle { + cursor: move; + width: 17px; + text-align: center; + vertical-align: text-top; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + height: 100%; + margin: 4px 0 0 0; + } + } + } + + .column-name { + width: 22%; + } + + .column-product_cat, + .column-product_tag { + width: 11% !important; + } + + .column-featured, + .column-product_type { + width: 48px; + text-align: left !important; + } + + .column-customer_message, + .column-order_notes { + width: 48px; + text-align: center; + + img { + margin: 0 auto; + padding-top: 0 !important; + } + } + + .manage-column.column-featured img, + .manage-column.column-product_type img { + padding-left: 2px; + } + + .column-price .woocommerce-price-suffix { + display: none; + } + + img { + margin: 1px 2px; + } + + .row-actions { + color: #999; + } + + .row-actions span.id { + padding-top: 8px; + } + + td.column-thumb img { + margin: 0; + width: auto; + height: auto; + max-width: 40px; + max-height: 40px; + vertical-align: middle; + } + + span.na { + color: #999; + } + + .column-sku { + width: 10%; + } + + .column-price { + width: 10ch; + } + + .column-is_in_stock { + text-align: left !important; + width: 12ch; + } + + span.wc-image, + span.wc-featured { + + @include ir(); + margin: 0 auto; + + &::before { + + @include icon_dashicons("\f128"); + } + } + + span.wc-featured { + + &::before { + content: "\f155"; + } + + &.not-featured::before { + content: "\f154"; + } + } + + td.column-featured span.wc-featured { + font-size: 1.6em; + cursor: pointer; + } + + mark { + + &.instock, + &.outofstock, + &.onbackorder { + font-weight: 700; + background: transparent none; + line-height: 1; + } + + &.instock { + color: $green; + } + + &.outofstock { + color: #a44; + } + + &.onbackorder { + color: #eaa600; + } + } + + .order-notes_head, + .notes_head, + .status_head { + + @include ir(); + margin: 0 auto; + + &::after { + + @include icon; + } + } + + .order-notes_head::after { + content: "\e028"; + } + + .notes_head::after { + content: "\e026"; + } + + .status_head::after { + content: "\e011"; + } + + .column-order_items { + width: 12%; + + table.order_items { + width: 100%; + margin: 3px 0 0; + padding: 0; + display: none; + + td { + border: 0; + margin: 0; + padding: 0 0 3px; + } + + td.qty { + color: #999; + padding-right: 6px; + text-align: left; + } + } + } +} + +mark.notice { + background: #fff; + color: $red; + margin: 0 0 0 10px; +} + +a.export_rates, +a.import_rates { + float: right; + margin-left: 9px; + margin-top: -2px; + margin-bottom: 0; +} + +#rates-search { + float: right; + + input.wc-tax-rates-search-field { + padding: 4px 8px; + font-size: 1.2em; + } +} + +#rates-pagination { + float: right; + margin-right: 0.5em; + + .tablenav { + margin: 0; + } +} + +.wc_input_table_wrapper { + overflow-x: auto; + display: block; +} + +table.wc_tax_rates, +table.wc_input_table { + width: 100%; + + th, + td { + display: table-cell !important; + } + + span.tips { + color: $blue; + } + + th { + white-space: nowrap; + padding: 10px; + } + + td { + padding: 0; + border-right: 1px solid #dfdfdf; + border-bottom: 1px solid #dfdfdf; + border-top: 0; + background: #fff; + cursor: default; + + input[type="text"], + input[type="number"] { + width: 100% !important; + min-width: 100px; + padding: 8px 10px; + margin: 0; + border: 0; + outline: 0; + background: transparent none; + + &:focus { + outline: 0; + box-shadow: none; + } + } + + &.compound, + &.apply_to_shipping { + padding: 5px 7px; + vertical-align: middle; + + input { + padding: 0; + } + } + } + + td:last-child { + border-right: 0; + } + + tr.current td { + background-color: #fefbcc; + } + + .item_cost, + .cost { + text-align: right; + + input { + text-align: right; + } + } + + th.sort { + width: 17px; + padding: 0 4px; + } + + td.sort { + padding: 0 4px; + } + + .ui-sortable:not(.ui-sortable-disabled) td.sort { + cursor: move; + font-size: 15px; + background: #f9f9f9; + text-align: center; + vertical-align: middle; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + } + + &:hover::before { + color: #333; + } + } + + .button { + float: left; + margin-right: 5px; + } + + .export, + .import { + float: right; + margin-right: 0; + margin-left: 5px; + } + + span.tips { + padding: 0 3px; + } + + .pagination { + float: right; + + .button { + margin-left: 5px; + margin-right: 0; + } + + .current { + background: #bbb; + text-shadow: none; + } + } + + tr:last-child td { + border-bottom: 0; + } +} + +table.wc_tax_rates { + + td.country { + position: relative; + } +} + +table.wc_gateways, +table.wc_emails, +table.wc_shipping { + position: relative; + + th, + td { + display: table-cell !important; + padding: 1em !important; + vertical-align: top; + line-height: 1.75em; + } + + &.wc_emails td { + vertical-align: middle; + } + + tr:nth-child(odd) td { + background: #f9f9f9; + } + + td.name { + font-weight: 700; + } + + .settings { + text-align: right; + } + + .radio, + .default, + .status { + text-align: center; + + .tips { + margin: 0 auto; + } + + input { + margin: 0; + } + } + + td.sort { + font-size: 15px; + text-align: center; + + .wc-item-reorder-nav { + white-space: nowrap; + width: 72px; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 24px; + float: left; + height: 100%; + line-height: 24px; + cursor: move; + } + + button { + position: relative; + overflow: hidden; + float: left; + display: block; + width: 24px; + height: 24px; + margin: 0; + background: transparent; + border: none; + box-shadow: none; + color: #82878c; + text-indent: -9999px; + cursor: pointer; + outline: none; + } + + button::before { + display: inline-block; + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + font: normal 20px/23px dashicons; + text-align: center; + text-indent: 0; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + button:hover, + button:focus { + color: #191e23; + } + + .wc-move-down::before { + content: "\f347"; + } + + .wc-move-up::before { + content: "\f343"; + } + + .wc-move-disabled { + color: #d5d5d5 !important; + cursor: default; + pointer-events: none; + } + } + } + + .wc-payment-gateway-method-name { + font-weight: normal; + } + + .wc-email-settings-table-name { + font-weight: 700; + + span { + font-weight: normal; + color: #999; + margin: 0 0 0 4px !important; + } + } + + .wc-payment-gateway-method-toggle-enabled, + .wc-payment-gateway-method-toggle-disabled { + padding-top: 1px; + display: block; + outline: 0; + box-shadow: none; + } + + .wc-email-settings-table-status { + text-align: center; + width: 1em; + + .tips { + margin: 0 auto; + } + } +} + +.wc-shipping-zone-settings { + + th { + padding: 24px 24px 24px 0; + } + + td.forminp { + + input, + textarea { + padding: 8px; + max-width: 100% !important; + } + + .wc-shipping-zone-region-select { + width: 448px; + max-width: 100% !important; + + .select2-choices { + padding: 8px 8px 4px; + border-color: #ddd; + min-height: 0; + line-height: 1; + + input { + padding: 0; + } + + li { + margin: 0 4px 4px 0; + } + } + } + } + + .wc-shipping-zone-postcodes-toggle { + margin: 0.5em 0 0; + font-size: 0.9em; + text-decoration: underline; + display: block; + } + + .wc-shipping-zone-postcodes-toggle + .wc-shipping-zone-postcodes { + display: none; + } + + .wc-shipping-zone-postcodes { + + textarea { + margin: 10px 0; + } + + .description { + font-size: 0.9em; + color: #999; + } + } +} + +.wc-shipping-zone-settings + p.submit { + margin-top: 0; +} + +.wc-shipping-zone-settings tbody { + display: table-row-group; +} + +table { + + tr, + tr:hover { + + table.wc-shipping-zone-methods { + + tr .row-actions { + position: relative; + } + + tr:hover .row-actions { + position: static; + } + } + } +} + +.wc-shipping-zones-heading .page-title-action { + display: inline-block; +} + +table.wc-shipping-zones, +table.wc-shipping-zone-methods, +table.wc-shipping-classes { + + td, + th { + vertical-align: top; + line-height: 24px; + padding: 1em !important; + font-size: 14px; + background: #fff; + display: table-cell !important; + + li { + line-height: 24px; + font-size: 14px; + } + + .woocommerce-help-tip { + margin: 0 !important; + } + } + + thead { + + th { + vertical-align: middle; + } + + .wc-shipping-zone-sort { + text-align: center; + } + } + + td.wc-shipping-zones-blank-state, + td.wc-shipping-zone-method-blank-state { + background: #f7f1f6 !important; + overflow: hidden; + position: relative; + padding: 7.5em 7.5% !important; + border-bottom: 2px solid #eee2ec; + + &.wc-shipping-zone-method-blank-state { + padding: 2em !important; + + p { + margin-bottom: 0; + } + } + + p, + li { + color: #a46497; + font-size: 1.5em; + line-height: 1.5em; + margin: 0 0 1em; + position: relative; + z-index: 1; + text-shadow: 1px 1px 1px white; + + &.main { + font-size: 2em; + } + } + + li { + margin-left: 1em; + list-style: circle inside; + } + + &::before { + content: "\e01b"; + font-family: "WooCommerce"; + text-align: center; + line-height: 1; + color: #eee2ec; + display: block; + width: 1em; + font-size: 40em; + top: 50%; + right: -3.75%; + margin-top: -0.1875em; + position: absolute; + } + + .button-primary { + background-color: #804877; + border-color: #804877; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.2), + 0 1px 0 rgba(0, 0, 0, 0.15); + margin: 0; + opacity: 1; + text-shadow: + 0 -1px 1px #8a4f7f, + 1px 0 1px #8a4f7f, + 0 1px 1px #8a4f7f, + -1px 0 1px #8a4f7f; + font-size: 1.5em; + padding: 0.75em 1em; + height: auto; + position: relative; + z-index: 1; + } + } + + .wc-shipping-zone-method-rows { + + tr:nth-child(even) td { + background: #f9f9f9; + } + } + + tr.odd, + .wc-shipping-class-rows tr:nth-child(odd) { + + td { + background: #f9f9f9; + } + } + + tbody.wc-shipping-zone-rows { + + td { + border-top: 2px solid #f9f9f9; + } + + tr:first-child { + + td { + border-top: 0; + } + } + } + + tr.wc-shipping-zone-worldwide { + + td { + background: #f9f9f9; + border-top: 2px solid #e1e1e1; + } + } + + ul, + p { + margin: 0; + } + + td.wc-shipping-zone-sort, + td.wc-shipping-zone-method-sort { + cursor: move; + font-size: 15px; + text-align: center; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + line-height: 24px; + } + + &:hover::before { + color: #333; + } + } + + td.wc-shipping-zone-worldwide { + text-align: center; + + &::before { + content: "\f319"; + font-family: "dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + line-height: 24px; + } + } + + .wc-shipping-zone-name, + .wc-shipping-zone-methods { + width: 25%; + } + + .wc-shipping-class-description, + .wc-shipping-class-name, + .wc-shipping-class-slug, + .wc-shipping-zone-name, + .wc-shipping-zone-region { + + input, + select, + textarea { + width: 100%; + } + + a.wc-shipping-zone-delete, + a.wc-shipping-class-delete { + color: #a00; + } + + a.wc-shipping-zone-delete:hover, + a.wc-shipping-class-delete:hover { + color: red; + } + } + + .wc-shipping-class-count { + text-align: center; + } + + td.wc-shipping-zone-methods { + color: #555; + + .method_disabled { + text-decoration: line-through; + } + + ul { + position: relative; + padding-right: 32px; + + li { + color: #555; + display: inline; + margin: 0; + } + + li::before { + content: ", "; + } + + li:first-child::before { + content: ""; + } + } + + .add_shipping_method { + display: block; + width: 24px; + padding: 24px 0 0; + height: 0; + overflow: hidden; + cursor: pointer; + + &::before { + + @include icon; + font-family: "Dashicons"; + content: "\f502"; + color: #999; + vertical-align: middle; + line-height: 24px; + font-size: 16px; + margin: 0; + } + + &.disabled { + cursor: not-allowed; + + &::before { + color: #ccc; + } + } + } + } + + .wc-shipping-zone-method-title { + width: 25%; + + .wc-shipping-zone-method-delete { + color: red; + } + } + + .wc-shipping-zone-method-enabled { + text-align: center; + + a { + display: inline-block; + } + + .woocommerce-input-toggle { + margin-top: 3px; + } + } + + .wc-shipping-zone-method-type { + display: block; + } + + tfoot { + + input, + select { + vertical-align: middle !important; + } + + .button-secondary { + float: right; + } + } + + .editing { + + .wc-shipping-zone-view, + .wc-shipping-zone-edit { + display: none; + } + } +} + +.woocommerce-input-toggle { + height: 16px; + width: 32px; + border: 2px solid #935687; + background-color: #935687; + display: inline-block; + text-indent: -9999px; + border-radius: 10em; + position: relative; + margin-top: -1px; + vertical-align: text-top; + + &::before { + content: ""; + display: block; + width: 16px; + height: 16px; + background: #fff; + position: absolute; + top: 0; + right: 0; + border-radius: 100%; + } + + &.woocommerce-input-toggle--disabled { + border-color: #999; + background-color: #999; + + &::before { + right: auto; + left: 0; + } + } + + &.woocommerce-input-toggle--loading { + opacity: 0.5; + } +} + +.wc-modal-shipping-method-settings { + background: #f8f8f8; + padding: 1em !important; + + form .form-table { + width: 100%; + background: #fff; + margin: 0 0 1.5em; + + tr { + + th { + width: 30%; + position: relative; + + .woocommerce-help-tip { + float: right; + margin: -8px -0.5em 0 0; + vertical-align: middle; + right: 0; + top: 50%; + position: absolute; + } + } + + td { + + input, + select, + textarea { + width: 50%; + min-width: 250px; + } + + input[type="checkbox"] { + width: auto; + min-width: 16px; + } + } + + td, + th { + vertical-align: middle; + margin: 0; + line-height: 24px; + padding: 1em; + border-bottom: 1px solid #f8f8f8; + } + } + + &:last-of-type { + margin-bottom: 0; + } + } +} + +.wc-backbone-modal .wc-shipping-zone-method-selector { + + p { + margin-top: 0; + } + + .wc-shipping-zone-method-description { + margin: 0.75em 1px 0; + line-height: 1.5em; + color: #999; + font-style: italic; + } + + select { + width: 100%; + cursor: pointer; + } +} + +img.help_tip { + margin: 0 0 0 9px; + vertical-align: middle; +} + +.postbox img.help_tip { + margin-top: 0; +} + +.postbox .woocommerce-help-tip { + margin: 0 0 0 9px; +} + +.status-enabled, +.status-manual, +.status-disabled { + font-size: 1.4em; + + @include ir(); +} + +.status-manual::before { + + @include icon("\e008"); + color: #999; +} + +.status-enabled::before { + + @include icon("\e015"); + color: $woocommerce; +} + +.status-disabled::before { + + @include icon("\e013"); + color: #ccc; +} + +.woocommerce { + + h2.woo-nav-tab-wrapper { + margin-bottom: 1em; + } + + nav.woo-nav-tab-wrapper { + margin: 1.5em 0 1em; + } + + .subsubsub { + margin: -8px 0 0; + } + + .wc-admin-breadcrumb { + margin-left: 0.5em; + + a { + color: #a46497; + } + } + + #template div { + margin: 0; + + p .button { + float: right; + margin-left: 10px; + margin-top: -4px; + } + + .editor textarea { + margin-bottom: 8px; + } + } + + textarea[disabled="disabled"] { + background: #dfdfdf !important; + } + + table.form-table { + margin: 0; + position: relative; + table-layout: fixed; + + .forminp-radio ul { + margin: 0; + + li { + line-height: 1.4em; + } + } + + input[type="text"], + input[type="number"], + input[type="email"] { + height: auto; + } + + textarea.input-text { + height: 100%; + min-width: 150px; + display: block; + } + + // Give regular settings inputs a standard width and padding. + textarea, + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"], + input[type="datetime"], + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="url"], + input[type="tel"], + input.regular-input { + width: 400px; + margin: 0; + padding: 6px; + box-sizing: border-box; + vertical-align: top; + } + + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="tel"] { + width: 200px; + } + + select { + width: 400px; + margin: 0; + box-sizing: border-box; + line-height: 32px; + vertical-align: top; + } + + input[size] { + width: auto !important; + } + + // Ignore nested inputs. + table { + + select, + textarea, + input[type="text"], + input[type="email"], + input[type="number"], + input.regular-input { + width: auto; + } + } + + textarea.wide-input { + width: 100%; + } + + img.help_tip, + .woocommerce-help-tip { + padding: 0; + margin: -4px 0 0 5px; + vertical-align: middle; + cursor: help; + line-height: 1; + } + + span.help_tip { + cursor: help; + color: $blue; + } + + th { + position: relative; + padding-right: 24px; + } + + th label { + position: relative; + display: block; + + img.help_tip, + .woocommerce-help-tip { + margin: -8px -24px 0 0; + position: absolute; + right: 0; + top: 50%; + } + } + + th label + .woocommerce-help-tip { + margin: 0 0 0 0; + position: absolute; + right: 0; + top: 20px; + } + + .select2-container { + vertical-align: top; + margin-bottom: 3px; + } + + .select2-container + span.description { + display: block; + margin-top: 8px; + } + + table.widefat th { + padding-right: inherit; + } + + .wp-list-table .woocommerce-help-tip { + float: none; + } + + fieldset { + margin-top: 4px; + + img.help_tip, + .woocommerce-help-tip { + margin: -3px 0 0 5px; + } + + p.description { + margin-bottom: 8px; + } + + &:first-child { + margin-top: 0; + } + } + + .iris-picker { + z-index: 100; + display: none; + position: absolute; + border: 1px solid #ccc; + border-radius: 3px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + + .ui-slider { + border: 0 !important; + margin: 0 !important; + width: auto !important; + height: auto !important; + background: none transparent !important; + + .ui-slider-handle { + margin-bottom: 0 !important; + } + } + } + + .iris-error { + background-color: #ffafaf; + } + + .colorpickpreview { + padding: 7px 0; + line-height: 1em; + display: inline-block; + width: 26px; + border: 1px solid #ddd; + font-size: 14px; + } + + .image_width_settings { + vertical-align: middle; + + label { + margin-left: 10px; + } + + input { + width: auto; + } + } + + .wc_payment_gateways_wrapper, + .wc_emails_wrapper { + padding: 0 15px 10px 0; + } + } + + .wc-shipping-zone-settings { + + td.forminp { + + input, + textarea { + width: 448px; + padding: 6px 11px; + } + + .select2-search input { + padding: 6px; + } + } + } +} + +.wc-wp-version-gte-53 { + + .woocommerce { + + h2.wc-table-list-header { + margin: 1em 0 0.35em 0; + } + + input + .subsubsub { + margin: 8px 0 0; + } + + table.form-table { + // Give regular settings inputs a standard width and padding. + textarea, + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"], + input[type="datetime"], + input[type="datetime-local"], + input[type="date"], + input[type="time"], + input[type="week"], + input[type="url"], + input[type="tel"], + input.regular-input { + padding: 0 8px; + + @media only screen and (max-width: 782px) { + width: 100%; + } + } + + select { + + @media only screen and (max-width: 782px) { + width: 100%; + } + } + + th label { + + img.help_tip, + .woocommerce-help-tip { + margin: -7px -24px 0 0; + + @media only screen and (max-width: 782px) { + right: auto; + margin-left: 5px; + } + } + } + + .forminp-color { + font-size: 0; + } + + .colorpickpreview { + padding: 0; + width: 30px; + height: 30px; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); + font-size: 16px; + border-radius: 4px; + margin-right: 3px; + + @media only screen and (max-width: 782px) { + float: left; + width: 40px; + height: 40px; + } + } + } + } +} + +.woocommerce #tabs-wrap table a.remove { + margin-left: 4px; +} + +.woocommerce #tabs-wrap table p { + margin: 0 0 4px !important; + overflow: hidden; + zoom: 1; +} + +.woocommerce #tabs-wrap table p a.add { + float: left; +} + +#wp-excerpt-editor-container { + background: #fff; +} + +#product_variation-parent #parent_id { + width: 100%; +} + +#postimagediv img { + border: 1px solid #d5d5d5; + max-width: 100%; +} + +#woocommerce-product-images .inside { + margin: 0; + padding: 0; + + .add_product_images { + padding: 0 12px 12px; + } + + #product_images_container { + padding: 0 0 0 9px; + + ul { + + @include clearfix(); + margin: 0; + padding: 0; + + li.image, + li.add, + li.wc-metabox-sortable-placeholder { + width: 80px; + float: left; + cursor: move; + border: 1px solid #d5d5d5; + margin: 9px 9px 0 0; + background: #f7f7f7; + + @include border-radius(2px); + position: relative; + box-sizing: border-box; + + img { + width: 100%; + height: auto; + display: block; + } + } + + li.wc-metabox-sortable-placeholder { + border: 3px dashed #ddd; + position: relative; + + &::after { + + @include icon_dashicons("\f161"); + font-size: 2.618em; + line-height: 72px; + color: #ddd; + } + } + + ul.actions { + position: absolute; + top: -8px; + right: -8px; + padding: 2px; + display: none; + + @media (max-width: 768px) { + display: block; + } + + li { + float: right; + margin: 0 0 0 2px; + + a { + width: 1em; + height: 1em; + margin: 0; + height: 0; + display: block; + overflow: hidden; + + &.tips { + cursor: pointer; + } + } + + a.delete { + + @include ir(); + font-size: 1.4em; + + &::before { + + @include icon_dashicons("\f153"); + color: #999; + background: #fff; + border-radius: 50%; + height: 1em; + width: 1em; + line-height: 1em; + } + + &:hover::before { + color: $red; + } + } + } + } + + li:hover ul.actions { + display: block; + } + } + } +} + +#woocommerce-product-data { + + .hndle { + padding: 10px; + + span { + display: block; + line-height: 24px; + } + + .type_box { + display: inline; + line-height: inherit; + vertical-align: baseline; + } + + select { + margin: 0; + } + + label { + padding-right: 1em; + font-size: 12px; + vertical-align: baseline; + } + + label:first-child { + margin-right: 1em; + border-right: 1px solid #dfdfdf; + } + + input, + select { + margin-top: -3px 0 0; + vertical-align: middle; + } + + select { + margin-left: 0.5em; + } + } + + > .handlediv { + margin-top: 4px; + } + + .wrap { + margin: 0; + } +} + +#woocommerce-coupon-description { + padding: 3px 8px; + font-size: 1.7em; + line-height: 1.42em; + height: auto; + width: 100%; + outline: 0; + margin: 10px 0; + display: block; + + &::-webkit-input-placeholder { + line-height: 1.42em; + color: #bbb; + } + + &::-moz-placeholder { + line-height: 1.42em; + color: #bbb; + } + + &:-ms-input-placeholder { + line-height: 1.42em; + color: #bbb; + } + + &:-moz-placeholder { + line-height: 1.42em; + color: #bbb; + } +} + +#woocommerce-product-data, +#woocommerce-coupon-data { + + .panel-wrap { + background: #fff; + } + + .woocommerce_options_panel, + .wc-metaboxes-wrapper { + float: left; + width: 80%; + + .wc-radios { + display: block; + float: left; + margin: 0; + + li { + display: block; + padding: 0 0 10px; + + input { + width: auto; + } + } + } + } +} + +#woocommerce-product-data, +#woocommerce-coupon-data, +.woocommerce { + + .panel-wrap { + overflow: hidden; + } + + ul.wc-tabs { + margin: 0; + width: 20%; + float: left; + line-height: 1em; + padding: 0 0 10px; + position: relative; + background-color: #fafafa; + border-right: 1px solid #eee; + box-sizing: border-box; + + &::after { + content: ""; + display: block; + width: 100%; + height: 9999em; + position: absolute; + bottom: -9999em; + left: 0; + background-color: #fafafa; + border-right: 1px solid #eee; + } + + li { + margin: 0; + padding: 0; + display: block; + position: relative; + + a { + margin: 0; + padding: 10px; + display: block; + box-shadow: none; + text-decoration: none; + line-height: 20px !important; + border-bottom: 1px solid #eee; + + span { + margin-left: 0.618em; + margin-right: 0.618em; + } + + &::before { + + @include iconbeforedashicons("\f107"); + } + } + + &.general_options a::before { + content: "\f107"; + } + + &.inventory_options a::before { + content: "\f481"; + } + + &.shipping_options a::before { + font-family: "WooCommerce"; + content: "\e01a"; + } + + &.linked_product_options a::before { + content: "\f103"; + } + + &.attribute_options a::before { + content: "\f175"; + } + + &.advanced_options a::before { + font-family: "Dashicons"; + content: "\f111"; + } + + &.marketplace-suggestions_options a::before { + content: none; + } + + &.variations_options a::before { + content: "\f509"; + } + + &.usage_restriction_options a::before { + font-family: "WooCommerce"; + content: "\e602"; + } + + &.usage_limit_options a::before { + font-family: "WooCommerce"; + content: "\e601"; + } + + &.general_coupon_data a::before { + font-family: "WooCommerce"; + content: "\e600"; + } + + &.active a { + color: #555; + position: relative; + background-color: #eee; + } + } + } +} + +/** + * Shipping + */ +.woocommerce_page_wc-settings { + + input[type="url"], + input[type="email"] { + direction: ltr; + } + + .shippingrows { + + th.check-column { + padding-top: 20px; + } + + tfoot th { + padding-left: 10px; + } + + .add.button::before { + + @include iconbefore("\e007"); + } + } + + h3.wc-settings-sub-title { + font-size: 1.2em; + } +} + +#woocommerce-product-data, +#woocommerce-product-type-options, +#woocommerce-order-data, +#woocommerce-order-downloads, +#woocommerce-coupon-data { + + .inside { + margin: 0; + padding: 0; + } +} + +.woocommerce_options_panel, +.panel { + padding: 9px; + color: #555; + + .form-field .woocommerce-help-tip { + font-size: 1.4em; + } +} + +.woocommerce_page_settings .woocommerce_options_panel, +.panel { + padding: 0; +} + +#woocommerce-product-type-options .panel, +#woocommerce-product-specs .inside { + margin: 0; + padding: 9px; +} + +.woocommerce_options_panel p, +#woocommerce-product-type-options .panel p, +.woocommerce_options_panel fieldset.form-field { + margin: 0 0 9px; + font-size: 12px; + padding: 5px 9px; + line-height: 24px; + + &::after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; + } +} + +.woocommerce_options_panel .checkbox, +.woocommerce_variable_attributes .checkbox { + margin: 4px 0 !important; + vertical-align: middle; + float: left; +} + +.woocommerce_variations, +.woocommerce_options_panel { + + .downloadable_files table { + width: 100%; + padding: 0 !important; + + th { + padding: 7px 0 7px 7px !important; + + &.sort { + width: 17px; + padding: 7px !important; + } + + .woocommerce-help-tip { + font-size: 1.1em; + margin-left: 0; + } + } + + td { + vertical-align: middle !important; + padding: 4px 0 4px 7px !important; + position: relative; + + &:last-child { + padding-right: 7px !important; + } + + input.input_text { + width: 100%; + float: none; + min-width: 0; + margin: 1px 0; + } + + .upload_file_button { + width: auto; + float: right; + cursor: pointer; + } + + .delete { + + @include ir(); + font-size: 1.2em; + + &::before { + + @include icon_dashicons("\f153"); + color: #999; + } + + &:hover { + + &::before { + color: $red; + } + } + } + } + + td.sort { + width: 17px; + cursor: move; + font-size: 15px; + text-align: center; + background: #f9f9f9; + padding-right: 7px !important; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 1; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + } + + &:hover::before { + color: #333; + } + } + } +} + +.woocommerce_attribute, +.woocommerce_variation { + + h3 .sort { + width: 17px; + height: 26px; + cursor: move; + float: right; + font-size: 15px; + font-weight: 400; + margin-right: 0.5em; + visibility: hidden; + text-align: center; + vertical-align: middle; + + &::before { + content: "\f333"; + font-family: "Dashicons"; + text-align: center; + line-height: 28px; + color: #999; + display: block; + width: 17px; + float: left; + height: 100%; + } + + &:hover::before { + color: #777; + } + } + + h3:hover, + &.ui-sortable-helper { + + .sort { + visibility: visible; + } + } +} + +.woocommerce_options_panel { + min-height: 175px; + box-sizing: border-box; + + .downloadable_files { + padding: 0 9px 0 162px; + position: relative; + margin: 9px 0; + + label { + position: absolute; + left: 0; + margin: 0 0 0 12px; + line-height: 24px; + } + } + + p { + margin: 9px 0; + } + + p.form-field, + fieldset.form-field { + padding: 5px 20px 5px 162px !important; /** Padding for aligning labels left - 12px + 150 label width **/ + } + + .sale_price_dates_fields { + + .short:first-of-type { + margin-bottom: 1em; + } + + .short:nth-of-type(2) { + clear: left; + } + } + + label, + legend { + float: left; + width: 150px; + padding: 0; + margin: 0 0 0 -150px; + + .req { + font-weight: 700; + font-style: normal; + color: $red; + } + } + + .description { + padding: 0; + margin: 0 0 0 7px; + clear: none; + display: inline; + } + + .description-block { + margin-left: 0; + display: block; + } + + textarea, + input, + select { + margin: 0; + } + + textarea { + float: left; + height: 3.5em; + line-height: 1.5em; + vertical-align: top; + } + + input[type="text"], + input[type="email"], + input[type="number"], + input[type="password"] { + width: 50%; + float: left; + } + + input.button { + width: auto; + margin-left: 8px; + } + + select { + float: left; + } + + input[type="text"].short, + input[type="email"].short, + input[type="number"].short, + input[type="password"].short, + .short { + width: 50%; + } + + .sized { + width: auto !important; + margin-right: 6px; + } + + .options_group { + border-top: 1px solid white; + border-bottom: 1px solid #eee; + + &:first-child { + border-top: 0; + } + + &:last-child { + border-bottom: 0; + } + + fieldset { + margin: 9px 0; + font-size: 12px; + padding: 5px 9px; + line-height: 24px; + + label { + width: auto; + float: none; + } + + ul { + float: left; + width: 50%; + margin: 0; + padding: 0; + + li { + margin: 0; + width: auto; + + input { + width: auto; + float: none; + margin-right: 4px; + } + } + } + + ul.wc-radios label { + margin-left: 0; + } + } + } + + .dimensions_field .wrap { + display: block; + width: 50%; + + input { + width: 30.75%; + margin-right: 3.8%; + } + + .last { + margin-right: 0; + } + } + + &.padded { + padding: 1em; + } + + .select2-container { + float: left; + } +} + +#woocommerce-product-data input.dp-applied { + float: left; +} + +#grouped_product_options, +#virtual_product_options, +#simple_product_options { + padding: 12px; + font-style: italic; + color: #666; +} + +/** + * WooCommerce meta boxes + */ +.wc-metaboxes-wrapper { + + .toolbar { + margin: 0 !important; + border-top: 1px solid white; + border-bottom: 1px solid #eee; + padding: 9px 12px !important; + + &:first-child { + border-top: 0; + } + + &:last-child { + border-bottom: 0; + } + + .add_variation { + float: right; + margin-left: 5px; + } + + .save-variation-changes, + .cancel-variation-changes { + float: left; + margin-right: 5px; + } + } + + p.toolbar { + overflow: hidden; + zoom: 1; + } + + .expand-close { + margin-right: 2px; + color: #777; + font-size: 12px; + font-style: italic; + + a { + background: none; + padding: 0; + font-size: 12px; + text-decoration: none; + } + } + + &#product_attributes .expand-close { + float: right; + line-height: 28px; + } + + button.add_variable_attribute, + .fr { + float: right; + margin: 0 0 0 6px; + } + + .wc-metaboxes { + border-bottom: 1px solid #eee; + } + + .wc-metabox-sortable-placeholder { + border-color: #bbb; + background-color: #f5f5f5; + margin-bottom: 9px; + border-width: 1px; + border-style: dashed; + } + + .wc-metabox { + background: #fff; + border-bottom: 1px solid #eee; + margin: 0 !important; + + select { + font-weight: 400; + } + + &:last-of-type { + border-bottom: 0; + } + + .handlediv { + width: 27px; + float: right; + + &::before { + content: "\f142" !important; + cursor: pointer; + display: inline-block; + font: 400 20px/1 "Dashicons"; + line-height: 0.5 !important; + padding: 8px 10px; + position: relative; + right: 12px; + top: 0; + } + } + + &.closed { + + @include border-radius(3px); + + .handlediv::before { + content: "\f140" !important; + } + + h3 { + border: 0; + } + } + + h3 { + margin: 0 !important; + padding: 0.75em 0.75em 0.75em 1em !important; + font-size: 1em !important; + overflow: hidden; + zoom: 1; + cursor: move; + + button, + a.delete { + float: right; + } + + a.delete { + color: red; + font-weight: normal; + line-height: 26px; + text-decoration: none; + position: relative; + visibility: hidden; + } + + strong { + font-weight: normal; + line-height: 26px; + font-weight: 700; + } + + select { + font-family: sans-serif; + max-width: 20%; + margin: 0.25em 0.25em 0.25em 0; + } + + .handlediv { + background-position: 6px 5px !important; + visibility: hidden; + height: 26px; + } + + &.fixed { + cursor: pointer !important; + } + } + + &.woocommerce_attribute h3, + &.woocommerce_variation h3 { + cursor: pointer; + padding: 0.5em 0.75em 0.5em 1em !important; + + a.delete, + .handlediv, + .sort { + margin-top: 0.25em; + } + } + + h3:hover, + &.ui-sortable-helper { + + a.delete, + .handlediv { + visibility: visible; + } + } + + table { + width: 100%; + position: relative; + background-color: #fdfdfd; + padding: 1em; + border-top: 1px solid #eee; + + td { + text-align: left; + padding: 0 6px 1em 0; + vertical-align: top; + border: 0; + + label { + text-align: left; + display: block; + line-height: 21px; + } + + input { + float: left; + min-width: 200px; + } + + input, + textarea { + width: 100%; + margin: 0; + display: block; + font-size: 14px; + padding: 4px; + color: #555; + } + + select, + .select2-container { + width: 100% !important; + } + + input.short { + width: 200px; + } + + input.checkbox { + width: 16px; + min-width: inherit; + vertical-align: text-bottom; + display: inline-block; + float: none; + } + } + + td.attribute_name { + width: 200px; + } + + .plus, + .minus { + margin-top: 6px; + } + + .fl { + float: left; + } + + .fr { + float: right; + } + } + } +} + +.variations-pagenav { + float: right; + line-height: 24px; + + .displaying-num { + color: #777; + font-size: 12px; + font-style: italic; + } + + a { + padding: 0 10px 3px; + background: rgba(0, 0, 0, 0.05); + font-size: 16px; + font-weight: 400; + text-decoration: none; + } + + a.disabled, + a.disabled:active, + a.disabled:focus, + a.disabled:hover { + color: #a0a5aa; + background: rgba(0, 0, 0, 0.05); + } +} + +.variations-defaults { + float: left; + + select { + margin: 0.25em 0.25em 0.25em 0; + } +} + +.woocommerce_variable_attributes { + background-color: #fdfdfd; + border-top: 1px solid #eee; + + .data { + + @include clearfix; + padding: 1em 2em; + } + + .upload_image_button { + display: block; + width: 64px; + height: 64px; + float: left; + margin-right: 20px; + position: relative; + cursor: pointer; + + img { + width: 100%; + height: auto; + display: none; + } + + &::before { + content: "\f128"; + font-family: "Dashicons"; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + text-align: center; + line-height: 64px; + font-size: 64px; + font-weight: 400; + -webkit-font-smoothing: antialiased; + } + + &.remove { + + img { + display: block; + } + + &::before { + content: "\f335"; + display: none; + } + + &:hover::before { + display: block; + } + } + } + + .options { + border: 1px solid #eee; + border-width: 1px 0; + padding: 0.25em 0; + + label { + display: inline-block; + padding: 4px 1em 2px 0; + } + + input[type="checkbox"] { + margin: 0 5px 0 0.5em !important; + vertical-align: middle; + } + } +} + +.form-row { + + label { + display: inline-block; + } + + .woocommerce-help-tip { + float: right; + } + + input[type="text"], + input[type="number"], + input[type="password"], + input[type="color"], + input[type="date"], + input[type="datetime"], + input[type="datetime-local"], + input[type="email"], + input[type="month"], + input[type="search"], + input[type="tel"], + input[type="time"], + input[type="url"], + input[type="week"], + select, + textarea { + width: 100%; + vertical-align: middle; + margin: 2px 0 0; + padding: 5px; + } + + select { + height: 40px; + } + + &.dimensions_field { + + .wrap { + clear: left; + display: block; + } + + input { + width: 33%; + float: left; + vertical-align: middle; + + &:last-of-type { + margin-right: 0; + width: 34%; + } + } + } + + &.form-row-first, + &.form-row-last { + width: 48%; + float: right; + } + + &.form-row-first { + clear: both; + float: left; + } + + &.form-row-full { + clear: both; + } +} + +/** + * Tooltips + */ +.tips { + cursor: help; + text-decoration: none; +} + +img.tips { + padding: 5px 0 0; +} + +#tiptip_holder { + display: none; + z-index: 8675309; + position: absolute; + top: 0; + + /*rtl:ignore*/ + left: 0; + + &.tip_top { + padding-bottom: 5px; + + #tiptip_arrow_inner { + margin-top: -7px; + margin-left: -6px; + border-top-color: #333; + } + } + + &.tip_bottom { + padding-top: 5px; + + #tiptip_arrow_inner { + margin-top: -5px; + margin-left: -6px; + border-bottom-color: #333; + } + } + + &.tip_right { + padding-left: 5px; + + #tiptip_arrow_inner { + margin-top: -6px; + margin-left: -5px; + border-right-color: #333; + } + } + + &.tip_left { + padding-right: 5px; + + #tiptip_arrow_inner { + margin-top: -6px; + margin-left: -7px; + border-left-color: #333; + } + } +} + +#tiptip_content, +.chart-tooltip, +.wc_error_tip { + color: #fff; + font-size: 0.8em; + max-width: 150px; + background: #333; + text-align: center; + border-radius: 3px; + padding: 0.618em 1em; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + + code { + padding: 1px; + background: #888; + } +} + +#tiptip_arrow, +#tiptip_arrow_inner { + position: absolute; + border-color: transparent; + border-style: solid; + border-width: 6px; + height: 0; + width: 0; +} + +/*rtl:raw: + #tiptip_arrow { + right: 50%; + margin-right: -6px; + } + */ + +.wc_error_tip { + max-width: 20em; + line-height: 1.8em; + position: absolute; + white-space: normal; + background: #d82223; + margin: 1.5em 1px 0 -1em; + z-index: 9999999; + + &::after { + content: ""; + display: block; + border: 8px solid #d82223; + border-right-color: transparent; + border-left-color: transparent; + border-top-color: transparent; + position: absolute; + top: -3px; + left: 50%; + margin: -1em 0 0 -3px; + } +} + +/** + * Date picker + */ +img.ui-datepicker-trigger { + vertical-align: middle; + margin-top: -1px; + cursor: pointer; +} + +.woocommerce_options_panel img.ui-datepicker-trigger, +.wc-metabox-content img.ui-datepicker-trigger { + float: left; + margin-right: 8px; + margin-top: 4px; + margin-left: 4px; +} + +#ui-datepicker-div { + display: none; +} + +/** + * Reports + */ +.woocommerce-reports-remove-filter { + color: red; + text-decoration: none; +} + +.woocommerce-reports-wrap, +.woocommerce-reports-wide { + + &.woocommerce-reports-wrap { + margin-left: 300px; + padding-top: 18px; + } + + &.halved { + margin: 0; + overflow: hidden; + zoom: 1; + } + + .widefat th { + padding: 7px; + } + + .widefat td { + vertical-align: top; + padding: 7px; + + .description { + margin: 4px 0 0; + } + } + + .postbox { + + &::after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; + } + + h3 { + cursor: default !important; + } + + .inside { + padding: 10px; + margin: 0 !important; + } + + div.stats_range, + h3.stats_range { + border-bottom-color: #dfdfdf; + margin: 0; + padding: 0 !important; + + .export_csv { + float: right; + line-height: 26px; + border-left: 1px solid #dfdfdf; + padding: 10px; + display: block; + text-decoration: none; + + &::before { + + @include iconbeforedashicons("\f346"); + margin-right: 4px; + } + } + + ul { + list-style: none outside; + margin: 0; + padding: 0; + zoom: 1; + background: #f5f5f5; + border-bottom: 1px solid #ccc; + + &::before, + &::after { + content: " "; + display: table; + } + + &::after { + clear: both; + } + + li { + float: left; + margin: 0; + padding: 0; + line-height: 26px; + font-weight: bold; + font-size: 14px; + + a { + border-right: 1px solid #dfdfdf; + padding: 10px; + display: block; + text-decoration: none; + } + + &.active { + background: #fff; + box-shadow: 0 4px 0 0 #fff; + + a { + color: #777; + } + } + + &.custom { + padding: 9px 10px; + vertical-align: middle; + + form, + div { + display: inline; + margin: 0; + + input.range_datepicker { + padding: 0; + margin: 0 10px 0 0; + background: transparent; + border: 0; + color: #777; + text-align: center; + box-shadow: none; + + &.from { + margin-right: 0; + } + } + } + } + } + } + } + + .chart-with-sidebar { + padding: 12px 12px 12px 249px; + margin: 0 !important; + + .chart-sidebar { + width: 225px; + margin-left: -237px; + float: left; + } + } + + .chart-widgets { + margin: 0; + padding: 0; + + li.chart-widget { + margin: 0 0 1em; + background: #fafafa; + border: 1px solid #dfdfdf; + + &::after { + content: "."; + display: block; + height: 0; + clear: both; + visibility: hidden; + } + + h4 { + background: #fff; + border: 1px solid #dfdfdf; + border-left-width: 0; + border-right-width: 0; + padding: 10px; + margin: 0; + color: $blue; + border-top-width: 0; + background-image: linear-gradient(to top, #ececec, #f9f9f9); + + &.section_title:hover { + color: $red; + } + } + + .section_title { + cursor: pointer; + + span { + display: block; + + &::after { + + @include iconafter("\e035"); + float: right; + font-size: 0.9em; + line-height: 1.618; + } + } + + &.open { + color: #333; + + span::after { + display: none; + } + } + } + + .section { + border-bottom: 1px solid #dfdfdf; + + .select2-container { + width: 100% !important; + } + + &:last-of-type { + border-radius: 0 0 3px 3px; + } + } + + table { + width: 100%; + + td { + padding: 7px 10px; + vertical-align: top; + border-top: 1px solid #e5e5e5; + line-height: 1.4em; + } + + tr:first-child td { + border-top: 0; + } + + td.count { + background: #f5f5f5; + } + + td.name { + max-width: 175px; + + a { + word-wrap: break-word; + } + } + + td.sparkline { + vertical-align: middle; + } + + .wc_sparkline { + width: 32px; + height: 1em; + display: block; + float: right; + } + + tr.active td { + background: #f5f5f5; + } + } + + form, + p { + margin: 0; + padding: 10px; + + .submit { + margin-top: 10px; + } + } + + #product_ids { + width: 100%; + } + + .select_all, + .select_none { + float: right; + color: #999; + margin-left: 4px; + margin-top: 10px; + } + + .description { + margin-left: 0.5em; + font-weight: normal; + opacity: 0.8; + } + } + } + + .chart-legend { + list-style: none outside; + margin: 0 0 1em; + padding: 0; + border: 1px solid #dfdfdf; + border-right-width: 0; + border-bottom-width: 0; + background: #fff; + + li { + border-right: 5px solid #aaa; + color: #aaa; + padding: 1em; + display: block; + margin: 0; + transition: all ease 0.5s; + box-shadow: inset 0 -1px 0 0 #dfdfdf; + + strong { + font-size: 1.618em; + line-height: 1.2em; + color: #464646; + font-weight: normal; + display: block; + font-family: + "HelveticaNeue-Light", + "Helvetica Neue Light", + "Helvetica Neue", + sans-serif; + + del { + color: #e74c3c; + font-weight: normal; + } + } + + &:hover { + box-shadow: + inset 0 -1px 0 0 #dfdfdf, + inset 300px 0 0 rgba(156, 93, 144, 0.1); + border-right: 5px solid #9c5d90 !important; + padding-left: 1.5em; + color: #9c5d90; + } + } + } + + .pie-chart-legend { + margin: 12px 0 0; + overflow: hidden; + + li { + float: left; + margin: 0; + padding: 6px 0 0; + border-top: 4px solid #999; + text-align: center; + box-sizing: border-box; + width: 50%; + } + } + + .stat { + font-size: 1.5em !important; + font-weight: 700; + text-align: center; + } + + .chart-placeholder { + width: 100%; + height: 650px; + overflow: hidden; + position: relative; + } + + .chart-prompt { + line-height: 650px; + margin: 0; + color: #999; + font-size: 1.2em; + font-style: italic; + text-align: center; + } + + .chart-container { + background: #fff; + padding: 12px; + position: relative; + border: 1px solid #dfdfdf; + border-radius: 3px; + } + + .main .chart-legend { + margin-top: 12px; + + li { + border-right: 0; + margin: 0 8px 0 0; + float: left; + border-top: 4px solid #aaa; + } + } + } + + .woocommerce-reports-main { + float: left; + min-width: 100%; + + table td { + padding: 9px; + } + } + + .woocommerce-reports-sidebar { + display: inline; + width: 281px; + margin-left: -300px; + clear: both; + float: left; + } + + .woocommerce-reports-left { + width: 49.5%; + float: left; + } + + .woocommerce-reports-right { + width: 49.5%; + float: right; + } +} + +.woocommerce-wide-reports-wrap { + padding-bottom: 11px; + + .widefat { + + .export-data { + float: right; + } + + th, + td { + vertical-align: middle; + padding: 7px; + } + } +} + +form.report_filters { + + p { + vertical-align: middle; + } + + label, + input, + div { + vertical-align: middle; + } +} + +.chart-tooltip { + position: absolute; + display: none; + line-height: 1; +} + +table.bar_chart { + width: 100%; + + thead th { + text-align: left; + color: #ccc; + padding: 6px 0; + } + + tbody { + + th { + padding: 6px 0; + width: 25%; + text-align: left !important; + font-weight: normal !important; + border-bottom: 1px solid #fee; + } + + td { + text-align: right; + line-height: 24px; + padding: 6px 6px 6px 0; + border-bottom: 1px solid #fee; + + span { + color: #8a4b75; + display: block; + } + + span.alt { + color: #47a03e; + margin-top: 6px; + } + } + + td.bars { + position: relative; + text-align: left; + padding: 6px 6px 6px 0; + border-bottom: 1px solid #fee; + + span, + a { + text-decoration: none; + clear: both; + background: #8a4b75; + float: left; + display: block; + line-height: 24px; + height: 24px; + border-radius: 3px; + } + + span.alt { + clear: both; + background: #47a03e; + + span { + margin: 0; + color: #c5dec2 !important; + text-shadow: 0 1px 0 #47a03e; + background: transparent; + } + } + } + } +} + +.post-type-shop_order .woocommerce-BlankState-message::before { + + @include icon("\e01d"); +} + +.post-type-shop_coupon .woocommerce-BlankState-message::before { + + @include icon("\e600"); +} + +.post-type-product .woocommerce-BlankState-message::before { + + @include icon("\e006"); +} + +.woocommerce-BlankState--api .woocommerce-BlankState-message::before { + + @include icon("\e01c"); +} + +.woocommerce-BlankState--webhooks .woocommerce-BlankState-message::before { + + @include icon("\e01b"); +} + +.woocommerce-BlankState { + text-align: center; + padding: 5em 0 0; + + .woocommerce-BlankState-message { + color: #aaa; + margin: 0 auto 1.5em; + line-height: 1.5em; + font-size: 1.2em; + max-width: 500px; + + &::before { + color: #ddd; + text-shadow: + 0 -1px 1px rgba(0, 0, 0, 0.2), + 0 1px 0 rgba(255, 255, 255, 0.8); + font-size: 8em; + display: block; + position: relative !important; + top: auto; + left: auto; + line-height: 1em; + margin: 0 0 0.1875em; + } + } + + .woocommerce-BlankState-cta { + font-size: 1.2em; + padding: 0.75em 1.5em; + margin: 0 0.25em; + height: auto; + display: inline-block !important; + } +} + +.post-type-product .woocommerce-BlankState, +.post-type-shop_order .woocommerce-BlankState { + max-width: 764px; + text-align: center; + margin: auto; + + .woocommerce-BlankState-message { + color: #444; + font-size: 1.5em; + margin: 0 auto 1em; + } + + .woocommerce-BlankState-message::before { + font-size: 120px; + } + + .woocommerce-BlankState-buttons { + margin-bottom: 4em; + } +} + +.post-type-product { + + #wp-pointer-2 .wp-pointer-arrow { + left: 240px; + } + + #wp-pointer-3 .wp-pointer-arrow, + #wp-pointer-4 .wp-pointer-arrow { + left: 46%; + } +} + +/** + * Small screen optimisation + */ +@media only screen and (max-width: 1280px) { + + #order_data { + + .order_data_column { + width: 48%; + + &:first-child { + width: 100%; + } + } + } + + .woocommerce_options_panel { + + .description { + display: block; + clear: both; + margin-left: 0; + } + + .short, + input[type="text"].short, + input[type="email"].short, + input[type="number"].short, + input[type="password"].short, + .dimensions_field .wrap { + width: 80%; + } + } + + .woocommerce_variations, + .woocommerce_options_panel { + + .downloadable_files { + padding: 0; + clear: both; + + label { + position: static; + } + + table { + margin: 0 12px 24px; + width: 94%; + + .sort { + visibility: hidden; + } + } + } + + .woocommerce_variable_attributes .downloadable_files table { + margin: 0 0 1em; + width: 100%; + } + } +} + +/** + * Optimisation for screens 900px and smaller + */ +@media only screen and (max-width: 900px) { + + #woocommerce-coupon-data ul.coupon_data_tabs, + #woocommerce-product-data ul.product_data_tabs, + #woocommerce-product-data .wc-tabs-back { + width: 10%; + } + + #woocommerce-coupon-data .wc-metaboxes-wrapper, + #woocommerce-coupon-data .woocommerce_options_panel, + #woocommerce-product-data .wc-metaboxes-wrapper, + #woocommerce-product-data .woocommerce_options_panel { + width: 90%; + } + + #woocommerce-coupon-data ul.coupon_data_tabs li a, + #woocommerce-product-data ul.product_data_tabs li a { + position: relative; + text-indent: -999px; + padding: 10px; + + &::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + text-indent: 0; + text-align: center; + line-height: 40px; + width: 100%; + height: 40px; + } + } +} + +/** + * Optimisation for screens 782px and smaller + */ +@media only screen and (max-width: 782px) { + + #wp-excerpt-media-buttons a { + font-size: 16px; + line-height: 37px; + height: 39px; + padding: 0 20px 0 15px; + } + + #wp-excerpt-editor-tools { + padding-top: 20px; + padding-right: 15px; + overflow: hidden; + margin-bottom: -1px; + } + + #woocommerce-product-data .checkbox { + width: 25px; + } + + .variations-pagenav { + float: none; + text-align: center; + font-size: 18px; + + .displaying-num { + font-size: 16px; + } + + a { + padding: 8px 20px 11px; + font-size: 18px; + } + + select { + padding: 0 20px; + } + } + + .variations-defaults { + float: none; + text-align: center; + margin-top: 10px; + } + + .post-type-product { + + .wp-list-table { + + .column-thumb { + display: none; + text-align: left; + padding-bottom: 0; + + &::before { + display: none !important; + } + + img { + max-width: 32px; + } + } + + .is-expanded td:not(.hidden) { + overflow: visible; + } + + .toggle-row { + top: -28px; + } + } + } + + .post-type-shop_order { + + .wp-list-table { + + .column-customer_message, + .column-order_notes { + text-align: inherit; + } + + .column-order_notes .note-on { + font-size: 1.3em; + margin: 0; + } + + .is-expanded td:not(.hidden) { + overflow: visible; + } + + .toggle-row { + top: -15px; + } + } + } +} + +@media only screen and (max-width: 500px) { + + .woocommerce_options_panel label, + .woocommerce_options_panel legend { + float: none; + width: auto; + display: block; + margin: 0; + } + + .woocommerce_options_panel fieldset.form-field, + .woocommerce_options_panel p.form-field { + padding: 5px 20px !important; + } + + .addons-wcs-banner-block { + flex-direction: column; + } + + .wc-addons-wrap { + + .addons-wcs-banner-block { + padding: 40px; + } + + .addons-wcs-banner-block-image { + padding: 1em; + text-align: center; + width: 100%; + padding: 2em 0; + margin: 0; + + .addons-img { + margin: 0; + } + } + } +} + +/** + * Backbone modal dialog + */ +.wc-backbone-modal { + + * { + box-sizing: border-box; + } + + .wc-backbone-modal-content { + position: fixed; + background: #fff; + z-index: 100000; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + max-width: 100%; + min-width: 500px; + + article { + overflow: auto; + } + } + + &.wc-backbone-modal-shipping-method-settings .wc-backbone-modal-content { + width: 75%; + min-width: 500px; + } + + .select2-container { + width: 100% !important; + } +} + +@media screen and (max-width: 782px) { + + .wc-backbone-modal .wc-backbone-modal-content { + width: 100%; + height: 100%; + min-width: 100%; + } +} + +.wc-backbone-modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + min-height: 360px; + background: #000; + opacity: 0.7; + z-index: 99900; +} + +.wc-backbone-modal-main { + padding-bottom: 55px; + + header, + article { + display: block; + position: relative; + } + + .wc-backbone-modal-header { + height: auto; + background: #fcfcfc; + padding: 1em 1.5em; + border-bottom: 1px solid #ddd; + + h1 { + margin: 0; + font-size: 18px; + font-weight: 700; + line-height: 1.5em; + } + + .modal-close-link { + cursor: pointer; + color: #777; + height: 54px; + width: 54px; + padding: 0; + position: absolute; + top: 0; + right: 0; + text-align: center; + border: 0; + border-left: 1px solid #ddd; + background-color: transparent; + transition: color 0.1s ease-in-out, background 0.1s ease-in-out; + + &::before { + font: normal 22px/50px "dashicons" !important; + color: #666; + display: block; + content: "\f335"; + font-weight: 300; + } + + &:hover, + &:focus { + background: #ddd; + border-color: #ccc; + color: #000; + } + + &:focus { + outline: none; + } + } + } + + article { + padding: 1.5em; + + p { + margin: 1.5em 0; + } + + p:first-child { + margin-top: 0; + } + + p:last-child { + margin-bottom: 0; + } + + .pagination { + padding: 10px 0 0; + text-align: center; + } + + table.widefat { + margin: 0; + width: 100%; + border: 0; + box-shadow: none; + + thead th { + padding: 0 1em 1em 1em; + text-align: left; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + text-align: right; + } + } + + tbody td, + tbody th { + padding: 1em; + text-align: left; + vertical-align: middle; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + text-align: right; + } + + select, + .select2-container { + width: 100%; + } + } + } + } + + footer { + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + padding: 1em 1.5em; + background: #fcfcfc; + border-top: 1px solid #dfdfdf; + box-shadow: 0 -4px 4px -4px rgba(0, 0, 0, 0.1); + + .inner { + text-align: right; + line-height: 23px; + + .button { + margin-bottom: 0; + } + } + } +} + +/** + * Select2 elements. + */ +.select2-drop, +.select2-dropdown { + z-index: 999999 !important; +} + +.select2-results { + line-height: 1.5em; + + .select2-results__option, + .select2-results__group { + margin: 0; + padding: 8px; + } + + .description { + display: block; + color: #999; + padding-top: 4px; + } +} + +.select2-dropdown { + border-color: #ddd; +} + +.select2-dropdown--below { + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} + +.select2-dropdown--above { + box-shadow: 0 -1px 1px rgba(0, 0, 0, 0.1); +} + +.select2-container { + + .select2-selection__rendered.ui-sortable li { + cursor: move; + } + + .select2-selection { + border-color: #ddd; + } + + .select2-search__field { + min-width: 150px; + } + + .select2-selection--single { + height: 40px; + + .select2-selection__rendered { + line-height: 40px; + padding-right: 24px; + } + + .select2-selection__arrow { + right: 3px; + height: 36px; + } + } + + .select2-selection--multiple { + min-height: 28px; + border-radius: 0; + line-height: 1.5; + + li { + margin: 0; + } + + .select2-selection__choice { + padding: 2px 6px; + + .description { + display: none; + } + } + } + + .select2-selection__clear { + color: #999; + margin-top: -1px; + z-index: 1; + } + + .select2-search--inline .select2-search__field { + font-family: inherit; + font-size: inherit; + font-weight: inherit; + padding: 3px 0; + } +} + +.woocommerce table.form-table .select2-container { + min-width: 400px !important; +} + +.wc-wp-version-gte-53 { + + .select2-results { + + .select2-results__option, + .select2-results__group { + + &:focus { + outline: none; + } + } + } + + .select2-dropdown { + border-color: #007cba; + + &::after { + position: absolute; + left: 0; + right: 0; + height: 1px; + background: #fff; + content: ""; + } + } + + .select2-dropdown--below { + box-shadow: 0 0 0 1px #007cba, 0 2px 1px rgba(0, 0, 0, 0.1); + + &::after { + top: -1px; + } + } + + .select2-dropdown--above { + box-shadow: 0 0 0 1px #007cba, 0 -2px 1px rgba(0, 0, 0, 0.1); + + &::after { + bottom: -1px; + } + } + + .select2-container { + + @media only screen and (max-width: 782px) { + font-size: 16px; + } + + &:focus { + outline: none; + } + + .select2-selection--single { + height: 30px; + border-color: #7e8993; + + @media only screen and (max-width: 782px) { + height: 40px; + } + + &:focus { + outline: none; + } + + .select2-selection__rendered { + line-height: 28px; + + @media only screen and (max-width: 782px) { + line-height: 38px; + } + + &:hover { + color: #007cba; + } + } + + .select2-selection__arrow { + right: 1px; + height: 28px; + width: 23px; + background: + url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E") + no-repeat right 5px top 55%; + background-size: 16px 16px; + + @media only screen and (max-width: 782px) { + height: 38px; + } + + b { + display: none; + } + } + } + + &.select2-container--focus .select2-selection--single, + &.select2-container--open .select2-selection--single, + &.select2-container--open .select2-selection--multiple { + border-color: #007cba; + box-shadow: 0 0 0 1px #007cba; + } + + .select2-selection--multiple { + min-height: 30px; + border-color: #7e8993; + border-radius: 4px; + } + + .select2-search--inline .select2-search__field { + padding: 0 0 0 3px; + min-height: 28px; + } + } + + .woocommerce table.form-table .select2-container { + + @media only screen and (max-width: 782px) { + min-width: 100% !important; + } + } +} + +.wc-wp-version-gte-55 { + + #woocommerce-product-data { + + .hndle { + display: block; + line-height: 24px; + + .type_box { + display: inline; + line-height: inherit; + vertical-align: baseline; + } + } + } +} + +/** + * Select2 colors for built-in admin color themes. + */ +.admin-color { + $wp_admin_colors: ( + blue: #096484, + coffee: #c7a589, + ectoplasm: #a3b745, + midnight: #e14d43, + ocean: #9ebaa0, + sunrise: #dd823b, + light: #04a4cc, + ); + + @each $name, $color in $wp_admin_colors { + &-#{$name}.wc-wp-version-gte-53 { + + .select2-dropdown { + border-color: $color; + } + + .select2-dropdown--below { + box-shadow: 0 0 0 1px $color, 0 2px 1px rgba(0, 0, 0, 0.1); + } + + .select2-dropdown--above { + box-shadow: 0 0 0 1px $color, 0 -2px 1px rgba(0, 0, 0, 0.1); + } + + .select2-selection--single .select2-selection__rendered:hover { + color: $color; + } + + .select2-container.select2-container--focus + .select2-selection--single, + .select2-container.select2-container--open + .select2-selection--single, + .select2-container.select2-container--open + .select2-selection--multiple { + border-color: $color; + box-shadow: 0 0 0 1px $color; + } + + .select2-container--default + .select2-results__option--highlighted[aria-selected], + .select2-container--default + .select2-results__option--highlighted[data-selected] { + background-color: $color; + } + } + } +} + +.post-type-product .tablenav, +.post-type-shop_order .tablenav { + + .actions { + overflow: visible; + } + + select, + input { + height: 32px; + } + + .select2-container { + float: left; + width: 240px !important; + font-size: 14px; + vertical-align: middle; + margin: 1px 6px 4px 1px; + } +} + +.woocommerce-progress-form-wrapper, +.woocommerce-exporter-wrapper, +.woocommerce-importer-wrapper { + text-align: center; + max-width: 700px; + margin: 40px auto; + + .error { + text-align: left; + } + + .wc-progress-steps { + padding: 0 0 24px; + margin: 0; + list-style: none outside; + overflow: hidden; + color: #ccc; + width: 100%; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + + li { + width: 25%; + float: left; + padding: 0 0 0.8em; + margin: 0; + text-align: center; + position: relative; + border-bottom: 4px solid #ccc; + line-height: 1.4em; + } + + li::before { + content: ""; + border: 4px solid #ccc; + border-radius: 100%; + width: 4px; + height: 4px; + position: absolute; + bottom: 0; + left: 50%; + margin-left: -6px; + margin-bottom: -8px; + background: #fff; + } + + li.active { + border-color: #a16696; + color: #a16696; + + &::before { + border-color: #a16696; + } + } + + li.done { + border-color: #a16696; + color: #a16696; + + &::before { + border-color: #a16696; + background: #a16696; + } + } + } + + .button { + font-size: 1.25em; + padding: 0.5em 1em !important; + line-height: 1.5em !important; + margin-right: 0.5em; + margin-bottom: 2px; + height: auto !important; + border-radius: 4px; + background-color: #bb77ae; + border-color: #a36597; + -webkit-box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0 #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + text-shadow: + 0 -1px 1px #a36597, + 1px 0 1px #a36597, + 0 1px 1px #a36597, + -1px 0 1px #a36597; + margin: 0; + opacity: 1; + + &:hover, + &:focus, + &:active { + background: #a36597; + border-color: #a36597; + -webkit-box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0 #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + } + } + + .error .button { + font-size: 1em; + } + + .wc-actions { + overflow: hidden; + border-top: 1px solid #eee; + margin: 0; + padding: 23px 24px 24px; + line-height: 3em; + + .button { + float: right; + } + + .woocommerce-importer-toggle-advanced-options { + color: #999; + } + } + + .woocommerce-exporter, + .woocommerce-importer, + .wc-progress-form-content { + background: #fff; + overflow: hidden; + padding: 0; + margin: 0 0 16px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); + color: #555; + text-align: left; + + header { + border-bottom: 1px solid #eee; + margin: 0; + padding: 24px 24px 0; + } + + section { + padding: 24px 24px 0; + } + + h2 { + margin: 0 0 24px; + color: #555; + font-size: 24px; + font-weight: normal; + line-height: 1em; + } + + p { + font-size: 1em; + line-height: 1.75em; + font-size: 16px; + color: #555; + margin: 0 0 24px; + } + + .form-row { + margin-top: 24px; + } + + .spinner { + display: none; + } + + .woocommerce-importer-options th, + .woocommerce-importer-options td, + .woocommerce-exporter-options th, + .woocommerce-exporter-options td { + vertical-align: top; + line-height: 1.75em; + padding: 0 0 24px 0; + + label { + color: #555; + font-weight: normal; + } + + input[type="checkbox"] { + margin: 0 4px 0 0; + padding: 7px; + } + + input[type="text"], + input[type="number"] { + padding: 7px; + height: auto; + margin: 0; + } + + .woocommerce-importer-file-url-field-wrapper { + border: 1px solid #ddd; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.07); + background-color: #fff; + color: #32373c; + outline: 0; + line-height: 1; + display: block; + + code { + background: none; + font-size: smaller; + padding: 0; + margin: 0; + color: #999; + padding: 7px 0 0 7px; + display: inline-block; + } + + input { + font-family: Consolas, Monaco, monospace; + border: 0; + margin: 0; + outline: 0; + box-shadow: none; + display: inline-block; + min-width: 100%; + } + } + } + + .woocommerce-exporter-options th, + .woocommerce-importer-options th { + width: 35%; + padding-right: 20px; + } + + progress { + width: 100%; + height: 42px; + margin: 0 auto 24px; + display: block; + -webkit-appearance: none; + border: none; + display: none; + background: #f5f5f5; + border: 2px solid #eee; + border-radius: 4px; + padding: 0; + box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.2); + } + + progress::-webkit-progress-bar { + background: transparent none; + border: 0; + border-radius: 4px; + padding: 0; + box-shadow: none; + } + + progress::-webkit-progress-value { + border-radius: 3px; + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); + background: #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; + transition: width 1s ease; + } + + progress::-moz-progress-bar { + border-radius: 3px; + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); + background: #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; + transition: width 1s ease; + } + + progress::-ms-fill { + border-radius: 3px; + box-shadow: inset 0 1px 1px 0 rgba(255, 255, 255, 0.4); + background: #a46497; + background: linear-gradient(to bottom, #a46497, #66405f), #a46497; + transition: width 1s ease; + } + + &.woocommerce-exporter__exporting, + &.woocommerce-importer__importing { + + .spinner { + display: block; + } + + progress { + display: block; + } + + .wc-actions, + .woocommerce-exporter-options { + display: none; + } + } + + .wc-importer-mapping-table-wrapper, + .wc-importer-error-log { + padding: 0; + } + + .wc-importer-mapping-table, + .wc-importer-error-log-table { + margin: 0; + border: 0; + box-shadow: none; + width: 100%; + table-layout: fixed; + + td, + th { + border: 0; + padding: 12px; + vertical-align: middle; + word-wrap: break-word; + + select { + width: 100%; + } + } + + tbody tr:nth-child(odd) td, + tbody tr:nth-child(odd) th { + background: #fbfbfb; + } + + th { + font-weight: bold; + } + + td:first-child, + th:first-child { + padding-left: 24px; + } + + td:last-child, + th:last-child { + padding-right: 24px; + } + + .wc-importer-mapping-table-name { + width: 50%; + + .description { + color: #999; + margin-top: 4px; + display: block; + + code { + background: none; + padding: 0; + white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */ + word-wrap: break-word; /* IE */ + word-break: break-all; + } + } + } + } + + .woocommerce-importer-done { + text-align: center; + padding: 48px 24px; + font-size: 1.5em; + line-height: 1.75em; + + &::before { + + @include icon("\e015"); + color: #a16696; + position: static; + font-size: 100px; + display: block; + float: none; + margin: 0 0 24px; + } + } + } +} + +.wc-pointer { + + .wc-pointer-buttons { + + .close { + float: left; + margin: 6px 0 0 15px; + } + } +} + +.wc-quick-edit-warning { + color: darkred; + font-weight: bold; +} + +@media screen and (min-width: 600px) { + + .wc-addons-wrap { + + .marketplace-header { + padding-left: 84px; + } + + .storefront { + + h2 { + margin-top: 0; + } + + img { + float: left; + margin: 0 16px 0 auto; + width: 278px; + } + } + } +} diff --git a/assets/css/auth-rtl.css b/assets/css/auth-rtl.css new file mode 100644 index 0000000..8b2575a --- /dev/null +++ b/assets/css/auth-rtl.css @@ -0,0 +1 @@ +body{background:#f1f1f1;box-shadow:none;margin:100px auto 24px;padding:0}#wc-logo{border:0;margin:0 0 24px;padding:0;text-align:center}#wc-logo img{max-width:50%}.wc-auth-content{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.13);overflow:hidden;padding:24px 24px 0;zoom:1}.wc-auth-content h1,.wc-auth-content h2,.wc-auth-content h3,.wc-auth-content table{border:0;clear:none;color:#666;margin:0 0 24px;padding:0}.wc-auth-content p,.wc-auth-content ul{color:#666;font-size:1em;line-height:1.75em;margin:0 0 24px}.wc-auth-content p{padding:0}.wc-auth-content a{color:#a16696}.wc-auth-content a:focus,.wc-auth-content a:hover{color:#111}.wc-auth-content .wc-auth-login label{color:#999;display:block;margin-bottom:.5em}.wc-auth-content .wc-auth-login input{box-sizing:border-box;font-size:1.3em;padding:.5em;width:100%}.wc-auth-content .wc-auth-login .wc-auth-actions{padding:0}.wc-auth-content .wc-auth-login .wc-auth-actions .wc-auth-login-button{float:none;width:100%}.wc-auth-permissions{list-style:disc inside;padding:0}.wc-auth-permissions li{font-size:1em}.wc-auth-logged-in-as{background:#f5f5f5;border-bottom:2px solid #eee;line-height:70px;margin:0 0 24px;padding:0 0 0 1em}.wc-auth-logged-in-as p{margin:0;line-height:70px}.wc-auth-logged-in-as img{float:right;height:70px;margin:0 0 0 1em}.wc-auth-logged-in-as .wc-auth-logout{float:left}.wc-auth .wc-auth-actions{overflow:hidden;padding-right:24px}.wc-auth .wc-auth-actions .button{background:#f7f7f7;border-bottom-width:2px;border:1px solid #d7d7d7;box-sizing:border-box;color:#777;float:left;font-size:1.25em;height:auto;line-height:1em;padding:1em 2em;text-align:center;width:50%}.wc-auth .wc-auth-actions .button:focus,.wc-auth .wc-auth-actions .button:hover{background:#fcfcfc}.wc-auth .wc-auth-actions .button-primary{background:#ad6ea1;border-color:#a16696;box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);color:#fff;float:left;opacity:1;text-shadow:0 -1px 1px #8a4f7f,-1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,1px 0 1px #8a4f7f}.wc-auth .wc-auth-actions .button-primary:focus,.wc-auth .wc-auth-actions .button-primary:hover{background:#b472a8;color:#fff}.wc-auth .wc-auth-actions .wc-auth-approve{float:left}.wc-auth .wc-auth-actions .wc-auth-deny{float:right;margin-right:-24px} \ No newline at end of file diff --git a/assets/css/auth.css b/assets/css/auth.css new file mode 100644 index 0000000..d119704 --- /dev/null +++ b/assets/css/auth.css @@ -0,0 +1 @@ +body{background:#f1f1f1;box-shadow:none;margin:100px auto 24px;padding:0}#wc-logo{border:0;margin:0 0 24px;padding:0;text-align:center}#wc-logo img{max-width:50%}.wc-auth-content{background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.13);overflow:hidden;padding:24px 24px 0;zoom:1}.wc-auth-content h1,.wc-auth-content h2,.wc-auth-content h3,.wc-auth-content table{border:0;clear:none;color:#666;margin:0 0 24px;padding:0}.wc-auth-content p,.wc-auth-content ul{color:#666;font-size:1em;line-height:1.75em;margin:0 0 24px}.wc-auth-content p{padding:0}.wc-auth-content a{color:#a16696}.wc-auth-content a:focus,.wc-auth-content a:hover{color:#111}.wc-auth-content .wc-auth-login label{color:#999;display:block;margin-bottom:.5em}.wc-auth-content .wc-auth-login input{box-sizing:border-box;font-size:1.3em;padding:.5em;width:100%}.wc-auth-content .wc-auth-login .wc-auth-actions{padding:0}.wc-auth-content .wc-auth-login .wc-auth-actions .wc-auth-login-button{float:none;width:100%}.wc-auth-permissions{list-style:disc inside;padding:0}.wc-auth-permissions li{font-size:1em}.wc-auth-logged-in-as{background:#f5f5f5;border-bottom:2px solid #eee;line-height:70px;margin:0 0 24px;padding:0 1em 0 0}.wc-auth-logged-in-as p{margin:0;line-height:70px}.wc-auth-logged-in-as img{float:left;height:70px;margin:0 1em 0 0}.wc-auth-logged-in-as .wc-auth-logout{float:right}.wc-auth .wc-auth-actions{overflow:hidden;padding-left:24px}.wc-auth .wc-auth-actions .button{background:#f7f7f7;border-bottom-width:2px;border:1px solid #d7d7d7;box-sizing:border-box;color:#777;float:right;font-size:1.25em;height:auto;line-height:1em;padding:1em 2em;text-align:center;width:50%}.wc-auth .wc-auth-actions .button:focus,.wc-auth .wc-auth-actions .button:hover{background:#fcfcfc}.wc-auth .wc-auth-actions .button-primary{background:#ad6ea1;border-color:#a16696;box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 0 rgba(0,0,0,.15);color:#fff;float:right;opacity:1;text-shadow:0 -1px 1px #8a4f7f,1px 0 1px #8a4f7f,0 1px 1px #8a4f7f,-1px 0 1px #8a4f7f}.wc-auth .wc-auth-actions .button-primary:focus,.wc-auth .wc-auth-actions .button-primary:hover{background:#b472a8;color:#fff}.wc-auth .wc-auth-actions .wc-auth-approve{float:right}.wc-auth .wc-auth-actions .wc-auth-deny{float:left;margin-left:-24px} \ No newline at end of file diff --git a/assets/css/auth.scss b/assets/css/auth.scss new file mode 100644 index 0000000..2358f8e --- /dev/null +++ b/assets/css/auth.scss @@ -0,0 +1,149 @@ +body { + background: #f1f1f1; + box-shadow: none; + margin: 100px auto 24px; + padding: 0; +} + +#wc-logo { + border: 0; + margin: 0 0 24px; + padding: 0; + text-align: center; + + img { + max-width: 50%; + } +} + +.wc-auth-content { + background: #fff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.13); + overflow: hidden; + padding: 24px 24px 0; + zoom: 1; + + h1, h2, h3, table { + border: 0; + clear: none; + color: #666; + margin: 0 0 24px; + padding: 0; + } + + p, ul { + color: #666; + font-size: 1em; + line-height: 1.75em; + margin: 0 0 24px; + } + + p { + padding: 0; + } + + a { + color: #a16696; + &:hover, &:focus { + color: #111; + } + } + + .wc-auth-login { + label { + color: #999; + display: block; + margin-bottom: 0.5em; + } + + input { + box-sizing: border-box; + font-size: 1.3em; + padding: 0.5em; + width: 100%; + } + + .wc-auth-actions { + padding: 0; + + .wc-auth-login-button { + float: none; + width: 100%; + } + } + } +} +.wc-auth-permissions { + list-style: disc inside; + padding: 0; + + li { + font-size: 1em; + } +} +.wc-auth-logged-in-as { + background: #f5f5f5; + border-bottom: 2px solid #eee; + line-height: 70px; + margin: 0 0 24px; + padding: 0 1em 0 0; + + p { + margin: 0; + line-height: 70px; + } + + img { + float: left; + height: 70px; + margin: 0 1em 0 0; + } + + .wc-auth-logout { + float: right; + } +} +.wc-auth .wc-auth-actions { + overflow: hidden; + padding-left: 24px; + + .button { + background: #f7f7f7; + border-bottom-width: 2px; + border: 1px solid #d7d7d7; + box-sizing: border-box; + color: #777; + float: right; + font-size: 1.25em; + height: auto; + line-height: 1em; + padding: 1em 2em; + text-align: center; + width: 50%; + + &:hover, &:focus { + background: #fcfcfc; + } + } + .button-primary { + background: #ad6ea1; + border-color: #a16696; + box-shadow: inset 0 1px 0 rgba( 255, 255, 255, 0.2 ), 0 1px 0 rgba( 0, 0, 0, 0.15 ); + color: #fff; + float: right; + opacity: 1; + text-shadow: 0 -1px 1px #8a4f7f, 1px 0 1px #8a4f7f, 0 1px 1px #8a4f7f, -1px 0 1px #8a4f7f; + + &:hover, &:focus { + background: #b472a8; + color: #fff; + } + } + .wc-auth-approve { + float: right; + } + .wc-auth-deny { + float: left; + margin-left: -24px; + } +} diff --git a/assets/css/dashboard-rtl.css b/assets/css/dashboard-rtl.css new file mode 100644 index 0000000..3f97154 --- /dev/null +++ b/assets/css/dashboard-rtl.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--woocommerce:#a46497;--wc-green:#7ad03a;--wc-red:#a00;--wc-orange:#ffba00;--wc-blue:#2ea2cc;--wc-primary:#a46497;--wc-primary-text:white;--wc-secondary:#ebe9eb;--wc-secondary-text:#515151;--wc-highlight:#77a464;--wc-highligh-text:white;--wc-content-bg:#fff;--wc-subtext:#767676}@font-face{font-family:star;src:url(../fonts/star.eot);src:url(../fonts/star.eot?#iefix) format("embedded-opentype"),url(../fonts/star.woff) format("woff"),url(../fonts/star.ttf) format("truetype"),url(../fonts/star.svg#star) format("svg");font-weight:400;font-style:normal}@font-face{font-family:WooCommerce;src:url(../fonts/WooCommerce.eot);src:url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"),url(../fonts/WooCommerce.woff) format("woff"),url(../fonts/WooCommerce.ttf) format("truetype"),url(../fonts/WooCommerce.svg#WooCommerce) format("svg");font-weight:400;font-style:normal}ul.woocommerce_stats{overflow:hidden;zoom:1}ul.woocommerce_stats li{width:25%;padding:0 1em;text-align:center;float:right;font-size:.8em;border-right:1px solid #fff;border-left:1px solid #ececec;box-sizing:border-box}ul.woocommerce_stats li:first-child{border-right:0}ul.woocommerce_stats li:last-child{border-left:0}ul.woocommerce_stats strong{font-family:Georgia,"Times New Roman","Bitstream Charter",Times,serif;font-size:4em;line-height:1.2em;font-weight:400;text-align:center;display:block}#woocommerce_dashboard_status .inside{padding:0;margin:0}#woocommerce_dashboard_status .best-seller-this-month a strong{margin-left:48px}#woocommerce_dashboard_status .wc_status_list{overflow:hidden;margin:0}#woocommerce_dashboard_status .wc_status_list li{width:50%;float:right;padding:0;box-sizing:border-box;margin:0;border-top:1px solid #ececec;color:#aaa}#woocommerce_dashboard_status .wc_status_list li a{display:block;color:#aaa;padding:9px 12px;-webkit-transition:all ease .5s;transition:all ease .5s;position:relative;font-size:12px}#woocommerce_dashboard_status .wc_status_list li a .wc_sparkline{width:4em;height:2em;display:block;float:left;position:absolute;left:0;top:50%;margin-left:12px;margin-top:-1.25em}#woocommerce_dashboard_status .wc_status_list li a strong{font-size:18px;line-height:1.2em;font-weight:400;display:block;color:#21759b}#woocommerce_dashboard_status .wc_status_list li a:hover{color:#2ea2cc}#woocommerce_dashboard_status .wc_status_list li a:hover strong,#woocommerce_dashboard_status .wc_status_list li a:hover::before{color:#2ea2cc!important}#woocommerce_dashboard_status .wc_status_list li a::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";font-size:2em;position:relative;width:auto;line-height:1.2em;color:#464646;float:right;margin-left:12px;margin-bottom:12px}#woocommerce_dashboard_status .wc_status_list li:first-child{border-top:0}#woocommerce_dashboard_status .wc_status_list li.sales-this-month{width:100%}#woocommerce_dashboard_status .wc_status_list li.sales-this-month a::before{font-family:Dashicons;content:"\f185"}#woocommerce_dashboard_status .wc_status_list li.best-seller-this-month{width:100%}#woocommerce_dashboard_status .wc_status_list li.best-seller-this-month a::before{content:"\e006"}#woocommerce_dashboard_status .wc_status_list li.processing-orders{border-left:1px solid #ececec}#woocommerce_dashboard_status .wc_status_list li.processing-orders a::before{content:"\e011";color:#7ad03a}#woocommerce_dashboard_status .wc_status_list li.on-hold-orders a::before{content:"\e033";color:#999}#woocommerce_dashboard_status .wc_status_list li.low-in-stock{border-left:1px solid #ececec}#woocommerce_dashboard_status .wc_status_list li.low-in-stock a::before{content:"\e016";color:#ffba00}#woocommerce_dashboard_status .wc_status_list li.out-of-stock a::before{content:"\e013";color:#a00}#woocommerce_dashboard_recent_reviews li{line-height:1.5em;margin-bottom:12px}#woocommerce_dashboard_recent_reviews h4.meta{line-height:1.4;margin:-.2em 0 0 0;font-weight:400;color:#999}#woocommerce_dashboard_recent_reviews blockquote{padding:0;margin:0}#woocommerce_dashboard_recent_reviews .avatar{float:right;margin:0 0 5px 10px}#woocommerce_dashboard_recent_reviews .star-rating{float:left;overflow:hidden;position:relative;height:1.5em;line-height:1.5;margin-right:.5em;width:5.4em;font-family:WooCommerce!important}#woocommerce_dashboard_recent_reviews .star-rating::before{content:"\e021\e021\e021\e021\e021";color:#b3b2b2;float:right;top:0;right:0;position:absolute;letter-spacing:.1em}#woocommerce_dashboard_recent_reviews .star-rating span{overflow:hidden;float:right;top:0;right:0;position:absolute;padding-top:1.5em}#woocommerce_dashboard_recent_reviews .star-rating span::before{content:"\e020\e020\e020\e020\e020";top:0;position:absolute;right:0;letter-spacing:.1em;color:#9c5d90}#dash-right-now li.product-count a::before{font-family:WooCommerce;content:"\e01d"}#dashboard_activity #activity-widget #the-comment-list .review.comment-item .avatar{margin-left:12px;position:relative;top:0;float:right} \ No newline at end of file diff --git a/assets/css/dashboard-setup-rtl.css b/assets/css/dashboard-setup-rtl.css new file mode 100644 index 0000000..94d5d7c --- /dev/null +++ b/assets/css/dashboard-setup-rtl.css @@ -0,0 +1 @@ +.dashboard-widget-finish-setup .progress-wrapper{border:1px solid #757575;border-radius:16px;font-size:.9em;padding:2px 8px 2px 8px;display:inline-block;box-sizing:border-box}.dashboard-widget-finish-setup .progress-wrapper span{position:relative;top:-3px;color:#757575}.dashboard-widget-finish-setup .description div{margin-top:11px;float:right;width:70%}.dashboard-widget-finish-setup .description img{float:left;width:30%}.dashboard-widget-finish-setup .circle-progress{margin-top:1px;margin-right:-3px}.dashboard-widget-finish-setup .circle-progress circle{stroke:#f0f0f0;stroke-width:1px}.dashboard-widget-finish-setup .circle-progress .bar{stroke:#949494} \ No newline at end of file diff --git a/assets/css/dashboard-setup.css b/assets/css/dashboard-setup.css new file mode 100644 index 0000000..ead7881 --- /dev/null +++ b/assets/css/dashboard-setup.css @@ -0,0 +1 @@ +.dashboard-widget-finish-setup .progress-wrapper{border:1px solid #757575;border-radius:16px;font-size:.9em;padding:2px 8px 2px 8px;display:inline-block;box-sizing:border-box}.dashboard-widget-finish-setup .progress-wrapper span{position:relative;top:-3px;color:#757575}.dashboard-widget-finish-setup .description div{margin-top:11px;float:left;width:70%}.dashboard-widget-finish-setup .description img{float:right;width:30%}.dashboard-widget-finish-setup .circle-progress{margin-top:1px;margin-left:-3px}.dashboard-widget-finish-setup .circle-progress circle{stroke:#f0f0f0;stroke-width:1px}.dashboard-widget-finish-setup .circle-progress .bar{stroke:#949494} \ No newline at end of file diff --git a/assets/css/dashboard-setup.scss b/assets/css/dashboard-setup.scss new file mode 100644 index 0000000..cee2344 --- /dev/null +++ b/assets/css/dashboard-setup.scss @@ -0,0 +1,52 @@ +/** + * dashboard-setup.scss + * Styles for WooCommerce dashboard finish setup widgets + * only loaded on the dashboard itself. + */ + +/** + * Styling begins + */ + +.dashboard-widget-finish-setup { + + .progress-wrapper { + border: 1px solid #757575; + border-radius: 16px; + font-size: 0.9em; + padding: 2px 8px 2px 8px; + display: inline-block; + box-sizing: border-box; + } + + .progress-wrapper span { + position: relative; + top: -3px; + color: #757575; + } + + .description div { + margin-top: 11px; + float: left; + width: 70%; + } + + .description img { + float: right; + width: 30%; + } + + .circle-progress { + margin-top: 1px; + margin-left: -3px; + + circle { + stroke: #f0f0f0; + stroke-width: 1px; + } + + .bar { + stroke: #949494; + } + } +} diff --git a/assets/css/dashboard.css b/assets/css/dashboard.css new file mode 100644 index 0000000..442c6b6 --- /dev/null +++ b/assets/css/dashboard.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--woocommerce:#a46497;--wc-green:#7ad03a;--wc-red:#a00;--wc-orange:#ffba00;--wc-blue:#2ea2cc;--wc-primary:#a46497;--wc-primary-text:white;--wc-secondary:#ebe9eb;--wc-secondary-text:#515151;--wc-highlight:#77a464;--wc-highligh-text:white;--wc-content-bg:#fff;--wc-subtext:#767676}@font-face{font-family:star;src:url(../fonts/star.eot);src:url(../fonts/star.eot?#iefix) format("embedded-opentype"),url(../fonts/star.woff) format("woff"),url(../fonts/star.ttf) format("truetype"),url(../fonts/star.svg#star) format("svg");font-weight:400;font-style:normal}@font-face{font-family:WooCommerce;src:url(../fonts/WooCommerce.eot);src:url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"),url(../fonts/WooCommerce.woff) format("woff"),url(../fonts/WooCommerce.ttf) format("truetype"),url(../fonts/WooCommerce.svg#WooCommerce) format("svg");font-weight:400;font-style:normal}ul.woocommerce_stats{overflow:hidden;zoom:1}ul.woocommerce_stats li{width:25%;padding:0 1em;text-align:center;float:left;font-size:.8em;border-left:1px solid #fff;border-right:1px solid #ececec;box-sizing:border-box}ul.woocommerce_stats li:first-child{border-left:0}ul.woocommerce_stats li:last-child{border-right:0}ul.woocommerce_stats strong{font-family:Georgia,"Times New Roman","Bitstream Charter",Times,serif;font-size:4em;line-height:1.2em;font-weight:400;text-align:center;display:block}#woocommerce_dashboard_status .inside{padding:0;margin:0}#woocommerce_dashboard_status .best-seller-this-month a strong{margin-right:48px}#woocommerce_dashboard_status .wc_status_list{overflow:hidden;margin:0}#woocommerce_dashboard_status .wc_status_list li{width:50%;float:left;padding:0;box-sizing:border-box;margin:0;border-top:1px solid #ececec;color:#aaa}#woocommerce_dashboard_status .wc_status_list li a{display:block;color:#aaa;padding:9px 12px;-webkit-transition:all ease .5s;transition:all ease .5s;position:relative;font-size:12px}#woocommerce_dashboard_status .wc_status_list li a .wc_sparkline{width:4em;height:2em;display:block;float:right;position:absolute;right:0;top:50%;margin-right:12px;margin-top:-1.25em}#woocommerce_dashboard_status .wc_status_list li a strong{font-size:18px;line-height:1.2em;font-weight:400;display:block;color:#21759b}#woocommerce_dashboard_status .wc_status_list li a:hover{color:#2ea2cc}#woocommerce_dashboard_status .wc_status_list li a:hover strong,#woocommerce_dashboard_status .wc_status_list li a:hover::before{color:#2ea2cc!important}#woocommerce_dashboard_status .wc_status_list li a::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";font-size:2em;position:relative;width:auto;line-height:1.2em;color:#464646;float:left;margin-right:12px;margin-bottom:12px}#woocommerce_dashboard_status .wc_status_list li:first-child{border-top:0}#woocommerce_dashboard_status .wc_status_list li.sales-this-month{width:100%}#woocommerce_dashboard_status .wc_status_list li.sales-this-month a::before{font-family:Dashicons;content:"\f185"}#woocommerce_dashboard_status .wc_status_list li.best-seller-this-month{width:100%}#woocommerce_dashboard_status .wc_status_list li.best-seller-this-month a::before{content:"\e006"}#woocommerce_dashboard_status .wc_status_list li.processing-orders{border-right:1px solid #ececec}#woocommerce_dashboard_status .wc_status_list li.processing-orders a::before{content:"\e011";color:#7ad03a}#woocommerce_dashboard_status .wc_status_list li.on-hold-orders a::before{content:"\e033";color:#999}#woocommerce_dashboard_status .wc_status_list li.low-in-stock{border-right:1px solid #ececec}#woocommerce_dashboard_status .wc_status_list li.low-in-stock a::before{content:"\e016";color:#ffba00}#woocommerce_dashboard_status .wc_status_list li.out-of-stock a::before{content:"\e013";color:#a00}#woocommerce_dashboard_recent_reviews li{line-height:1.5em;margin-bottom:12px}#woocommerce_dashboard_recent_reviews h4.meta{line-height:1.4;margin:-.2em 0 0 0;font-weight:400;color:#999}#woocommerce_dashboard_recent_reviews blockquote{padding:0;margin:0}#woocommerce_dashboard_recent_reviews .avatar{float:left;margin:0 10px 5px 0}#woocommerce_dashboard_recent_reviews .star-rating{float:right;overflow:hidden;position:relative;height:1.5em;line-height:1.5;margin-left:.5em;width:5.4em;font-family:WooCommerce!important}#woocommerce_dashboard_recent_reviews .star-rating::before{content:"\e021\e021\e021\e021\e021";color:#b3b2b2;float:left;top:0;left:0;position:absolute;letter-spacing:.1em}#woocommerce_dashboard_recent_reviews .star-rating span{overflow:hidden;float:left;top:0;left:0;position:absolute;padding-top:1.5em}#woocommerce_dashboard_recent_reviews .star-rating span::before{content:"\e020\e020\e020\e020\e020";top:0;position:absolute;left:0;letter-spacing:.1em;color:#9c5d90}#dash-right-now li.product-count a::before{font-family:WooCommerce;content:"\e01d"}#dashboard_activity #activity-widget #the-comment-list .review.comment-item .avatar{margin-right:12px;position:relative;top:0;float:left} \ No newline at end of file diff --git a/assets/css/dashboard.scss b/assets/css/dashboard.scss new file mode 100644 index 0000000..5325e19 --- /dev/null +++ b/assets/css/dashboard.scss @@ -0,0 +1,272 @@ +/** + * dashboard.scss + * Styles for WooCommerce dashboard widgets, only loaded on the dashboard itself. + */ + +/** + * Imports + */ +@import "mixins"; +@import "variables"; +@import "fonts"; + +/** + * Styling begins + */ +ul.woocommerce_stats { + overflow: hidden; + zoom: 1; + + li { + width: 25%; + padding: 0 1em; + text-align: center; + float: left; + font-size: 0.8em; + border-left: 1px solid #fff; + border-right: 1px solid #ececec; + box-sizing: border-box; + } + + li:first-child { + border-left: 0; + } + + li:last-child { + border-right: 0; + } + + strong { + font-family: Georgia, "Times New Roman", "Bitstream Charter", Times, serif; + font-size: 4em; + line-height: 1.2em; + font-weight: normal; + text-align: center; + display: block; + } +} + +#woocommerce_dashboard_status { + + .inside { + padding: 0; + margin: 0; + } + + .best-seller-this-month { + + a { + + strong { + margin-right: 48px; + } + } + } + + .wc_status_list { + overflow: hidden; + margin: 0; + + li { + width: 50%; + float: left; + padding: 0; + box-sizing: border-box; + margin: 0; + border-top: 1px solid #ececec; + color: #aaa; + + a { + display: block; + color: #aaa; + padding: 9px 12px; + transition: all ease 0.5s; + position: relative; + font-size: 12px; + + .wc_sparkline { + width: 4em; + height: 2em; + display: block; + float: right; + position: absolute; + right: 0; + top: 50%; + margin-right: 12px; + margin-top: -1.25em; + } + + strong { + font-size: 18px; + line-height: 1.2em; + font-weight: normal; + display: block; + color: #21759b; + } + + &:hover { + color: #2ea2cc; + + &::before, + strong { + color: #2ea2cc !important; + } + } + + &::before { + + @include icon(); + font-size: 2em; + position: relative; + width: auto; + line-height: 1.2em; + color: #464646; + float: left; + margin-right: 12px; + margin-bottom: 12px; + } + } + } + + li:first-child { + border-top: 0; + } + + li.sales-this-month { + width: 100%; + + a::before { + font-family: "Dashicons"; + content: "\f185"; + } + } + + li.best-seller-this-month { + width: 100%; + + a::before { + content: "\e006"; + } + } + + li.processing-orders { + border-right: 1px solid #ececec; + + a::before { + content: "\e011"; + color: $green; + } + } + + li.on-hold-orders { + + a::before { + content: "\e033"; + color: #999; + } + } + + li.low-in-stock { + border-right: 1px solid #ececec; + + a::before { + content: "\e016"; + color: $orange; + } + } + + li.out-of-stock { + + a::before { + content: "\e013"; + color: $red; + } + } + } +} + +#woocommerce_dashboard_recent_reviews { + + li { + line-height: 1.5em; + margin-bottom: 12px; + } + + h4.meta { + line-height: 1.4; + margin: -0.2em 0 0 0; + font-weight: normal; + color: #999; + } + + blockquote { + padding: 0; + margin: 0; + } + + .avatar { + float: left; + margin: 0 10px 5px 0; + } + + .star-rating { + float: right; + overflow: hidden; + position: relative; + height: 1.5em; + line-height: 1.5; + margin-left: 0.5em; + width: 5.4em; + font-family: "WooCommerce" !important; + + &::before { + content: "\e021\e021\e021\e021\e021"; + color: darken(#ccc, 10%); + float: left; + top: 0; + left: 0; + position: absolute; + letter-spacing: 0.1em; + letter-spacing: 0\9; // IE8 & below hack ;-( + } + + span { + overflow: hidden; + float: left; + top: 0; + left: 0; + position: absolute; + padding-top: 1.5em; + } + + span::before { + content: "\e020\e020\e020\e020\e020"; + top: 0; + position: absolute; + left: 0; + letter-spacing: 0.1em; + letter-spacing: 0\9; // IE8 & below hack ;-( + color: #9c5d90; + } + } +} + +#dash-right-now li.product-count a::before { + font-family: "WooCommerce"; + content: "\e01d"; +} + +#dashboard_activity { + #activity-widget { + #the-comment-list { + .review.comment-item { + .avatar { + margin-right: 12px; + position: relative; + top: 0; + float: left; + } + } + } + } +} diff --git a/assets/css/helper-rtl.css b/assets/css/helper-rtl.css new file mode 100644 index 0000000..c950d73 --- /dev/null +++ b/assets/css/helper-rtl.css @@ -0,0 +1 @@ +.wc-helper .nav-tab-wrapper{margin-bottom:22px}@media only screen and (max-width:784px){.wc-helper .nav-tab{max-width:40%;overflow:hidden;text-overflow:ellipsis}}.wc-helper .button,.wc-helper .button:active,.wc-helper .button:focus,.wc-helper .button:hover{background-color:#955a89;border-width:0;box-shadow:none;border-radius:3px;color:#fff;height:auto;padding:3px 14px;text-align:center;white-space:normal!important}@media only screen and (max-width:782px){.wc-helper .button,.wc-helper .button:active,.wc-helper .button:focus,.wc-helper .button:hover{line-height:2}}.wc-helper .button.button-secondary,.wc-helper .button:active.button-secondary,.wc-helper .button:focus.button-secondary,.wc-helper .button:hover.button-secondary{background-color:#e6e6e6;color:#3c3c3c;text-shadow:none}.wc-helper .button:hover{opacity:.8}.wc-helper .subscription-filter{color:#2e4453;font-size:13px;line-height:13px;margin:22px 0}.wc-helper .subscription-filter label{display:none;position:relative}.wc-helper .subscription-filter label .chevron{color:#e1e1e1;border-bottom-width:0;line-height:1;padding:0;position:absolute;top:10px;left:14px}.wc-helper .subscription-filter li{color:#0073aa;display:inline-block;padding:0 8px 0 4px;position:relative}.wc-helper .subscription-filter li::before{background-color:#979797;content:" ";position:absolute;top:0;right:0;bottom:0;width:1px}.wc-helper .subscription-filter li:first-of-type::before{display:none}.wc-helper .subscription-filter a{color:#0073aa;text-decoration:none}.wc-helper .subscription-filter a.current{color:#000;font-weight:600}.wc-helper .subscription-filter .count{color:#555d66;font-weight:400}@media only screen and (max-width:600px){.wc-helper .subscription-filter{background-color:#fff;border:1px solid #e1e1e1;border-radius:4px;font-size:14px}.wc-helper .subscription-filter label,.wc-helper .subscription-filter li{line-height:21px;padding:8px 16px;margin:0}.wc-helper .subscription-filter label:last-child,.wc-helper .subscription-filter li:last-child{border-bottom:none}.wc-helper .subscription-filter li{border-bottom:1px solid #e1e1e1}.wc-helper .subscription-filter label,.wc-helper .subscription-filter span.chevron{display:block}.wc-helper .subscription-filter label{text-decoration:none}.wc-helper .subscription-filter li{display:none}.wc-helper .subscription-filter li::before{display:none}.wc-helper .subscription-filter a{cursor:pointer}.wc-helper .subscription-filter span.chevron{color:#555;opacity:.5;-webkit-transform:rotateX(180deg);transform:rotateX(180deg)}.wc-helper .subscription-filter:focus,.wc-helper .subscription-filter:hover{box-shadow:0 3px 5px rgba(0,0,0,.2)}.wc-helper .subscription-filter:focus label,.wc-helper .subscription-filter:hover label{border-bottom:1px solid #e1e1e1}.wc-helper .subscription-filter:focus li,.wc-helper .subscription-filter:hover li{display:block}.wc-helper .subscription-filter:focus span.chevron,.wc-helper .subscription-filter:hover span.chevron{-webkit-transform:rotateX(0);transform:rotateX(0)}}.wc-helper .subscriptions-header{margin:3em 0 0;position:relative;z-index:10}.wc-helper .subscriptions-header h2{display:inline-block;line-height:25px;margin:0 0 1.5em 0}.wc-helper .button-update,.wc-helper .button-update:hover{background-color:#e6e6e6;border-radius:4px;color:#333;font-weight:800;font-size:10px;line-height:20px;margin-right:6px;opacity:.75;padding:3px 7px;text-transform:uppercase}.wc-helper .button-update .dashicons,.wc-helper .button-update:hover .dashicons{font-size:12px;height:12px;width:12px;vertical-align:text-bottom}.wc-helper .button-update:hover{opacity:1}.wc-helper .user-info{background-color:#fff;border:1px solid #e1e1e1;border-radius:4px;font-size:12px;line-height:26px;position:absolute;top:-10px;left:0;-webkit-transition:all .1s ease-in;transition:all .1s ease-in}@media only screen and (max-width:600px){.wc-helper .user-info{position:relative;width:100%}}.wc-helper .user-info p{line-height:26px;margin:0}.wc-helper .user-info:hover{box-shadow:0 3px 5px rgba(0,0,0,.2)}.wc-helper .user-info header{color:#555;font-weight:600;padding:6px 14px;position:relative}.wc-helper .user-info header p{padding-left:26px}.wc-helper .user-info header .dashicons{opacity:.5;position:absolute;top:9px;left:14px}.wc-helper .user-info header:hover{cursor:pointer}.wc-helper .user-info section{display:none}.wc-helper .user-info section p{border-top:1px solid #e1e1e1;padding:6px 14px;text-align:center}.wc-helper .user-info section .actions{border-top:1px solid #e1e1e1;display:-webkit-box;display:flex}.wc-helper .user-info section a{color:#a26897;cursor:pointer;font-weight:600;line-height:38px;padding:0 14px;text-align:center;text-decoration:none;white-space:nowrap;width:50%}.wc-helper .user-info section a .dashicons{margin-top:-3px;vertical-align:middle}.wc-helper .user-info section a:first-child{border-left:1px solid #e1e1e1}.wc-helper .user-info section a:hover{background-color:#a26897;color:#fff}.wc-helper .user-info section .avatar{border:1px solid #ece1ea;border-radius:50%;height:auto;margin-left:6px;width:24px;vertical-align:bottom}.wc-helper .user-info:active header .dashicons,.wc-helper .user-info:focus header .dashicons,.wc-helper .user-info:hover header .dashicons{-webkit-transform:rotateX(180deg);transform:rotateX(180deg)}.wc-helper .user-info:active section,.wc-helper .user-info:focus section,.wc-helper .user-info:hover section{display:block}.wc-helper .alternate,.wc-helper .striped>tbody>:nth-child(odd),.wc-helper ul.striped>:nth-child(odd){background-color:#fff}.wc-helper .comment-ays,.wc-helper .feature-filter,.wc-helper .imgedit-group,.wc-helper .popular-tags,.wc-helper .stuffbox,.wc-helper .widgets-holder-wrap,.wc-helper .wp-editor-container,.wc-helper p.popular-tags,.wc-helper table.widefat{padding-top:5px}.wc-helper .widefat tfoot tr td,.wc-helper .widefat tfoot tr th,.wc-helper .widefat thead tr td,.wc-helper .widefat thead tr th{color:#32373c;padding-bottom:15px;padding-top:10px}.wc-helper .widefat td{padding-bottom:15px;padding-top:15px}.wc-helper .wp-list-table{border:0;box-shadow:none;padding-top:0!important;z-index:1}@media only screen and (max-width:782px){.wc-helper .button{font-size:11px}}.wc-helper .wp-list-table__row{background-color:rgba(0,0,0,0)}.wc-helper .wp-list-table__row td{-webkit-box-align:center;align-items:center;background-color:#fff;border:0;padding:16px 22px;vertical-align:middle}@media only screen and (max-width:782px){.wc-helper .wp-list-table__row td{padding:16px}}.wc-helper .wp-list-table__row td.color-bar{border-right:0}.wc-helper .wp-list-table__row.is-ext-header td{border-top:1px solid #e1e1e1}@media only screen and (max-width:782px){.wc-helper .wp-list-table__row.is-ext-header{display:-webkit-inline-box;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;width:100%}.wc-helper .wp-list-table__row.is-ext-header .wp-list-table__ext-details{display:block;-webkit-box-flex:2;flex:2}.wc-helper .wp-list-table__row.is-ext-header .wp-list-table__ext-actions{display:block;-webkit-box-flex:1;flex:1;min-width:0}}.wc-helper .wp-list-table__row:last-child td{border-bottom:24px solid #f1f1f1;box-shadow:inset 0 -1px 0 #e1e1e1}.wc-helper .wp-list-table__ext-details,.wc-helper .wp-list-table__ext-status,.wc-helper .wp-list-table__licence-container{padding-left:22px;position:relative;width:100%}.wc-helper .wp-list-table__ext-details::before,.wc-helper .wp-list-table__ext-status::before,.wc-helper .wp-list-table__licence-container::before{background-color:#e1e1e1;content:" ";position:absolute;top:0;bottom:0;right:0!important;width:1px!important}.wc-helper .wp-list-table__ext-details{display:-webkit-box;display:flex}@media only screen and (max-width:782px){.wc-helper .wp-list-table__ext-details{display:table}}.wc-helper .wp-list-table__ext-title{color:#0073aa;font-size:18px;font-weight:600;width:60%}@media only screen and (max-width:782px){.wc-helper .wp-list-table__ext-title{margin-bottom:12px;width:100%}}@media only screen and (max-width:320px){.wc-helper .wp-list-table__ext-title{max-width:120px}}.wc-helper .wp-list-table__ext-description{color:#333;padding-right:12px;width:40%}@media only screen and (max-width:782px){.wc-helper .wp-list-table__ext-description{padding-right:0;width:100%}}.wc-helper .wp-list-table__ext-status{position:relative}.wc-helper .wp-list-table__ext-status.update-available::after{background-color:#ffc322;content:" ";position:absolute;top:0;right:0;bottom:0;width:5px}.wc-helper .wp-list-table__ext-status.expired::after{background-color:#b81c23;content:" ";position:absolute;top:0;right:0;bottom:0;width:5px}.wc-helper .wp-list-table__ext-status .dashicons-update{color:#ffc322}.wc-helper .wp-list-table__ext-status .dashicons-info{color:#b81c23}.wc-helper .wp-list-table__ext-status p{color:#333;margin:0}.wc-helper .wp-list-table__ext-status .dashicons{margin-left:5px}.wc-helper .wp-list-table__ext-actions{min-width:150px;position:relative;width:25%;text-align:left}.wc-helper .wp-list-table__ext-actions::after{background-color:#e1e1e1;content:" ";position:absolute;top:0;bottom:0;left:0;width:1px}.wc-helper .wp-list-table__ext-licence td,.wc-helper .wp-list-table__ext-updates td{position:relative}.wc-helper .wp-list-table__ext-licence td::before,.wc-helper .wp-list-table__ext-updates td::before{background-color:#e1e1e1;content:" ";height:1px;position:absolute;top:0;right:0;left:0}.wc-helper .wp-list-table__ext-licence td.wp-list-table__ext-status::before,.wc-helper .wp-list-table__ext-licence td.wp-list-table__licence-container::before,.wc-helper .wp-list-table__ext-updates td.wp-list-table__ext-status::before,.wc-helper .wp-list-table__ext-updates td.wp-list-table__licence-container::before{right:22px!important;width:auto!important}.wc-helper .wp-list-table__ext-licence td.wp-list-table__ext-actions::before,.wc-helper .wp-list-table__ext-updates td.wp-list-table__ext-actions::before{left:22px}@media only screen and (max-width:782px){.wc-helper .wp-list-table__ext-licence,.wc-helper .wp-list-table__ext-updates{display:-webkit-box;display:flex}.wc-helper .wp-list-table__ext-licence .wp-list-table__ext-status,.wc-helper .wp-list-table__ext-updates .wp-list-table__ext-status{-webkit-box-flex:2;flex:2}.wc-helper .wp-list-table__ext-licence .wp-list-table__ext-status::before,.wc-helper .wp-list-table__ext-updates .wp-list-table__ext-status::before{right:0!important;width:100%!important}.wc-helper .wp-list-table__ext-licence .wp-list-table__ext-actions,.wc-helper .wp-list-table__ext-updates .wp-list-table__ext-actions{-webkit-box-flex:1;flex:1;min-width:0}.wc-helper .wp-list-table__ext-licence .wp-list-table__ext-actions::before,.wc-helper .wp-list-table__ext-updates .wp-list-table__ext-actions::before{right:0!important;left:0!important;width:100%!important}}.wc-helper .wp-list-table__licence-container{padding:0!important}.wc-helper .wp-list-table__licence-container::after{background-color:#e1e1e1;content:" ";position:absolute;top:0;bottom:0;left:0;width:1px}.wc-helper .wp-list-table__licence-form{display:-webkit-box;display:flex;padding:16px 22px}@media only screen and (max-width:782px){.wc-helper .wp-list-table__licence-form{display:block}}.wc-helper .wp-list-table__licence-form::before{background-color:#e1e1e1;content:" ";height:1px;position:absolute;top:0;left:22px;right:22px}@media only screen and (max-width:782px){.wc-helper .wp-list-table__licence-form::before{left:0;right:0}}.wc-helper .wp-list-table__licence-form div{padding-left:16px;vertical-align:middle}@media only screen and (max-width:782px){.wc-helper .wp-list-table__licence-form div{padding:0}}.wc-helper .wp-list-table__licence-form p{margin:0!important}.wc-helper .wp-list-table__licence-label label{color:#23282d;font-weight:600;line-height:30px}.wc-helper .wp-list-table__licence-field input{height:32px}@media only screen and (max-width:480px){.wc-helper .wp-list-table__licence-field input{width:100%}}@media only screen and (max-width:782px){.wc-helper .wp-list-table__licence-field{padding:8px 0 16px!important}}.wc-helper .wp-list-table__licence-actions{-webkit-box-flex:2;flex-grow:2;padding-left:0!important}.wc-helper .wp-list-table__licence-actions .button{margin-left:8px}.wc-helper .wp-list-table__licence-actions .button-secondary{float:left;margin:0 8px 0 0}@media only screen and (max-width:480px){.wc-helper .wp-list-table__licence-actions{text-align:left}}.wc-helper td.color-bar{border-right:solid 4px transparent}.wc-helper td.color-bar.expired{border-right-color:#b81c23}.wc-helper td.color-bar.expiring{border-right-color:orange}.wc-helper td.color-bar.update-available{border-right-color:#8fae1b}.wc-helper td.color-bar.expiring.update-available{border-right-color:#8fae1b}.wc-helper .connect-wrapper{background-color:#fff;border:1px solid #e5e5e5;margin-bottom:25px;overflow:auto}.wc-helper .connected{display:-webkit-box;display:flex}.wc-helper .connected .user-info{display:-webkit-box;display:flex;padding:20px;width:100%;vertical-align:middle}.wc-helper .connected img{border:1px solid #e5e5e5;height:34px;width:34px}.wc-helper .connected .buttons{padding:20px;white-space:nowrap}.wc-helper .connected p{-webkit-box-flex:2;flex:2;margin:10px 20px 0 0}.wc-helper .connected .chevron{display:none}.wc-helper .connected .chevron:hover{color:#955a89;cursor:pointer}@media only screen and (max-width:784px){.wc-helper .connected{display:block}.wc-helper .connected strong{display:block;overflow:hidden;text-overflow:ellipsis}.wc-helper .connected p{margin:0;overflow:hidden;text-overflow:ellipsis;width:80%}.wc-helper .connected .user-info{padding-left:0;width:auto}.wc-helper .connected .avatar{margin-left:12px}.wc-helper .connected .chevron{color:#e1e1e1;display:block;margin:10px;-webkit-transform:rotateX(0);transform:rotateX(0)}.wc-helper .connected .buttons{display:none;border-top:1px solid #e1e1e1;padding:10px 20px}.wc-helper .connected .buttons.active{display:block}}.wc-helper .start-container{background-color:#fff;border-right:4px solid #cc99c2;padding:45px 30px 20px 20px;position:relative;overflow:hidden}.wc-helper .start-container h2,.wc-helper .start-container p{max-width:800px}.wc-helper .start-container::before{color:#eee2ec;content:"\e01C";display:block;font-family:WooCommerce;font-size:192px;line-height:1;position:absolute;top:65%;left:-3%;text-align:center;width:1em}.wc-helper .start-container h2{font-size:24px;line-height:29px;position:relative}.wc-helper .start-container p{font-size:16px;margin-bottom:30px;position:relative}.wc-helper .button-helper-connect{height:37px;line-height:37px;min-width:124px;padding:0 13px;text-shadow:none}.wc-helper .button-helper-connect:active,.wc-helper .button-helper-connect:focus,.wc-helper .button-helper-connect:hover{padding:0 13px}.form-toggle__wrapper{position:relative}.form-toggle__wrapper label{cursor:default}.form-toggle{cursor:pointer;display:block;position:absolute;top:0;bottom:-1px;right:0;left:0;text-align:right;text-indent:-100000px;z-index:2}.form-toggle:focus{box-shadow:none}.form-toggle.disabled{cursor:default}.form-toggle__switch{align-self:flex-start;background:#c8d7e1;border-radius:12px;box-sizing:border-box;display:inline-block;padding:2px;outline:0;position:relative;width:40px;height:24px;-webkit-transition:all .4s ease,box-shadow 0s;transition:all .4s ease,box-shadow 0s;vertical-align:middle}.form-toggle__switch::after,.form-toggle__switch::before{content:"";display:block;position:relative;width:20px;height:20px}.form-toggle__switch::after{border-radius:50%;background:#fff;right:0;-webkit-transition:all .2s ease;transition:all .2s ease}.form-toggle__switch::before{display:none}.accessible-focus .form-toggle__switch:focus{box-shadow:0 0 0 2px #955a89}.form-toggle__label{vertical-align:bottom;z-index:1}.form-toggle__label .form-toggle__label-content{color:#87a6bc;-webkit-box-flex:0;flex:0 1 100%;font-size:13px;line-height:16px;margin-right:12px;margin-left:8px;vertical-align:top;text-transform:uppercase}@media only screen and (max-width:480px){.form-toggle__label .form-toggle__label-content{display:none}}.accessible-focus .form-toggle:focus+.form-toggle__label .form-toggle__switch{box-shadow:0 0 0 2px #955a89}.accessible-focus .form-toggle:focus:checked+.form-toggle__label .form-toggle__switch{box-shadow:0 0 0 2px #bb77ae}.form-toggle+.form-toggle__label .form-toggle__switch{background:#a8bece}.form-toggle:not(:disabled)+.form-toggle__label:hover .form-toggle__switch{background:#c8d7e1}.form-toggle.active+.form-toggle__label .form-toggle__switch{background:#955a89}.form-toggle.active+.form-toggle__label .form-toggle__switch::after{right:8px}.form-toggle.active+.form-toggle__label:hover .form-toggle__switch{background:#bb77ae}.form-toggle.disabled+label.form-toggle__label span.form-toggle__switch{opacity:.25}.form-toggle.is-toggling+.form-toggle__label .form-toggle__switch{background:#955a89}.form-toggle.is-toggling:checked+.form-toggle__label .form-toggle__switch{background:#c8d7e1}.form-toggle.is-compact+.form-toggle__label .form-toggle__switch{border-radius:8px;width:24px;height:16px}.form-toggle.is-compact+.form-toggle__label .form-toggle__switch::after,.form-toggle.is-compact+.form-toggle__label .form-toggle__switch::before{height:12px;width:12px}.form-toggle.is-compact:checked+.form-toggle__label .form-toggle__switch::after{right:8px} \ No newline at end of file diff --git a/assets/css/helper.css b/assets/css/helper.css new file mode 100644 index 0000000..55c5e50 --- /dev/null +++ b/assets/css/helper.css @@ -0,0 +1 @@ +.wc-helper .nav-tab-wrapper{margin-bottom:22px}@media only screen and (max-width:784px){.wc-helper .nav-tab{max-width:40%;overflow:hidden;text-overflow:ellipsis}}.wc-helper .button,.wc-helper .button:active,.wc-helper .button:focus,.wc-helper .button:hover{background-color:#955a89;border-width:0;box-shadow:none;border-radius:3px;color:#fff;height:auto;padding:3px 14px;text-align:center;white-space:normal!important}@media only screen and (max-width:782px){.wc-helper .button,.wc-helper .button:active,.wc-helper .button:focus,.wc-helper .button:hover{line-height:2}}.wc-helper .button.button-secondary,.wc-helper .button:active.button-secondary,.wc-helper .button:focus.button-secondary,.wc-helper .button:hover.button-secondary{background-color:#e6e6e6;color:#3c3c3c;text-shadow:none}.wc-helper .button:hover{opacity:.8}.wc-helper .subscription-filter{color:#2e4453;font-size:13px;line-height:13px;margin:22px 0}.wc-helper .subscription-filter label{display:none;position:relative}.wc-helper .subscription-filter label .chevron{color:#e1e1e1;border-bottom-width:0;line-height:1;padding:0;position:absolute;top:10px;right:14px}.wc-helper .subscription-filter li{color:#0073aa;display:inline-block;padding:0 4px 0 8px;position:relative}.wc-helper .subscription-filter li::before{background-color:#979797;content:" ";position:absolute;top:0;left:0;bottom:0;width:1px}.wc-helper .subscription-filter li:first-of-type::before{display:none}.wc-helper .subscription-filter a{color:#0073aa;text-decoration:none}.wc-helper .subscription-filter a.current{color:#000;font-weight:600}.wc-helper .subscription-filter .count{color:#555d66;font-weight:400}@media only screen and (max-width:600px){.wc-helper .subscription-filter{background-color:#fff;border:1px solid #e1e1e1;border-radius:4px;font-size:14px}.wc-helper .subscription-filter label,.wc-helper .subscription-filter li{line-height:21px;padding:8px 16px;margin:0}.wc-helper .subscription-filter label:last-child,.wc-helper .subscription-filter li:last-child{border-bottom:none}.wc-helper .subscription-filter li{border-bottom:1px solid #e1e1e1}.wc-helper .subscription-filter label,.wc-helper .subscription-filter span.chevron{display:block}.wc-helper .subscription-filter label{text-decoration:none}.wc-helper .subscription-filter li{display:none}.wc-helper .subscription-filter li::before{display:none}.wc-helper .subscription-filter a{cursor:pointer}.wc-helper .subscription-filter span.chevron{color:#555;opacity:.5;-webkit-transform:rotateX(180deg);transform:rotateX(180deg)}.wc-helper .subscription-filter:focus,.wc-helper .subscription-filter:hover{box-shadow:0 3px 5px rgba(0,0,0,.2)}.wc-helper .subscription-filter:focus label,.wc-helper .subscription-filter:hover label{border-bottom:1px solid #e1e1e1}.wc-helper .subscription-filter:focus li,.wc-helper .subscription-filter:hover li{display:block}.wc-helper .subscription-filter:focus span.chevron,.wc-helper .subscription-filter:hover span.chevron{-webkit-transform:rotateX(0);transform:rotateX(0)}}.wc-helper .subscriptions-header{margin:3em 0 0;position:relative;z-index:10}.wc-helper .subscriptions-header h2{display:inline-block;line-height:25px;margin:0 0 1.5em 0}.wc-helper .button-update,.wc-helper .button-update:hover{background-color:#e6e6e6;border-radius:4px;color:#333;font-weight:800;font-size:10px;line-height:20px;margin-left:6px;opacity:.75;padding:3px 7px;text-transform:uppercase}.wc-helper .button-update .dashicons,.wc-helper .button-update:hover .dashicons{font-size:12px;height:12px;width:12px;vertical-align:text-bottom}.wc-helper .button-update:hover{opacity:1}.wc-helper .user-info{background-color:#fff;border:1px solid #e1e1e1;border-radius:4px;font-size:12px;line-height:26px;position:absolute;top:-10px;right:0;-webkit-transition:all .1s ease-in;transition:all .1s ease-in}@media only screen and (max-width:600px){.wc-helper .user-info{position:relative;width:100%}}.wc-helper .user-info p{line-height:26px;margin:0}.wc-helper .user-info:hover{box-shadow:0 3px 5px rgba(0,0,0,.2)}.wc-helper .user-info header{color:#555;font-weight:600;padding:6px 14px;position:relative}.wc-helper .user-info header p{padding-right:26px}.wc-helper .user-info header .dashicons{opacity:.5;position:absolute;top:9px;right:14px}.wc-helper .user-info header:hover{cursor:pointer}.wc-helper .user-info section{display:none}.wc-helper .user-info section p{border-top:1px solid #e1e1e1;padding:6px 14px;text-align:center}.wc-helper .user-info section .actions{border-top:1px solid #e1e1e1;display:-webkit-box;display:flex}.wc-helper .user-info section a{color:#a26897;cursor:pointer;font-weight:600;line-height:38px;padding:0 14px;text-align:center;text-decoration:none;white-space:nowrap;width:50%}.wc-helper .user-info section a .dashicons{margin-top:-3px;vertical-align:middle}.wc-helper .user-info section a:first-child{border-right:1px solid #e1e1e1}.wc-helper .user-info section a:hover{background-color:#a26897;color:#fff}.wc-helper .user-info section .avatar{border:1px solid #ece1ea;border-radius:50%;height:auto;margin-right:6px;width:24px;vertical-align:bottom}.wc-helper .user-info:active header .dashicons,.wc-helper .user-info:focus header .dashicons,.wc-helper .user-info:hover header .dashicons{-webkit-transform:rotateX(180deg);transform:rotateX(180deg)}.wc-helper .user-info:active section,.wc-helper .user-info:focus section,.wc-helper .user-info:hover section{display:block}.wc-helper .alternate,.wc-helper .striped>tbody>:nth-child(odd),.wc-helper ul.striped>:nth-child(odd){background-color:#fff}.wc-helper .comment-ays,.wc-helper .feature-filter,.wc-helper .imgedit-group,.wc-helper .popular-tags,.wc-helper .stuffbox,.wc-helper .widgets-holder-wrap,.wc-helper .wp-editor-container,.wc-helper p.popular-tags,.wc-helper table.widefat{padding-top:5px}.wc-helper .widefat tfoot tr td,.wc-helper .widefat tfoot tr th,.wc-helper .widefat thead tr td,.wc-helper .widefat thead tr th{color:#32373c;padding-bottom:15px;padding-top:10px}.wc-helper .widefat td{padding-bottom:15px;padding-top:15px}.wc-helper .wp-list-table{border:0;box-shadow:none;padding-top:0!important;z-index:1}@media only screen and (max-width:782px){.wc-helper .button{font-size:11px}}.wc-helper .wp-list-table__row{background-color:rgba(0,0,0,0)}.wc-helper .wp-list-table__row td{-webkit-box-align:center;align-items:center;background-color:#fff;border:0;padding:16px 22px;vertical-align:middle}@media only screen and (max-width:782px){.wc-helper .wp-list-table__row td{padding:16px}}.wc-helper .wp-list-table__row td.color-bar{border-left:0}.wc-helper .wp-list-table__row.is-ext-header td{border-top:1px solid #e1e1e1}@media only screen and (max-width:782px){.wc-helper .wp-list-table__row.is-ext-header{display:-webkit-inline-box;display:inline-flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-flow:row wrap;width:100%}.wc-helper .wp-list-table__row.is-ext-header .wp-list-table__ext-details{display:block;-webkit-box-flex:2;flex:2}.wc-helper .wp-list-table__row.is-ext-header .wp-list-table__ext-actions{display:block;-webkit-box-flex:1;flex:1;min-width:0}}.wc-helper .wp-list-table__row:last-child td{border-bottom:24px solid #f1f1f1;box-shadow:inset 0 -1px 0 #e1e1e1}.wc-helper .wp-list-table__ext-details,.wc-helper .wp-list-table__ext-status,.wc-helper .wp-list-table__licence-container{padding-right:22px;position:relative;width:100%}.wc-helper .wp-list-table__ext-details::before,.wc-helper .wp-list-table__ext-status::before,.wc-helper .wp-list-table__licence-container::before{background-color:#e1e1e1;content:" ";position:absolute;top:0;bottom:0;left:0!important;width:1px!important}.wc-helper .wp-list-table__ext-details{display:-webkit-box;display:flex}@media only screen and (max-width:782px){.wc-helper .wp-list-table__ext-details{display:table}}.wc-helper .wp-list-table__ext-title{color:#0073aa;font-size:18px;font-weight:600;width:60%}@media only screen and (max-width:782px){.wc-helper .wp-list-table__ext-title{margin-bottom:12px;width:100%}}@media only screen and (max-width:320px){.wc-helper .wp-list-table__ext-title{max-width:120px}}.wc-helper .wp-list-table__ext-description{color:#333;padding-left:12px;width:40%}@media only screen and (max-width:782px){.wc-helper .wp-list-table__ext-description{padding-left:0;width:100%}}.wc-helper .wp-list-table__ext-status{position:relative}.wc-helper .wp-list-table__ext-status.update-available::after{background-color:#ffc322;content:" ";position:absolute;top:0;left:0;bottom:0;width:5px}.wc-helper .wp-list-table__ext-status.expired::after{background-color:#b81c23;content:" ";position:absolute;top:0;left:0;bottom:0;width:5px}.wc-helper .wp-list-table__ext-status .dashicons-update{color:#ffc322}.wc-helper .wp-list-table__ext-status .dashicons-info{color:#b81c23}.wc-helper .wp-list-table__ext-status p{color:#333;margin:0}.wc-helper .wp-list-table__ext-status .dashicons{margin-right:5px}.wc-helper .wp-list-table__ext-actions{min-width:150px;position:relative;width:25%;text-align:right}.wc-helper .wp-list-table__ext-actions::after{background-color:#e1e1e1;content:" ";position:absolute;top:0;bottom:0;right:0;width:1px}.wc-helper .wp-list-table__ext-licence td,.wc-helper .wp-list-table__ext-updates td{position:relative}.wc-helper .wp-list-table__ext-licence td::before,.wc-helper .wp-list-table__ext-updates td::before{background-color:#e1e1e1;content:" ";height:1px;position:absolute;top:0;left:0;right:0}.wc-helper .wp-list-table__ext-licence td.wp-list-table__ext-status::before,.wc-helper .wp-list-table__ext-licence td.wp-list-table__licence-container::before,.wc-helper .wp-list-table__ext-updates td.wp-list-table__ext-status::before,.wc-helper .wp-list-table__ext-updates td.wp-list-table__licence-container::before{left:22px!important;width:auto!important}.wc-helper .wp-list-table__ext-licence td.wp-list-table__ext-actions::before,.wc-helper .wp-list-table__ext-updates td.wp-list-table__ext-actions::before{right:22px}@media only screen and (max-width:782px){.wc-helper .wp-list-table__ext-licence,.wc-helper .wp-list-table__ext-updates{display:-webkit-box;display:flex}.wc-helper .wp-list-table__ext-licence .wp-list-table__ext-status,.wc-helper .wp-list-table__ext-updates .wp-list-table__ext-status{-webkit-box-flex:2;flex:2}.wc-helper .wp-list-table__ext-licence .wp-list-table__ext-status::before,.wc-helper .wp-list-table__ext-updates .wp-list-table__ext-status::before{left:0!important;width:100%!important}.wc-helper .wp-list-table__ext-licence .wp-list-table__ext-actions,.wc-helper .wp-list-table__ext-updates .wp-list-table__ext-actions{-webkit-box-flex:1;flex:1;min-width:0}.wc-helper .wp-list-table__ext-licence .wp-list-table__ext-actions::before,.wc-helper .wp-list-table__ext-updates .wp-list-table__ext-actions::before{left:0!important;right:0!important;width:100%!important}}.wc-helper .wp-list-table__licence-container{padding:0!important}.wc-helper .wp-list-table__licence-container::after{background-color:#e1e1e1;content:" ";position:absolute;top:0;bottom:0;right:0;width:1px}.wc-helper .wp-list-table__licence-form{display:-webkit-box;display:flex;padding:16px 22px}@media only screen and (max-width:782px){.wc-helper .wp-list-table__licence-form{display:block}}.wc-helper .wp-list-table__licence-form::before{background-color:#e1e1e1;content:" ";height:1px;position:absolute;top:0;right:22px;left:22px}@media only screen and (max-width:782px){.wc-helper .wp-list-table__licence-form::before{right:0;left:0}}.wc-helper .wp-list-table__licence-form div{padding-right:16px;vertical-align:middle}@media only screen and (max-width:782px){.wc-helper .wp-list-table__licence-form div{padding:0}}.wc-helper .wp-list-table__licence-form p{margin:0!important}.wc-helper .wp-list-table__licence-label label{color:#23282d;font-weight:600;line-height:30px}.wc-helper .wp-list-table__licence-field input{height:32px}@media only screen and (max-width:480px){.wc-helper .wp-list-table__licence-field input{width:100%}}@media only screen and (max-width:782px){.wc-helper .wp-list-table__licence-field{padding:8px 0 16px!important}}.wc-helper .wp-list-table__licence-actions{-webkit-box-flex:2;flex-grow:2;padding-right:0!important}.wc-helper .wp-list-table__licence-actions .button{margin-right:8px}.wc-helper .wp-list-table__licence-actions .button-secondary{float:right;margin:0 0 0 8px}@media only screen and (max-width:480px){.wc-helper .wp-list-table__licence-actions{text-align:right}}.wc-helper td.color-bar{border-left:solid 4px transparent}.wc-helper td.color-bar.expired{border-left-color:#b81c23}.wc-helper td.color-bar.expiring{border-left-color:orange}.wc-helper td.color-bar.update-available{border-left-color:#8fae1b}.wc-helper td.color-bar.expiring.update-available{border-left-color:#8fae1b}.wc-helper .connect-wrapper{background-color:#fff;border:1px solid #e5e5e5;margin-bottom:25px;overflow:auto}.wc-helper .connected{display:-webkit-box;display:flex}.wc-helper .connected .user-info{display:-webkit-box;display:flex;padding:20px;width:100%;vertical-align:middle}.wc-helper .connected img{border:1px solid #e5e5e5;height:34px;width:34px}.wc-helper .connected .buttons{padding:20px;white-space:nowrap}.wc-helper .connected p{-webkit-box-flex:2;flex:2;margin:10px 0 0 20px}.wc-helper .connected .chevron{display:none}.wc-helper .connected .chevron:hover{color:#955a89;cursor:pointer}@media only screen and (max-width:784px){.wc-helper .connected{display:block}.wc-helper .connected strong{display:block;overflow:hidden;text-overflow:ellipsis}.wc-helper .connected p{margin:0;overflow:hidden;text-overflow:ellipsis;width:80%}.wc-helper .connected .user-info{padding-right:0;width:auto}.wc-helper .connected .avatar{margin-right:12px}.wc-helper .connected .chevron{color:#e1e1e1;display:block;margin:10px;-webkit-transform:rotateX(0);transform:rotateX(0)}.wc-helper .connected .buttons{display:none;border-top:1px solid #e1e1e1;padding:10px 20px}.wc-helper .connected .buttons.active{display:block}}.wc-helper .start-container{background-color:#fff;border-left:4px solid #cc99c2;padding:45px 20px 20px 30px;position:relative;overflow:hidden}.wc-helper .start-container h2,.wc-helper .start-container p{max-width:800px}.wc-helper .start-container::before{color:#eee2ec;content:"\e01C";display:block;font-family:WooCommerce;font-size:192px;line-height:1;position:absolute;top:65%;right:-3%;text-align:center;width:1em}.wc-helper .start-container h2{font-size:24px;line-height:29px;position:relative}.wc-helper .start-container p{font-size:16px;margin-bottom:30px;position:relative}.wc-helper .button-helper-connect{height:37px;line-height:37px;min-width:124px;padding:0 13px;text-shadow:none}.wc-helper .button-helper-connect:active,.wc-helper .button-helper-connect:focus,.wc-helper .button-helper-connect:hover{padding:0 13px}.form-toggle__wrapper{position:relative}.form-toggle__wrapper label{cursor:default}.form-toggle{cursor:pointer;display:block;position:absolute;top:0;bottom:-1px;left:0;right:0;text-align:left;text-indent:-100000px;z-index:2}.form-toggle:focus{box-shadow:none}.form-toggle.disabled{cursor:default}.form-toggle__switch{align-self:flex-start;background:#c8d7e1;border-radius:12px;box-sizing:border-box;display:inline-block;padding:2px;outline:0;position:relative;width:40px;height:24px;-webkit-transition:all .4s ease,box-shadow 0s;transition:all .4s ease,box-shadow 0s;vertical-align:middle}.form-toggle__switch::after,.form-toggle__switch::before{content:"";display:block;position:relative;width:20px;height:20px}.form-toggle__switch::after{border-radius:50%;background:#fff;left:0;-webkit-transition:all .2s ease;transition:all .2s ease}.form-toggle__switch::before{display:none}.accessible-focus .form-toggle__switch:focus{box-shadow:0 0 0 2px #955a89}.form-toggle__label{vertical-align:bottom;z-index:1}.form-toggle__label .form-toggle__label-content{color:#87a6bc;-webkit-box-flex:0;flex:0 1 100%;font-size:13px;line-height:16px;margin-left:12px;margin-right:8px;vertical-align:top;text-transform:uppercase}@media only screen and (max-width:480px){.form-toggle__label .form-toggle__label-content{display:none}}.accessible-focus .form-toggle:focus+.form-toggle__label .form-toggle__switch{box-shadow:0 0 0 2px #955a89}.accessible-focus .form-toggle:focus:checked+.form-toggle__label .form-toggle__switch{box-shadow:0 0 0 2px #bb77ae}.form-toggle+.form-toggle__label .form-toggle__switch{background:#a8bece}.form-toggle:not(:disabled)+.form-toggle__label:hover .form-toggle__switch{background:#c8d7e1}.form-toggle.active+.form-toggle__label .form-toggle__switch{background:#955a89}.form-toggle.active+.form-toggle__label .form-toggle__switch::after{left:8px}.form-toggle.active+.form-toggle__label:hover .form-toggle__switch{background:#bb77ae}.form-toggle.disabled+label.form-toggle__label span.form-toggle__switch{opacity:.25}.form-toggle.is-toggling+.form-toggle__label .form-toggle__switch{background:#955a89}.form-toggle.is-toggling:checked+.form-toggle__label .form-toggle__switch{background:#c8d7e1}.form-toggle.is-compact+.form-toggle__label .form-toggle__switch{border-radius:8px;width:24px;height:16px}.form-toggle.is-compact+.form-toggle__label .form-toggle__switch::after,.form-toggle.is-compact+.form-toggle__label .form-toggle__switch::before{height:12px;width:12px}.form-toggle.is-compact:checked+.form-toggle__label .form-toggle__switch::after{left:8px} \ No newline at end of file diff --git a/assets/css/helper.scss b/assets/css/helper.scss new file mode 100644 index 0000000..7c30706 --- /dev/null +++ b/assets/css/helper.scss @@ -0,0 +1,1091 @@ +/*------------------------------------------------------------------------------ + General table styling +------------------------------------------------------------------------------*/ + +$white: #fff; + +// Grays +$gray: #87a6bc; +$gray-light: lighten($gray, 33%); //#f3f6f8 +$gray-dark: darken($gray, 38%); //#2e4453 + +// $gray-text: ideal for standard, non placeholder text +// $gray-text-min: minimum contrast needed for WCAG 2.0 AA on white background +$gray-text: $gray-dark; +$gray-text-min: darken($gray, 18%); //#537994 + +$woo_pink1: #955a89; +$woo_pink2: #bb77ae; + + +$color_text_blue: #0073aa; +$color_button_primary: $woo_pink1; +$color_button_secondary: $woo_pink2; + +/*------------------------------------------------------------------------------ + Tab navigation +------------------------------------------------------------------------------*/ +.wc-helper { + + .nav-tab-wrapper { + margin-bottom: 22px; + } + + @media only screen and (max-width: 784px) { + + .nav-tab { + max-width: 40%; + overflow: hidden; + text-overflow: ellipsis; + } + } +} + + +/*------------------------------------------------------------------------------ + Buttons +------------------------------------------------------------------------------*/ + +.wc-helper { + + .button, + .button:hover, + .button:focus, + .button:active { + background-color: $color_button_primary; + border-width: 0; + box-shadow: none; + border-radius: 3px; + color: #fff; + height: auto; + padding: 3px 14px; + text-align: center; + white-space: normal !important; + + @media only screen and (max-width: 782px) { + line-height: 2; + } + + &.button-secondary { + background-color: #e6e6e6; + color: #3c3c3c; + text-shadow: none; + } + } + + .button:hover { + opacity: 0.8; + } +} + +.wc-helper .subscription-filter { + color: #2e4453; + font-size: 13px; + line-height: 13px; + margin: 22px 0; + + label { + display: none; + position: relative; + + .chevron { + color: #e1e1e1; + border-bottom-width: 0; + line-height: 1; + padding: 0; + position: absolute; + top: 10px; + right: 14px; + } + } + + li { + color: #0073aa; + display: inline-block; + padding: 0 4px 0 8px; + position: relative; + + &::before { + background-color: #979797; + content: " "; + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 1px; + } + + &:first-of-type { + + &::before { + display: none; + } + } + } + + a { + color: #0073aa; + text-decoration: none; + + &.current { + color: #000; + font-weight: 600; + } + } + + .count { + color: #555d66; + font-weight: 400; + } + + @media only screen and (max-width: 600px) { + background-color: #fff; + border: 1px solid #e1e1e1; + border-radius: 4px; + font-size: 14px; + + label, + li { + line-height: 21px; + padding: 8px 16px; + margin: 0; + + &:last-child { + border-bottom: none; + } + } + + li { + border-bottom: 1px solid #e1e1e1; + } + + label, + span.chevron { + display: block; + } + + label { + text-decoration: none; + } + + li { + display: none; + } + + li { + + &::before { + display: none; + } + } + + a { + cursor: pointer; + } + + span.chevron { + color: #555; + opacity: 0.5; + transform: rotateX(180deg); + } + + &:focus, + &:hover { + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + + label { + border-bottom: 1px solid #e1e1e1; + } + + li { + display: block; + } + + span.chevron { + transform: rotateX(0deg); + } + } + } + +} + +/*------------------------------------------------------------------------------ + Subscriptons Header +------------------------------------------------------------------------------*/ + +.wc-helper { + + .subscriptions-header { + margin: 3em 0 0; + position: relative; + z-index: 10; + + h2 { + display: inline-block; + line-height: 25px; + margin: 0 0 1.5em 0; + } + + } + + .button-update, + .button-update:hover { + background-color: #e6e6e6; + border-radius: 4px; + color: #333; + font-weight: 800; + font-size: 10px; + line-height: 20px; + margin-left: 6px; + opacity: 0.75; + padding: 3px 7px; + text-transform: uppercase; + + .dashicons { + font-size: 12px; + height: 12px; + width: 12px; + vertical-align: text-bottom; + } + } + + .button-update:hover { + opacity: 1; + } + + .user-info { + background-color: #fff; + border: 1px solid #e1e1e1; + border-radius: 4px; + font-size: 12px; + line-height: 26px; + position: absolute; + top: -10px; + right: 0; + transition: all 0.1s ease-in; + + @media only screen and (max-width: 600px) { + position: relative; + width: 100%; + } + + p { + line-height: 26px; + margin: 0; + } + + &:hover { + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + } + + header { + color: #555; + font-weight: 600; + padding: 6px 14px; + position: relative; + + p { + padding-right: 26px; + } + + .dashicons { + opacity: 0.5; + position: absolute; + top: 9px; + right: 14px; + } + + &:hover { + cursor: pointer; + } + } + + section { + display: none; + + p { + border-top: 1px solid #e1e1e1; + padding: 6px 14px; + text-align: center; + } + + .actions { + border-top: 1px solid #e1e1e1; + display: flex; + } + + a { + color: #a26897; + cursor: pointer; + font-weight: 600; + line-height: 38px; + padding: 0 14px; + text-align: center; + text-decoration: none; + white-space: nowrap; + width: 50%; + + .dashicons { + margin-top: -3px; + vertical-align: middle; + } + + &:first-child { + border-right: 1px solid #e1e1e1; + } + + &:hover { + background-color: #a26897; + color: #fff; + } + } + + .avatar { + border: 1px solid #ece1ea; + border-radius: 50%; + height: auto; + margin-right: 6px; + width: 24px; + vertical-align: bottom; + } + } + } + + .user-info:hover, + .user-info:focus, + .user-info:active { + + header .dashicons { + transform: rotateX(180deg); + } + + section { + display: block; + } + } +} + +/*------------------------------------------------------------------------------ + Subscripton table +------------------------------------------------------------------------------*/ + +.wc-helper { + + .striped > tbody > :nth-child(odd), + ul.striped > :nth-child(odd), + .alternate { + background-color: #fff; + } + + table.widefat, + .wp-editor-container, + .stuffbox, + p.popular-tags, + .widgets-holder-wrap, + .popular-tags, + .feature-filter, + .imgedit-group, + .comment-ays { + padding-top: 5px; + } + + .widefat thead tr th, + .widefat thead tr td, + .widefat tfoot tr th, + .widefat tfoot tr td { + color: #32373c; + padding-bottom: 15px; + padding-top: 10px; + } + + .widefat td { + padding-bottom: 15px; + padding-top: 15px; + } + + .wp-list-table { + border: 0; + box-shadow: none; + padding-top: 0 !important; + z-index: 1; + } + + .button { + + @media only screen and (max-width: 782px) { + font-size: 11px; + } + } + + .wp-list-table__row { + background-color: rgba(0, 0, 0, 0); + + td { + align-items: center; + background-color: #fff; + border: 0; + //border-top: 1px solid #e5e5e5; + padding: 16px 22px; + vertical-align: middle; + + @media only screen and (max-width: 782px) { + padding: 16px; + } + } + + td.color-bar { + border-left: 0; + } + + &.is-ext-header { + + td { + border-top: 1px solid #e1e1e1; + } + + @media only screen and (max-width: 782px) { + display: inline-flex; + flex-flow: row wrap; + width: 100%; + + .wp-list-table__ext-details { + display: block; + flex: 2; + } + + .wp-list-table__ext-actions { + display: block; + flex: 1; + min-width: 0; + } + } + } + + &:last-child td { + border-bottom: 24px solid #f1f1f1; + box-shadow: inset 0 -1px 0 #e1e1e1; + } + } + + .wp-list-table__ext-details, + .wp-list-table__ext-status, + .wp-list-table__licence-container { + padding-right: 22px; + position: relative; + width: 100%; + + &::before { + background-color: #e1e1e1; + content: " "; + position: absolute; + top: 0; + bottom: 0; + left: 0 !important; + width: 1px !important; + } + } + + .wp-list-table__ext-details { + display: flex; + + @media only screen and (max-width: 782px) { + display: table; + } + } + + .wp-list-table__ext-title { + color: $color_text_blue; + font-size: 18px; + font-weight: 600; + width: 60%; + + @media only screen and (max-width: 782px) { + margin-bottom: 12px; + width: 100%; + } + + @media only screen and (max-width: 320px) { + max-width: 120px; + } + } + + .wp-list-table__ext-description { + color: #333; + padding-left: 12px; + width: 40%; + + @media only screen and (max-width: 782px) { + padding-left: 0; + width: 100%; + } + } + + .wp-list-table__ext-status { + position: relative; + + &.update-available::after { + background-color: #ffc322; + content: " "; + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 5px; + } + + &.expired::after { + background-color: #b81c23; + content: " "; + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 5px; + } + + .dashicons-update { + color: #ffc322; + } + + .dashicons-info { + color: #b81c23; + } + + p { + color: #333; + margin: 0; + } + + .dashicons { + margin-right: 5px; + } + } + + .wp-list-table__ext-actions { + min-width: 150px; + position: relative; + width: 25%; + text-align: right; + + &::after { + background-color: #e1e1e1; + content: " "; + position: absolute; + top: 0; + bottom: 0; + right: 0; + width: 1px; + } + } + + .wp-list-table__ext-updates, + .wp-list-table__ext-licence { + + td { + position: relative; + + &::before { + background-color: #e1e1e1; + content: " "; + height: 1px; + position: absolute; + top: 0; + left: 0; + right: 0; + } + } + + td.wp-list-table__ext-status, + td.wp-list-table__licence-container { + + &::before { + left: 22px !important; + width: auto !important; + } + } + + td.wp-list-table__ext-actions::before { + right: 22px; + } + + @media only screen and (max-width: 782px) { + display: flex; + + .wp-list-table__ext-status { + flex: 2; + + &::before { + left: 0 !important; + width: 100% !important; + } + } + + .wp-list-table__ext-actions { + flex: 1; + min-width: 0; + + &::before { + left: 0 !important; + right: 0 !important; + width: 100% !important; + } + } + } + } + + .wp-list-table__licence-container { + padding: 0 !important; + + &::after { + background-color: #e1e1e1; + content: " "; + position: absolute; + top: 0; + bottom: 0; + right: 0; + width: 1px; + } + } + + .wp-list-table__licence-form { + display: flex; + padding: 16px 22px; + + @media only screen and (max-width: 782px) { + display: block; + } + + &::before { + background-color: #e1e1e1; + content: " "; + height: 1px; + position: absolute; + top: 0; + right: 22px; + left: 22px; + + @media only screen and (max-width: 782px) { + right: 0; + left: 0; + } + } + + div { + padding-right: 16px; + vertical-align: middle; + + @media only screen and (max-width: 782px) { + padding: 0; + } + } + + p { + margin: 0 !important; + } + } + + .wp-list-table__licence-label { + + label { + color: #23282d; + font-weight: 600; + line-height: 30px; + } + } + + .wp-list-table__licence-field { + + input { + height: 32px; + + @media only screen and (max-width: 480px) { + width: 100%; + } + } + + @media only screen and (max-width: 782px) { + padding: 8px 0 16px !important; + } + } + + .wp-list-table__licence-actions { + flex-grow: 2; + padding-right: 0 !important; + + .button { + margin-right: 8px; + } + + .button-secondary { + float: right; + margin: 0 0 0 8px; + } + + @media only screen and (max-width: 480px) { + text-align: right; + } + } +} + +/*------------------------------------------------------------------------------ + Expired notification bar +------------------------------------------------------------------------------*/ + +.wc-helper { + + td.color-bar { + border-left: solid 4px transparent; + } + + td.color-bar.expired { + border-left-color: #b81c23; + } + + td.color-bar.expiring { + border-left-color: orange; + } + + td.color-bar.update-available { + border-left-color: #8fae1b; + } + + td.color-bar.expiring.update-available { + border-left-color: #8fae1b; + } +} + +/*------------------------------------------------------------------------------ + Connected account table +------------------------------------------------------------------------------*/ + +.wc-helper { + + .connect-wrapper { + background-color: #fff; + border: 1px solid #e5e5e5; + margin-bottom: 25px; + overflow: auto; + } + + .connected { + display: flex; + + .user-info { + display: flex; + padding: 20px; + width: 100%; + vertical-align: middle; + } + + img { + border: 1px solid #e5e5e5; + height: 34px; + width: 34px; + } + + .buttons { + padding: 20px; + white-space: nowrap; + } + + p { + flex: 2; + margin: 10px 0 0 20px; + } + + .chevron { + display: none; + + &:hover { + color: $woo_pink1; + cursor: pointer; + } + } + + @media only screen and (max-width: 784px) { + display: block; + + strong { + display: block; + overflow: hidden; + text-overflow: ellipsis; + } + + p { + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + width: 80%; + } + + .user-info { + padding-right: 0; + width: auto; + } + + .avatar { + margin-right: 12px; + } + + .chevron { + color: #e1e1e1; + display: block; + margin: 10px; + transform: rotateX(0deg); + } + + .buttons { + display: none; + border-top: 1px solid #e1e1e1; + padding: 10px 20px; + + &.active { + display: block; + } + + } + } + } +} + +/*------------------------------------------------------------------------------ + Initial connection screen +------------------------------------------------------------------------------*/ + +.wc-helper { + + .start-container { + background-color: #fff; + border-left: 4px solid #cc99c2; + padding: 45px 20px 20px 30px; + position: relative; + overflow: hidden; + + h2, + p { + max-width: 800px; + } + } + + .start-container::before { + color: #eee2ec; + content: "\e01C"; + display: block; + font-family: WooCommerce; + font-size: 192px; + line-height: 1; + position: absolute; + top: 65%; + right: -3%; + text-align: center; + width: 1em; + } + + .start-container h2 { + font-size: 24px; + line-height: 29px; + position: relative; + } + + .start-container p { + font-size: 16px; + margin-bottom: 30px; + position: relative; + } + + .button-helper-connect { + height: 37px; + line-height: 37px; + min-width: 124px; + padding: 0 13px; + text-shadow: none; + + &:hover, + &:active, + &:focus { + padding: 0 13px; + } + } +} + + +// ========================================================================== +// FormToggle +// ========================================================================== + +.form-toggle__wrapper { + position: relative; + + label { + cursor: default; + } +} + +.form-toggle { + cursor: pointer; + display: block; + position: absolute; + top: 0; + bottom: -1px; + left: 0; + right: 0; + text-align: left; + text-indent: -100000px; + z-index: 2; + + &:focus { + box-shadow: none; + } + + &.disabled { + cursor: default; + } +} + +.form-toggle__switch { + align-self: flex-start; + background: lighten($gray, 20%); + border-radius: 12px; + box-sizing: border-box; + display: inline-block; + padding: 2px; + outline: 0; + position: relative; + width: 40px; + height: 24px; + transition: all 0.4s ease, box-shadow 0s; + vertical-align: middle; + + &::before, + &::after { + content: ""; + display: block; + position: relative; + width: 20px; + height: 20px; + } + + &::after { + border-radius: 50%; + background: $white; + left: 0; + transition: all 0.2s ease; + } + + &::before { + display: none; + } + + .accessible-focus &:focus { + box-shadow: 0 0 0 2px $woo_pink1; + } + + +} + +.form-toggle__label { + vertical-align: bottom; + z-index: 1; + + .form-toggle__label-content { + color: #87a6bc; + flex: 0 1 100%; + font-size: 13px; + line-height: 16px; + margin-left: 12px; + margin-right: 8px; + vertical-align: top; + text-transform: uppercase; + + @media only screen and (max-width: 480px) { + display: none; + } + } +} + +.form-toggle { + + .accessible-focus &:focus { + + + .form-toggle__label .form-toggle__switch { + box-shadow: 0 0 0 2px $woo_pink1; + } + + &:checked + .form-toggle__label .form-toggle__switch { + box-shadow: 0 0 0 2px $woo_pink2; + } + } + + & + .form-toggle__label .form-toggle__switch { + background: lighten($gray, 10%); + } + + &:not(:disabled) { + + + .form-toggle__label:hover .form-toggle__switch { + background: lighten($gray, 20%); + } + } + + &.active { + + + .form-toggle__label .form-toggle__switch { + background: $woo_pink1; + + &::after { + left: 8px; + } + } + + + .form-toggle__label:hover .form-toggle__switch { + background: $woo_pink2; + } + } + + &.disabled { + + + label.form-toggle__label span.form-toggle__switch { + opacity: 0.25; + } + } +} + +// Classes for toggle state before action is complete (updating plugin or something) +.form-toggle.is-toggling { + + + .form-toggle__label .form-toggle__switch { + background: $woo_pink1; + } + + &:checked { + + + .form-toggle__label .form-toggle__switch { + background: lighten($gray, 20%); + } + } +} + +.form-toggle.is-compact { + + + .form-toggle__label .form-toggle__switch { + border-radius: 8px; + width: 24px; + height: 16px; + + &::before, + &::after { + height: 12px; + width: 12px; + } + } + + &:checked { + + + .form-toggle__label .form-toggle__switch { + + &::after { + left: 8px; + } + } + } +} diff --git a/assets/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png b/assets/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100755 index 0000000..a2e6bfc Binary files /dev/null and b/assets/css/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png differ diff --git a/assets/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png b/assets/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png new file mode 100755 index 0000000..e36540b Binary files /dev/null and b/assets/css/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png differ diff --git a/assets/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png b/assets/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png new file mode 100755 index 0000000..2763b50 Binary files /dev/null and b/assets/css/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png differ diff --git a/assets/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png b/assets/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100755 index 0000000..5c1e17f Binary files /dev/null and b/assets/css/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/assets/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png b/assets/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png new file mode 100755 index 0000000..c712254 Binary files /dev/null and b/assets/css/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png differ diff --git a/assets/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png b/assets/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png new file mode 100755 index 0000000..693b8d1 Binary files /dev/null and b/assets/css/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png differ diff --git a/assets/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png b/assets/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png new file mode 100755 index 0000000..857314a Binary files /dev/null and b/assets/css/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png differ diff --git a/assets/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/assets/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png new file mode 100755 index 0000000..a54ca8c Binary files /dev/null and b/assets/css/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ diff --git a/assets/css/jquery-ui/images/ui-icons_222222_256x240.png b/assets/css/jquery-ui/images/ui-icons_222222_256x240.png new file mode 100644 index 0000000..e61b7ee Binary files /dev/null and b/assets/css/jquery-ui/images/ui-icons_222222_256x240.png differ diff --git a/assets/css/jquery-ui/images/ui-icons_2e83ff_256x240.png b/assets/css/jquery-ui/images/ui-icons_2e83ff_256x240.png new file mode 100644 index 0000000..d578ae2 Binary files /dev/null and b/assets/css/jquery-ui/images/ui-icons_2e83ff_256x240.png differ diff --git a/assets/css/jquery-ui/images/ui-icons_454545_256x240.png b/assets/css/jquery-ui/images/ui-icons_454545_256x240.png new file mode 100644 index 0000000..d7ebd23 Binary files /dev/null and b/assets/css/jquery-ui/images/ui-icons_454545_256x240.png differ diff --git a/assets/css/jquery-ui/images/ui-icons_888888_256x240.png b/assets/css/jquery-ui/images/ui-icons_888888_256x240.png new file mode 100644 index 0000000..e1caba9 Binary files /dev/null and b/assets/css/jquery-ui/images/ui-icons_888888_256x240.png differ diff --git a/assets/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png b/assets/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png new file mode 100644 index 0000000..ab58528 Binary files /dev/null and b/assets/css/jquery-ui/images/ui-icons_cd0a0a_256x240.png differ diff --git a/assets/css/jquery-ui/jquery-ui-rtl.css b/assets/css/jquery-ui/jquery-ui-rtl.css new file mode 100644 index 0000000..8a2e77a --- /dev/null +++ b/assets/css/jquery-ui/jquery-ui-rtl.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.11.4 - 2015-03-11 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:after,.ui-helper-clearfix:before{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;right:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;right:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .7em .5em .5em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-right:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-right:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;right:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;right:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-left:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:active,.ui-button:hover,.ui-button:link,.ui-button:visited{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icons .ui-button-text{padding-right:2.1em;padding-left:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-icons-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{right:50%;margin-right:-8px}.ui-button-icons-only .ui-button-icon-primary,.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary{right:.5em}.ui-button-icons-only .ui-button-icon-secondary,.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary{left:.5em}.ui-buttonset{margin-left:7px}.ui-buttonset .ui-button{margin-right:0;margin-left:-.3em}button.ui-button::-moz-focus-inner,input.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-next,.ui-datepicker .ui-datepicker-prev{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-next-hover,.ui-datepicker .ui-datepicker-prev-hover{top:1px}.ui-datepicker .ui-datepicker-prev{right:2px}.ui-datepicker .ui-datepicker-next{left:2px}.ui-datepicker .ui-datepicker-prev-hover{right:1px}.ui-datepicker .ui-datepicker-next-hover{left:1px}.ui-datepicker .ui-datepicker-next span,.ui-datepicker .ui-datepicker-prev span{display:block;position:absolute;right:50%;margin-right:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:700;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td a,.ui-datepicker td span{display:block;padding:.2em;text-align:left;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-right:0;border-left:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:left;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:right}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:right}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:right}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:ltr}.ui-datepicker-rtl .ui-datepicker-prev{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-next{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:left}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0;border-right-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;right:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:right;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;left:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:100% 0;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:right;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em .4em .5em 1em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:left}.ui-dialog .ui-dialog-buttonpane button{margin:.5em 0 .5em .4em;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;left:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px .4em 3px 1em;cursor:pointer;min-height:0;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-active,.ui-menu .ui-state-focus{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-right:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;right:.2em;margin:auto 0}.ui-menu .ui-menu-icon{right:auto;left:0}.ui-progressbar{height:2em;text-align:right;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url(data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==);height:100%;opacity:.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-autohide .ui-resizable-handle,.ui-resizable-disabled .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;right:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;right:0}.ui-resizable-e{cursor:e-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-se{cursor:sw-resize;width:12px;height:12px;left:1px;bottom:1px}.ui-resizable-sw{cursor:se-resize;width:9px;height:9px;right:-5px;bottom:-5px}.ui-resizable-nw{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-resizable-ne{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted #000}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;right:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:700;line-height:1.5;padding:2px .4em;margin:.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{left:.5em;right:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:right;padding:.4em 1em .4em 2.1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:right}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:100% 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{-webkit-filter:inherit;filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-right:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{right:0}.ui-slider-horizontal .ui-slider-range-max{left:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{right:-.3em;margin-right:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{right:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:100% 0;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-right:.4em;margin-left:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;left:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-left:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;right:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:right;position:relative;top:0;margin:1px 0 0 .2em;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:right;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:100% 0}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget button,.ui-widget input,.ui-widget select,.ui-widget textarea{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:700}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:400;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-focus,.ui-state-hover,.ui-widget-content .ui-state-focus,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-focus,.ui-widget-header .ui-state-hover{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:400;color:#212121}.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:400;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:700}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:400}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-focus .ui-icon,.ui-state-hover .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:100% 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:100% -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:100% -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:100% -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:100% -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:100% -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:100% -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:100% -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:100% -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:100% -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:100% -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:100% -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:100% -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:100% -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:100% -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-left,.ui-corner-tl,.ui-corner-top{border-top-right-radius:4px}.ui-corner-all,.ui-corner-right,.ui-corner-top,.ui-corner-tr{border-top-left-radius:4px}.ui-corner-all,.ui-corner-bl,.ui-corner-bottom,.ui-corner-left{border-bottom-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-br,.ui-corner-right{border-bottom-left-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px -8px 0 0;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} diff --git a/assets/css/jquery-ui/jquery-ui.css b/assets/css/jquery-ui/jquery-ui.css new file mode 100644 index 0000000..fe47dc0 --- /dev/null +++ b/assets/css/jquery-ui/jquery-ui.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.11.4 - 2015-03-11 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:after,.ui-helper-clearfix:before{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:active,.ui-button:hover,.ui-button:link,.ui-button:visited{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-icons-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-icons-only .ui-button-icon-primary,.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary{left:.5em}.ui-button-icons-only .ui-button-icon-secondary,.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}button.ui-button::-moz-focus-inner,input.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-next,.ui-datepicker .ui-datepicker-prev{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-next-hover,.ui-datepicker .ui-datepicker-prev-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-next span,.ui-datepicker .ui-datepicker-prev span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:700;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td a,.ui-datepicker td span{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:0 0;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-active,.ui-menu .ui-state-focus{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url(data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==);height:100%;opacity:.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-autohide .ui-resizable-handle,.ui-resizable-disabled .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted #000}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:700;line-height:1.5;padding:2px .4em;margin:.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{right:.5em;left:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:left;padding:.4em 2.1em .4em 1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{-webkit-filter:inherit;filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:0 0;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:0 0}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget button,.ui-widget input,.ui-widget select,.ui-widget textarea{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:700}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:400;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-focus,.ui-state-hover,.ui-widget-content .ui-state-focus,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-focus,.ui-widget-header .ui-state-hover{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:400;color:#212121}.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited,.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:400;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:700}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:400}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-focus .ui-icon,.ui-state-hover .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-left,.ui-corner-tl,.ui-corner-top{border-top-left-radius:4px}.ui-corner-all,.ui-corner-right,.ui-corner-top,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bl,.ui-corner-bottom,.ui-corner-left{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-br,.ui-corner-right{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} diff --git a/assets/css/jquery-ui/jquery-ui.min.css b/assets/css/jquery-ui/jquery-ui.min.css new file mode 100644 index 0000000..602662c --- /dev/null +++ b/assets/css/jquery-ui/jquery-ui.min.css @@ -0,0 +1,7 @@ +/*! jQuery UI - v1.11.4 - 2015-03-11 +* http://jqueryui.com +* Includes: core.css, accordion.css, autocomplete.css, button.css, datepicker.css, dialog.css, draggable.css, menu.css, progressbar.css, resizable.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + +.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;min-height:0;font-size:100%}.ui-accordion .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-icons .ui-accordion-icons{padding-left:2.2em}.ui-accordion .ui-accordion-header .ui-accordion-header-icon{position:absolute;left:.5em;top:50%;margin-top:-8px}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-button{display:inline-block;position:relative;padding:0;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2.2em}button.ui-button-icon-only{width:2.4em}.ui-button-icons-only{width:3.4em}button.ui-button-icons-only{width:3.7em}.ui-button .ui-button-text{display:block;line-height:normal}.ui-button-text-only .ui-button-text{padding:.4em 1em}.ui-button-icon-only .ui-button-text,.ui-button-icons-only .ui-button-text{padding:.4em;text-indent:-9999999px}.ui-button-text-icon-primary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 1em .4em 2.1em}.ui-button-text-icon-secondary .ui-button-text,.ui-button-text-icons .ui-button-text{padding:.4em 2.1em .4em 1em}.ui-button-text-icons .ui-button-text{padding-left:2.1em;padding-right:2.1em}input.ui-button{padding:.4em 1em}.ui-button-icon-only .ui-icon,.ui-button-text-icon-primary .ui-icon,.ui-button-text-icon-secondary .ui-icon,.ui-button-text-icons .ui-icon,.ui-button-icons-only .ui-icon{position:absolute;top:50%;margin-top:-8px}.ui-button-icon-only .ui-icon{left:50%;margin-left:-8px}.ui-button-text-icon-primary .ui-button-icon-primary,.ui-button-text-icons .ui-button-icon-primary,.ui-button-icons-only .ui-button-icon-primary{left:.5em}.ui-button-text-icon-secondary .ui-button-icon-secondary,.ui-button-text-icons .ui-button-icon-secondary,.ui-button-icons-only .ui-button-icon-secondary{right:.5em}.ui-buttonset{margin-right:7px}.ui-buttonset .ui-button{margin-left:0;margin-right:-.3em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-dialog{overflow:hidden;position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-se{width:12px;height:12px;right:-5px;bottom:-5px;background-position:16px 16px}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:none}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{position:relative;margin:0;padding:3px 1em 3px .4em;cursor:pointer;min-height:0;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-button{display:inline-block;overflow:hidden;position:relative;text-decoration:none;cursor:pointer}.ui-selectmenu-button span.ui-icon{right:0.5em;left:auto;margin-top:-8px;position:absolute;top:50%}.ui-selectmenu-button span.ui-selectmenu-text{text-align:left;padding:0.4em 2.1em 0.4em 1em;display:block;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{-webkit-filter:inherit;filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:22px}.ui-spinner-button{width:16px;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top:none;border-bottom:none;border-right:none}.ui-spinner .ui-icon{position:absolute;margin-top:-8px;top:50%;left:0}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-spinner .ui-icon-triangle-1-s{background-position:-65px -16px}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px;-webkit-box-shadow:0 0 5px #aaa;box-shadow:0 0 5px #aaa}body .ui-tooltip{border-width:2px}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url("images/ui-bg_flat_75_ffffff_40x100.png") 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url("images/ui-bg_highlight-soft_75_cccccc_1x100.png") 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url("images/ui-bg_glass_75_e6e6e6_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url("images/ui-bg_glass_75_dadada_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited,.ui-state-focus a,.ui-state-focus a:hover,.ui-state-focus a:link,.ui-state-focus a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url("images/ui-bg_glass_65_ffffff_1x400.png") 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url("images/ui-bg_glass_55_fbf9ee_1x400.png") 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url("images/ui-bg_glass_95_fef1ec_1x400.png") 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-widget-header .ui-icon{background-image:url("images/ui-icons_222222_256x240.png")}.ui-state-default .ui-icon{background-image:url("images/ui-icons_888888_256x240.png")}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-active .ui-icon{background-image:url("images/ui-icons_454545_256x240.png")}.ui-state-highlight .ui-icon{background-image:url("images/ui-icons_2e83ff_256x240.png")}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url("images/ui-icons_cd0a0a_256x240.png")}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url("images/ui-bg_flat_0_aaaaaa_40x100.png") 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} \ No newline at end of file diff --git a/assets/css/marketplace-suggestions-rtl.css b/assets/css/marketplace-suggestions-rtl.css new file mode 100644 index 0000000..547927b --- /dev/null +++ b/assets/css/marketplace-suggestions-rtl.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--woocommerce:#a46497;--wc-green:#7ad03a;--wc-red:#a00;--wc-orange:#ffba00;--wc-blue:#2ea2cc;--wc-primary:#a46497;--wc-primary-text:white;--wc-secondary:#ebe9eb;--wc-secondary-text:#515151;--wc-highlight:#77a464;--wc-highligh-text:white;--wc-content-bg:#fff;--wc-subtext:#767676}a.suggestion-dismiss{border:none;box-shadow:none;color:#ddd}a.suggestion-dismiss:hover{color:#aaa}a.suggestion-dismiss::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;content:"";text-decoration:none;font-size:1.5em}#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_tab a::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;content:"";text-decoration:none}@media only screen and (max-width:900px){#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_tab a::before{line-height:40px}}#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_tab a span{margin:0 .618em}.marketplace-suggestions-metabox-nosuggestions-placeholder{max-width:325px;margin:2em auto;text-align:center}.marketplace-suggestions-metabox-nosuggestions-placeholder .marketplace-suggestion-placeholder-content{margin-bottom:1em}.marketplace-suggestions-metabox-nosuggestions-placeholder a,.marketplace-suggestions-metabox-nosuggestions-placeholder h4,.marketplace-suggestions-metabox-nosuggestions-placeholder p{margin:auto;text-align:center;display:block;margin-top:.75em;line-height:1.75}.marketplace-suggestions-container.showing-suggestion{text-align:right}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container{-webkit-box-align:start;align-items:flex-start;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;position:relative}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container img.marketplace-suggestion-icon{height:40px;margin:0;margin-left:1.5em;-webkit-box-flex:0;flex:0 0 40px}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-content{-webkit-box-flex:1;flex:1 1 60%}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-content h4{margin:0}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-content p{margin:0;margin-top:4px;color:#444}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-cta{-webkit-box-flex:1;flex:1 1 30%;min-width:160px;text-align:left}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss{text-decoration:none;position:absolute;top:1em;left:1em}@media screen and (min-width:600px){.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container{-webkit-box-align:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row}}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-content h4{font-size:1.1em;margin:0;margin-bottom:0}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer]{margin-bottom:6em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;flex-direction:row-reverse}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta{text-align:right}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content.has-manage-link,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content.has-manage-link,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content.has-manage-link{text-align:left}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container{padding:1em 1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-content p,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content p,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-content p{padding:0;line-height:1.5}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container{padding:1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container{padding:.75em 1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container:first-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container:first-child{padding-top:1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container:last-child{padding-bottom:1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-content p:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-content p:last-child{margin-bottom:0}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header]{display:none}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button{display:inline-block;min-width:120px;text-align:center;margin:0}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout{font-size:1.1em;text-decoration:none}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons{margin-right:4px;bottom:2px;position:relative}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss{position:relative;top:5px;left:auto;margin-right:1em}@media screen and (min-width:600px){.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header]{display:block}}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header]{border:none}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body]{border:none;border-top:1px solid #eee;border-bottom:1px solid #eee}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header]{border:1px solid #ddd;border-bottom:none}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header]:last-child{border-bottom:1px solid #ddd} \ No newline at end of file diff --git a/assets/css/marketplace-suggestions.css b/assets/css/marketplace-suggestions.css new file mode 100644 index 0000000..dff919b --- /dev/null +++ b/assets/css/marketplace-suggestions.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--woocommerce:#a46497;--wc-green:#7ad03a;--wc-red:#a00;--wc-orange:#ffba00;--wc-blue:#2ea2cc;--wc-primary:#a46497;--wc-primary-text:white;--wc-secondary:#ebe9eb;--wc-secondary-text:#515151;--wc-highlight:#77a464;--wc-highligh-text:white;--wc-content-bg:#fff;--wc-subtext:#767676}a.suggestion-dismiss{border:none;box-shadow:none;color:#ddd}a.suggestion-dismiss:hover{color:#aaa}a.suggestion-dismiss::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;content:"";text-decoration:none;font-size:1.5em}#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_tab a::before{font-family:Dashicons;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;content:"";text-decoration:none}@media only screen and (max-width:900px){#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_tab a::before{line-height:40px}}#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_tab a span{margin:0 .618em}.marketplace-suggestions-metabox-nosuggestions-placeholder{max-width:325px;margin:2em auto;text-align:center}.marketplace-suggestions-metabox-nosuggestions-placeholder .marketplace-suggestion-placeholder-content{margin-bottom:1em}.marketplace-suggestions-metabox-nosuggestions-placeholder a,.marketplace-suggestions-metabox-nosuggestions-placeholder h4,.marketplace-suggestions-metabox-nosuggestions-placeholder p{margin:auto;text-align:center;display:block;margin-top:.75em;line-height:1.75}.marketplace-suggestions-container.showing-suggestion{text-align:left}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container{-webkit-box-align:start;align-items:flex-start;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;position:relative}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container img.marketplace-suggestion-icon{height:40px;margin:0;margin-right:1.5em;-webkit-box-flex:0;flex:0 0 40px}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-content{-webkit-box-flex:1;flex:1 1 60%}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-content h4{margin:0}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-content p{margin:0;margin-top:4px;color:#444}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-cta{-webkit-box-flex:1;flex:1 1 30%;min-width:160px;text-align:right}.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss{text-decoration:none;position:absolute;top:1em;right:1em}@media screen and (min-width:600px){.marketplace-suggestions-container.showing-suggestion .marketplace-suggestion-container{-webkit-box-align:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;flex-direction:row}}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content h4,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-content h4{font-size:1.1em;margin:0;margin-bottom:0}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer]{margin-bottom:6em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;flex-direction:row-reverse}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta{text-align:left}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content.has-manage-link,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content.has-manage-link,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content.has-manage-link{text-align:right}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container{padding:1em 1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-content p,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-content p,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-content p{padding:0;line-height:1.5}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container{padding:1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container{padding:.75em 1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container:first-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container:first-child{padding-top:1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container:last-child{padding-bottom:1.5em}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-content p:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-content p:last-child{margin-bottom:0}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header]{display:none}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.button{display:inline-block;min-width:120px;text-align:center;margin:0}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout{font-size:1.1em;text-decoration:none}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta a.linkout .dashicons{margin-left:4px;bottom:2px;position:relative}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header] .marketplace-suggestion-container .marketplace-suggestion-container-cta .suggestion-dismiss{position:relative;top:5px;right:auto;margin-left:1em}@media screen and (min-width:600px){.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header]{display:block}}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-header]{border:none}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=product-edit-meta-tab-body]{border:none;border-top:1px solid #eee;border-bottom:1px solid #eee}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer],.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header]{border:1px solid #ddd;border-bottom:none}.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-body]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-footer]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=orders-list-empty-header]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-body]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-footer]:last-child,.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context=products-list-empty-header]:last-child{border-bottom:1px solid #ddd} \ No newline at end of file diff --git a/assets/css/marketplace-suggestions.scss b/assets/css/marketplace-suggestions.scss new file mode 100644 index 0000000..342e947 --- /dev/null +++ b/assets/css/marketplace-suggestions.scss @@ -0,0 +1,304 @@ +/** + * marketplace-suggestions.scss + * Styling for in-product marketplace suggestions. + */ + +@import "mixins"; +@import "variables"; + +$suggestions-pale-gray: #ddd; +$suggestions-metabox-pale-gray: #eee; + +$suggestions-copy-text: #444; + +a.suggestion-dismiss { + border: none; + box-shadow: none; + color: $suggestions-pale-gray; +} + +a.suggestion-dismiss:hover { + color: #aaa; +} + +a.suggestion-dismiss::before { + + @include iconbeforedashicons( "\f335" ); + + font-size: 1.5em; +} + +#woocommerce-product-data ul.wc-tabs li.marketplace-suggestions_tab { + + + a::before { + + @include iconbeforedashicons( "\f106" ); + + @media only screen and (max-width: 900px) { + line-height: 40px; + } + } + + a span { + margin: 0 0.618em; + } +} + +.marketplace-suggestions-metabox-nosuggestions-placeholder { + max-width: 325px; + margin: 2em auto; + text-align: center; + + .marketplace-suggestion-placeholder-content { + margin-bottom: 1em; + } + + h4, + a, + p { + margin: auto; + text-align: center; + display: block; + margin-top: 0.75em; + line-height: 1.75; + } +} + +.marketplace-suggestions-container.showing-suggestion { + text-align: left; + + .marketplace-suggestion-container { + align-items: flex-start; + display: flex; + flex-direction: column; + + // Allows us to position the dismiss x button + // relative to container on mobile. + position: relative; + + img.marketplace-suggestion-icon { + height: 40px; + margin: 0; + margin-right: 1.5em; + flex: 0 0 40px; + } + + .marketplace-suggestion-container-content { + flex: 1 1 60%; + + h4 { + margin: 0; + } + + p { + margin: 0; + margin-top: 4px; + color: $suggestions-copy-text; + } + } + + .marketplace-suggestion-container-cta { + flex: 1 1 30%; + min-width: 160px; + text-align: right; + + .suggestion-dismiss { + text-decoration: none; + position: absolute; + top: 1em; + right: 1em; + } + } + } + + @media screen and (min-width: 600px) { + + .marketplace-suggestion-container { + align-items: center; + flex-direction: row; + + img.marketplace-suggestion-icon { + // display: inline-block; + } + } + } +} + +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"] { + + .marketplace-suggestion-container { + + .marketplace-suggestion-container-content { + + h4 { + font-size: 1.1em; + margin: 0; + margin-bottom: 0; + } + } + } +} + +// Additional breathing space margin under empty-state footer. +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"] { + + margin-bottom: 6em; +} + + +// Optimise footer suggestion layout for left-aligned CTA link button only. +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"] { + + .marketplace-suggestion-container { + + flex-direction: row-reverse; + + .marketplace-suggestion-container-cta { + + text-align: left; + } + + .marketplace-suggestion-container-content.has-manage-link { + text-align: right; + } + } +} + + +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-body"] { + + .marketplace-suggestion-container { + padding: 1em 1.5em; + + .marketplace-suggestion-container-content { + + p { + padding: 0; + line-height: 1.5; + } + } + } +} + +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"] { + + .marketplace-suggestion-container { + padding: 1.5em; + } +} + +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-body"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-body"] { + + .marketplace-suggestion-container { + padding: 0.75em 1.5em; + + &:first-child { + padding-top: 1.5em; + } + + &:last-child { + padding-bottom: 1.5em; + } + + .marketplace-suggestion-container-content { + + p:last-child { + margin-bottom: 0; + } + } + } +} + +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-body"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-body"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-body"] { + + // hide by default (mobile first) + display: none; + + .marketplace-suggestion-container .marketplace-suggestion-container-cta { + + a.button { + display: inline-block; + min-width: 120px; + text-align: center; + margin: 0; + } + + a.linkout { + font-size: 1.1em; + text-decoration: none; + } + + a.linkout .dashicons { + margin-left: 4px; + bottom: 2px; + position: relative; + } + + .suggestion-dismiss { + position: relative; + top: 5px; + right: auto; + margin-left: 1em; + } + } + + @media screen and (min-width: 600px) { + + // Display onboarding table suggestion on desktop only. (for now) + // There's limited room on mobile, and there are edge-case + // styling issues in some browsers. + display: block; + } +} + + +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-footer"] { + + border: none; +} + +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="product-edit-meta-tab-body"] { + + border: none; + border-top: 1px solid $suggestions-metabox-pale-gray; + border-bottom: 1px solid $suggestions-metabox-pale-gray; +} + +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="products-list-empty-body"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-header"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-footer"], +.marketplace-suggestions-container.showing-suggestion[data-marketplace-suggestions-context="orders-list-empty-body"] { + + border: 1px solid $suggestions-pale-gray; + border-bottom: none; + + &:last-child { + border-bottom: 1px solid $suggestions-pale-gray; + } +} diff --git a/assets/css/menu-rtl.css b/assets/css/menu-rtl.css new file mode 100644 index 0000000..70b99b9 --- /dev/null +++ b/assets/css/menu-rtl.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--woocommerce:#a46497;--wc-green:#7ad03a;--wc-red:#a00;--wc-orange:#ffba00;--wc-blue:#2ea2cc;--wc-primary:#a46497;--wc-primary-text:white;--wc-secondary:#ebe9eb;--wc-secondary-text:#515151;--wc-highlight:#77a464;--wc-highligh-text:white;--wc-content-bg:#fff;--wc-subtext:#767676}@font-face{font-family:star;src:url(../fonts/star.eot);src:url(../fonts/star.eot?#iefix) format("embedded-opentype"),url(../fonts/star.woff) format("woff"),url(../fonts/star.ttf) format("truetype"),url(../fonts/star.svg#star) format("svg");font-weight:400;font-style:normal}@font-face{font-family:WooCommerce;src:url(../fonts/WooCommerce.eot);src:url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"),url(../fonts/WooCommerce.woff) format("woff"),url(../fonts/WooCommerce.ttf) format("truetype"),url(../fonts/WooCommerce.svg#WooCommerce) format("svg");font-weight:400;font-style:normal}span.mce_woocommerce_shortcodes_button{background-image:none!important;display:block;text-indent:-9999px;position:relative;height:1em;width:1em}span.mce_woocommerce_shortcodes_button::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;right:0;width:100%;height:100%;text-align:center;content:"";font-size:.9em;line-height:1.2}#woocommerce-update .updating-message .wc_plugin_upgrade_notice{display:none}#woocommerce-update .dummy{display:none}#woocommerce-update .wc_plugin_upgrade_notice{font-weight:400;background:#fff8e5!important;border-right:4px solid #ffb900;border-top:1px solid #ffb900;padding:9px 12px 9px 0!important;margin:0 -16px 0 -12px!important}#woocommerce-update .wc_plugin_upgrade_notice::before{content:"\f348";display:inline-block;font:400 18px/1 dashicons;speak:never;margin:0 -2px 0 8px;vertical-align:top}#woocommerce-update .wc_plugin_upgrade_notice.major,#woocommerce-update .wc_plugin_upgrade_notice.minor{padding:20px 0!important}#woocommerce-update .wc_plugin_upgrade_notice.major::before,#woocommerce-update .wc_plugin_upgrade_notice.minor::before{display:none}#woocommerce-update .wc_plugin_upgrade_notice.major p,#woocommerce-update .wc_plugin_upgrade_notice.minor p{padding:0 20px;margin:0;max-width:700px;line-height:1.5em}#woocommerce-update .wc_plugin_upgrade_notice.major p::before,#woocommerce-update .wc_plugin_upgrade_notice.minor p::before{content:"";display:none}#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table{margin:.75em 0 0}#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table tr,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table tr{background:transparent none!important;border:0!important}#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table td,#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table th,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table td,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table th{background:transparent none!important;margin:0;padding:.75em 20px 0;border:0!important;font-size:1em;box-shadow:none}#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table th,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table th{font-weight:700}#wc_untested_extensions_modal{display:none}.wc_untested_extensions_modal_container{border-radius:4px;padding:0}.wc_untested_extensions_modal_container #TB_closeAjaxWindow{display:none}.wc_untested_extensions_modal_container #TB_title{display:none}.wc_untested_extensions_modal_container #TB_ajaxContent{height:100%!important;padding:0;margin:0;width:100%!important}.wc_untested_extensions_modal_container #TB_ajaxContent p{margin:0 0 1em}.wc_untested_extensions_modal--content h1{margin:2px 2px .5em;padding:.75em 1em;line-height:1.5em;font-size:2em;border-bottom:1px solid #eee;color:#fff;background:#96578a;border-top-right-radius:4px;border-top-left-radius:4px;text-shadow:none}.wc_untested_extensions_modal--content .extensions_warning{padding:0 2em}.wc_untested_extensions_modal--content .plugin-details-table-container{max-height:40vh;overflow-y:auto}.wc_untested_extensions_modal--content table.plugin-details-table{margin:20px 0}.wc_untested_extensions_modal--content table.plugin-details-table td,.wc_untested_extensions_modal--content table.plugin-details-table th{background:transparent none!important;margin:0;padding:.75em 20px 0;border:0!important;font-size:1em;box-shadow:none}.wc_untested_extensions_modal--content table.plugin-details-table th{font-weight:700;margin-top:0}.wc_untested_extensions_modal--content .actions{border-top:1px solid #eee;margin:0;padding:1em 0 2em 0;overflow:hidden}.wc_untested_extensions_modal--content .actions .woocommerce-actions{display:inline-block}.wc_untested_extensions_modal--content .actions a.button-primary{float:left;background:#bb77ae;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597;color:#fff;text-shadow:0 -1px 1px #a36597,-1px 0 1px #a36597,0 1px 1px #a36597,1px 0 1px #a36597}.wc_untested_extensions_modal--content .actions a.button-primary:active,.wc_untested_extensions_modal--content .actions a.button-primary:focus,.wc_untested_extensions_modal--content .actions a.button-primary:hover{background:#a36597;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597} \ No newline at end of file diff --git a/assets/css/menu.css b/assets/css/menu.css new file mode 100644 index 0000000..30da316 --- /dev/null +++ b/assets/css/menu.css @@ -0,0 +1 @@ +@charset "UTF-8";:root{--woocommerce:#a46497;--wc-green:#7ad03a;--wc-red:#a00;--wc-orange:#ffba00;--wc-blue:#2ea2cc;--wc-primary:#a46497;--wc-primary-text:white;--wc-secondary:#ebe9eb;--wc-secondary-text:#515151;--wc-highlight:#77a464;--wc-highligh-text:white;--wc-content-bg:#fff;--wc-subtext:#767676}@font-face{font-family:star;src:url(../fonts/star.eot);src:url(../fonts/star.eot?#iefix) format("embedded-opentype"),url(../fonts/star.woff) format("woff"),url(../fonts/star.ttf) format("truetype"),url(../fonts/star.svg#star) format("svg");font-weight:400;font-style:normal}@font-face{font-family:WooCommerce;src:url(../fonts/WooCommerce.eot);src:url(../fonts/WooCommerce.eot?#iefix) format("embedded-opentype"),url(../fonts/WooCommerce.woff) format("woff"),url(../fonts/WooCommerce.ttf) format("truetype"),url(../fonts/WooCommerce.svg#WooCommerce) format("svg");font-weight:400;font-style:normal}span.mce_woocommerce_shortcodes_button{background-image:none!important;display:block;text-indent:-9999px;position:relative;height:1em;width:1em}span.mce_woocommerce_shortcodes_button::before{font-family:WooCommerce;speak:never;font-weight:400;font-variant:normal;text-transform:none;line-height:1;margin:0;text-indent:0;position:absolute;top:0;left:0;width:100%;height:100%;text-align:center;content:"";font-size:.9em;line-height:1.2}#woocommerce-update .updating-message .wc_plugin_upgrade_notice{display:none}#woocommerce-update .dummy{display:none}#woocommerce-update .wc_plugin_upgrade_notice{font-weight:400;background:#fff8e5!important;border-left:4px solid #ffb900;border-top:1px solid #ffb900;padding:9px 0 9px 12px!important;margin:0 -12px 0 -16px!important}#woocommerce-update .wc_plugin_upgrade_notice::before{content:"\f348";display:inline-block;font:400 18px/1 dashicons;speak:never;margin:0 8px 0 -2px;vertical-align:top}#woocommerce-update .wc_plugin_upgrade_notice.major,#woocommerce-update .wc_plugin_upgrade_notice.minor{padding:20px 0!important}#woocommerce-update .wc_plugin_upgrade_notice.major::before,#woocommerce-update .wc_plugin_upgrade_notice.minor::before{display:none}#woocommerce-update .wc_plugin_upgrade_notice.major p,#woocommerce-update .wc_plugin_upgrade_notice.minor p{padding:0 20px;margin:0;max-width:700px;line-height:1.5em}#woocommerce-update .wc_plugin_upgrade_notice.major p::before,#woocommerce-update .wc_plugin_upgrade_notice.minor p::before{content:"";display:none}#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table{margin:.75em 0 0}#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table tr,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table tr{background:transparent none!important;border:0!important}#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table td,#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table th,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table td,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table th{background:transparent none!important;margin:0;padding:.75em 20px 0;border:0!important;font-size:1em;box-shadow:none}#woocommerce-update .wc_plugin_upgrade_notice.major table.plugin-details-table th,#woocommerce-update .wc_plugin_upgrade_notice.minor table.plugin-details-table th{font-weight:700}#wc_untested_extensions_modal{display:none}.wc_untested_extensions_modal_container{border-radius:4px;padding:0}.wc_untested_extensions_modal_container #TB_closeAjaxWindow{display:none}.wc_untested_extensions_modal_container #TB_title{display:none}.wc_untested_extensions_modal_container #TB_ajaxContent{height:100%!important;padding:0;margin:0;width:100%!important}.wc_untested_extensions_modal_container #TB_ajaxContent p{margin:0 0 1em}.wc_untested_extensions_modal--content h1{margin:2px 2px .5em;padding:.75em 1em;line-height:1.5em;font-size:2em;border-bottom:1px solid #eee;color:#fff;background:#96578a;border-top-left-radius:4px;border-top-right-radius:4px;text-shadow:none}.wc_untested_extensions_modal--content .extensions_warning{padding:0 2em}.wc_untested_extensions_modal--content .plugin-details-table-container{max-height:40vh;overflow-y:auto}.wc_untested_extensions_modal--content table.plugin-details-table{margin:20px 0}.wc_untested_extensions_modal--content table.plugin-details-table td,.wc_untested_extensions_modal--content table.plugin-details-table th{background:transparent none!important;margin:0;padding:.75em 20px 0;border:0!important;font-size:1em;box-shadow:none}.wc_untested_extensions_modal--content table.plugin-details-table th{font-weight:700;margin-top:0}.wc_untested_extensions_modal--content .actions{border-top:1px solid #eee;margin:0;padding:1em 0 2em 0;overflow:hidden}.wc_untested_extensions_modal--content .actions .woocommerce-actions{display:inline-block}.wc_untested_extensions_modal--content .actions a.button-primary{float:right;background:#bb77ae;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597;color:#fff;text-shadow:0 -1px 1px #a36597,1px 0 1px #a36597,0 1px 1px #a36597,-1px 0 1px #a36597}.wc_untested_extensions_modal--content .actions a.button-primary:active,.wc_untested_extensions_modal--content .actions a.button-primary:focus,.wc_untested_extensions_modal--content .actions a.button-primary:hover{background:#a36597;border-color:#a36597;box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 0 #a36597} \ No newline at end of file diff --git a/assets/css/menu.scss b/assets/css/menu.scss new file mode 100644 index 0000000..1103152 --- /dev/null +++ b/assets/css/menu.scss @@ -0,0 +1,210 @@ +/** + * menu.scss + * Styles applied to dashboard menu items added via WooCommerce. + * Adds icons to top level menu items, etc. + */ + +/** + * Imports + */ +@import "mixins"; +@import "variables"; +@import "fonts"; + +/** + * Styling begins + */ +span.mce_woocommerce_shortcodes_button { + background-image: none !important; + + @include ir(); + + &::before { + + @include icon("\e01d"); + font-size: 0.9em; + line-height: 1.2; + } +} + +#woocommerce-update { + + .updating-message { + + .wc_plugin_upgrade_notice { + display: none; + } + } + + .dummy { + display: none; + } + + .wc_plugin_upgrade_notice { + font-weight: normal; + background: #fff8e5 !important; + border-left: 4px solid #ffb900; + border-top: 1px solid #ffb900; + padding: 9px 0 9px 12px !important; + margin: 0 -12px 0 -16px !important; + + &::before { + content: "\f348"; + display: inline-block; + font: 400 18px/1 dashicons; + speak: never; + margin: 0 8px 0 -2px; + vertical-align: top; + } + + &.minor, + &.major { + padding: 20px 0 !important; + + &::before { + display: none; + } + + p { + padding: 0 20px; + margin: 0; + max-width: 700px; + line-height: 1.5em; + + &::before { + content: ""; + display: none; + } + } + + table.plugin-details-table { + margin: 0.75em 0 0; + + tr { + background: transparent none !important; + border: 0 !important; + } + + th, + td { + background: transparent none !important; + margin: 0; + padding: 0.75em 20px 0; + border: 0 !important; + font-size: 1em; + box-shadow: none; + } + + th { + font-weight: bold; + } + } + } + } +} + +#wc_untested_extensions_modal { + display: none; +} + +.wc_untested_extensions_modal_container { + border-radius: 4px; + padding: 0; + + #TB_closeAjaxWindow { + display: none; + } + + #TB_title { + display: none; + } + + #TB_ajaxContent { + height: 100% !important; + padding: 0; + margin: 0; + width: 100% !important; + + p { + margin: 0 0 1em; + } + } +} + +.wc_untested_extensions_modal--content { + + h1 { + margin: 2px 2px 0.5em; + padding: 0.75em 1em; + line-height: 1.5em; + font-size: 2em; + border-bottom: 1px solid #eee; + color: #fff; + background: #96578a; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + text-shadow: none; + } + + .extensions_warning { + padding: 0 2em; + } + + .plugin-details-table-container { + max-height: 40vh; + overflow-y: auto; + } + + table.plugin-details-table { + margin: 20px 0; + + th, + td { + background: transparent none !important; + margin: 0; + padding: 0.75em 20px 0; + border: 0 !important; + font-size: 1em; + box-shadow: none; + } + + th { + font-weight: bold; + margin-top: 0; + } + } + + .actions { + border-top: 1px solid #eee; + margin: 0; + padding: 1em 0 2em 0; + overflow: hidden; + + .woocommerce-actions { + display: inline-block; + } + + a.button-primary { + float: right; + background: #bb77ae; + border-color: #a36597; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597; + color: #fff; + text-shadow: + 0 -1px 1px #a36597, + 1px 0 1px #a36597, + 0 1px 1px #a36597, + -1px 0 1px #a36597; + + &:hover, + &:focus, + &:active { + background: #a36597; + border-color: #a36597; + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.25), + 0 1px 0 #a36597; + } + } + } +} diff --git a/assets/css/network-order-widget-rtl.css b/assets/css/network-order-widget-rtl.css new file mode 100644 index 0000000..bc799e6 --- /dev/null +++ b/assets/css/network-order-widget-rtl.css @@ -0,0 +1 @@ +#woocommerce_network_orders .inside{margin:0;padding:0}#woocommerce_network_orders .woocommerce-network-order-table,#woocommerce_network_orders .woocommerce-network-order-table-loading,#woocommerce_network_orders .woocommerce-network-orders-no-orders{width:100%;display:none}#woocommerce_network_orders .woocommerce-network-order-table-loading.is-active,#woocommerce_network_orders .woocommerce-network-order-table.is-active,#woocommerce_network_orders .woocommerce-network-orders-no-orders.is-active{display:block}#woocommerce_network_orders .woocommerce-network-order-table-loading p,#woocommerce_network_orders .woocommerce-network-orders-no-orders p{text-align:center}#woocommerce_network_orders .woocommerce-network-order-table{margin-top:0}#woocommerce_network_orders .woocommerce-network-order-table.is-active{display:table}#woocommerce_network_orders .woocommerce-network-order-table thead td{padding:.5em 1em}#woocommerce_network_orders .spinner{margin-top:0;float:none}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td,#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody th{border-top:1px solid #f5f5f5}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td{vertical-align:middle;padding:1em}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status{display:-webkit-inline-box;display:inline-flex;padding:0 1em;line-height:2.5em;color:#777;background:#e5e5e5;border-radius:4px;border-bottom:1px solid rgba(0,0,0,.05);margin:-.5em 0;cursor:inherit!important}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-completed{background:#c8d7e1;color:#2e4453}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-on-hold{background:#f8dda7;color:#94660c}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-failed{background:#eba3a3;color:#761919}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-processing{background:#c6e1c6;color:#5b841b}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-trash{background:#eba3a3;color:#761919} \ No newline at end of file diff --git a/assets/css/network-order-widget.css b/assets/css/network-order-widget.css new file mode 100644 index 0000000..bc799e6 --- /dev/null +++ b/assets/css/network-order-widget.css @@ -0,0 +1 @@ +#woocommerce_network_orders .inside{margin:0;padding:0}#woocommerce_network_orders .woocommerce-network-order-table,#woocommerce_network_orders .woocommerce-network-order-table-loading,#woocommerce_network_orders .woocommerce-network-orders-no-orders{width:100%;display:none}#woocommerce_network_orders .woocommerce-network-order-table-loading.is-active,#woocommerce_network_orders .woocommerce-network-order-table.is-active,#woocommerce_network_orders .woocommerce-network-orders-no-orders.is-active{display:block}#woocommerce_network_orders .woocommerce-network-order-table-loading p,#woocommerce_network_orders .woocommerce-network-orders-no-orders p{text-align:center}#woocommerce_network_orders .woocommerce-network-order-table{margin-top:0}#woocommerce_network_orders .woocommerce-network-order-table.is-active{display:table}#woocommerce_network_orders .woocommerce-network-order-table thead td{padding:.5em 1em}#woocommerce_network_orders .spinner{margin-top:0;float:none}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td,#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody th{border-top:1px solid #f5f5f5}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td{vertical-align:middle;padding:1em}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status{display:-webkit-inline-box;display:inline-flex;padding:0 1em;line-height:2.5em;color:#777;background:#e5e5e5;border-radius:4px;border-bottom:1px solid rgba(0,0,0,.05);margin:-.5em 0;cursor:inherit!important}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-completed{background:#c8d7e1;color:#2e4453}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-on-hold{background:#f8dda7;color:#94660c}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-failed{background:#eba3a3;color:#761919}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-processing{background:#c6e1c6;color:#5b841b}#woocommerce_network_orders .post-type-shop_order .woocommerce-network-order-table tbody td .order-status.status-trash{background:#eba3a3;color:#761919} \ No newline at end of file diff --git a/assets/css/network-order-widget.scss b/assets/css/network-order-widget.scss new file mode 100644 index 0000000..8aac784 --- /dev/null +++ b/assets/css/network-order-widget.scss @@ -0,0 +1,92 @@ +#woocommerce_network_orders { + .inside { + margin: 0; + padding: 0; + } + + .woocommerce-network-orders-no-orders, + .woocommerce-network-order-table-loading, + .woocommerce-network-order-table { + width: 100%; + display: none; + + &.is-active { + display: block; + } + } + + .woocommerce-network-orders-no-orders, + .woocommerce-network-order-table-loading { + p { + text-align: center; + } + } + + .woocommerce-network-order-table { + &.is-active { + display: table; + } + + margin-top: 0; + + thead td { + padding: 0.5em 1em; + } + } + + .spinner { + margin-top: 0; + float: none; + } + + .post-type-shop_order { + .woocommerce-network-order-table { + tbody { + th, td { + border-top: 1px solid #f5f5f5; + } + td { + vertical-align: middle; + padding: 1em; + + .order-status { + display: inline-flex; + padding: 0px 1em; + line-height: 2.5em; + color: #777; + background: #E5E5E5; + border-radius: 4px; + border-bottom: 1px solid rgba(0,0,0,0.05); + margin: -.5em 0; + cursor: inherit !important; + + &.status-completed { + background: #C8D7E1; + color: #2e4453; + } + + &.status-on-hold { + background: #f8dda7; + color: #94660c; + } + + &.status-failed { + background: #eba3a3; + color: #761919; + } + + &.status-processing { + background: #C6E1C6; + color: #5B841B; + } + + &.status-trash { + background: #eba3a3; + color: #761919; + } + } + } + } + } + } +} diff --git a/assets/css/photoswipe/default-skin/default-skin.css b/assets/css/photoswipe/default-skin/default-skin.css new file mode 100644 index 0000000..c961632 --- /dev/null +++ b/assets/css/photoswipe/default-skin/default-skin.css @@ -0,0 +1,482 @@ +/*! PhotoSwipe Default UI CSS by Dmitry Semenov | photoswipe.com | MIT license */ +/* + + Contents: + + 1. Buttons + 2. Share modal and links + 3. Index indicator ("1 of X" counter) + 4. Caption + 5. Loading indicator + 6. Additional styles (root element, top bar, idle state, hidden state, etc.) + +*/ +/* + + 1. Buttons + + */ +/* ' + + ''; + $items.append( $row ); + + return false; + }, + + remove: function() { + if ( window.confirm( woocommerce_admin_meta_boxes.remove_item_meta ) ) { + var $row = $( this ).closest( 'tr' ); + $row.find( ':input' ).val( '' ); + $row.hide(); + } + return false; + } + }, + + backbone: { + + init: function( e, target ) { + if ( 'wc-modal-add-products' === target ) { + $( document.body ).trigger( 'wc-enhanced-select-init' ); + + $( this ).on( 'change', '.wc-product-search', function() { + if ( ! $( this ).closest( 'tr' ).is( ':last-child' ) ) { + return; + } + var item_table = $( this ).closest( 'table.widefat' ), + item_table_body = item_table.find( 'tbody' ), + index = item_table_body.find( 'tr' ).length, + row = item_table_body.data( 'row' ).replace( /\[0\]/g, '[' + index + ']' ); + + item_table_body.append( '' + row + '' ); + $( document.body ).trigger( 'wc-enhanced-select-init' ); + } ); + } + }, + + response: function( e, target, data ) { + if ( 'wc-modal-add-tax' === target ) { + var rate_id = data.add_order_tax; + var manual_rate_id = ''; + + if ( data.manual_tax_rate_id ) { + manual_rate_id = data.manual_tax_rate_id; + } + + wc_meta_boxes_order_items.backbone.add_tax( rate_id, manual_rate_id ); + } + if ( 'wc-modal-add-products' === target ) { + // Build array of data. + var item_table = $( this ).find( 'table.widefat' ), + item_table_body = item_table.find( 'tbody' ), + rows = item_table_body.find( 'tr' ), + add_items = []; + + $( rows ).each( function() { + var item_id = $( this ).find( ':input[name="item_id"]' ).val(), + item_qty = $( this ).find( ':input[name="item_qty"]' ).val(); + + add_items.push( { + 'id' : item_id, + 'qty': item_qty ? item_qty: 1 + } ); + } ); + + return wc_meta_boxes_order_items.backbone.add_items( add_items ); + } + }, + + add_items: function( add_items ) { + wc_meta_boxes_order_items.block(); + + var data = { + action : 'woocommerce_add_order_item', + order_id : woocommerce_admin_meta_boxes.post_id, + security : woocommerce_admin_meta_boxes.order_item_nonce, + data : add_items + }; + + // Check if items have changed, if so pass them through so we can save them before adding a new item. + if ( 'true' === $( 'button.cancel-action' ).attr( 'data-reload' ) ) { + data.items = $( 'table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]' ).serialize(); + } + + $.ajax({ + type: 'POST', + url: woocommerce_admin_meta_boxes.ajax_url, + data: data, + success: function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + + // Update notes. + if ( response.data.notes_html ) { + $( 'ul.order_notes' ).empty(); + $( 'ul.order_notes' ).append( $( response.data.notes_html ).find( 'li' ) ); + } + + wc_meta_boxes_order_items.reloaded_items(); + wc_meta_boxes_order_items.unblock(); + } else { + wc_meta_boxes_order_items.unblock(); + window.alert( response.data.error ); + } + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_add_products', { + order_id: data.post_id, + status: $( '#order_status' ).val() + } ); + }, + dataType: 'json' + }); + }, + + add_tax: function( rate_id, manual_rate_id ) { + if ( manual_rate_id ) { + rate_id = manual_rate_id; + } + + if ( ! rate_id ) { + return false; + } + + var rates = $( '.order-tax-id' ).map( function() { + return $( this ).val(); + }).get(); + + // Test if already exists + if ( -1 === $.inArray( rate_id, rates ) ) { + wc_meta_boxes_order_items.block(); + + var data = { + action: 'woocommerce_add_order_tax', + rate_id: rate_id, + order_id: woocommerce_admin_meta_boxes.post_id, + security: woocommerce_admin_meta_boxes.order_item_nonce + }; + + $.ajax({ + url : woocommerce_admin_meta_boxes.ajax_url, + data : data, + dataType : 'json', + type : 'POST', + success : function( response ) { + if ( response.success ) { + $( '#woocommerce-order-items' ).find( '.inside' ).empty(); + $( '#woocommerce-order-items' ).find( '.inside' ).append( response.data.html ); + wc_meta_boxes_order_items.reloaded_items(); + } else { + window.alert( response.data.error ); + } + wc_meta_boxes_order_items.unblock(); + }, + complete: function() { + window.wcTracks.recordEvent( 'order_edit_add_tax', { + order_id: data.post_id, + status: $( '#order_status' ).val() + } ); + } + }); + } else { + window.alert( woocommerce_admin_meta_boxes.i18n_tax_rate_already_exists ); + } + } + }, + + stupidtable: { + init: function() { + $( '.woocommerce_order_items' ).stupidtable(); + $( '.woocommerce_order_items' ).on( 'aftertablesort', this.add_arrows ); + }, + + add_arrows: function( event, data ) { + var th = $( this ).find( 'th' ); + var arrow = data.direction === 'asc' ? '↑' : '↓'; + var index = data.column; + th.find( '.wc-arrow' ).remove(); + th.eq( index ).append( '' + arrow + '' ); + } + } + }; + + /** + * Order Notes Panel + */ + var wc_meta_boxes_order_notes = { + init: function() { + $( '#woocommerce-order-notes' ) + .on( 'click', 'button.add_note', this.add_order_note ) + .on( 'click', 'a.delete_note', this.delete_order_note ); + + }, + + add_order_note: function() { + if ( ! $( 'textarea#add_order_note' ).val() ) { + return; + } + + $( '#woocommerce-order-notes' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var data = { + action: 'woocommerce_add_order_note', + post_id: woocommerce_admin_meta_boxes.post_id, + note: $( 'textarea#add_order_note' ).val(), + note_type: $( 'select#order_note_type' ).val(), + security: woocommerce_admin_meta_boxes.add_order_note_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + $( 'ul.order_notes .no-items' ).remove(); + $( 'ul.order_notes' ).prepend( response ); + $( '#woocommerce-order-notes' ).unblock(); + $( '#add_order_note' ).val( '' ); + window.wcTracks.recordEvent( 'order_edit_add_order_note', { + order_id: data.post_id, + note_type: data.note_type || 'private', + status: $( '#order_status' ).val() + } ); + }); + + return false; + }, + + delete_order_note: function() { + if ( window.confirm( woocommerce_admin_meta_boxes.i18n_delete_note ) ) { + var note = $( this ).closest( 'li.note' ); + + $( note ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var data = { + action: 'woocommerce_delete_order_note', + note_id: $( note ).attr( 'rel' ), + security: woocommerce_admin_meta_boxes.delete_order_note_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function() { + $( note ).remove(); + }); + } + + return false; + } + }; + + /** + * Order Downloads Panel + */ + var wc_meta_boxes_order_downloads = { + init: function() { + $( '.order_download_permissions' ) + .on( 'click', 'button.grant_access', this.grant_access ) + .on( 'click', 'button.revoke_access', this.revoke_access ) + .on( 'click', '#copy-download-link', this.copy_link ) + .on( 'aftercopy', '#copy-download-link', this.copy_success ) + .on( 'aftercopyfailure', '#copy-download-link', this.copy_fail ); + }, + + grant_access: function() { + var products = $( '#grant_access_id' ).val(); + + if ( ! products ) { + return; + } + + $( '.order_download_permissions' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var data = { + action: 'woocommerce_grant_access_to_download', + product_ids: products, + loop: $('.order_download_permissions .wc-metabox').length, + order_id: woocommerce_admin_meta_boxes.post_id, + security: woocommerce_admin_meta_boxes.grant_access_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + + if ( response ) { + $( '.order_download_permissions .wc-metaboxes' ).append( response ); + } else { + window.alert( woocommerce_admin_meta_boxes.i18n_download_permission_fail ); + } + + $( document.body ).trigger( 'wc-init-datepickers' ); + $( '#grant_access_id' ).val( '' ).trigger( 'change' ); + $( '.order_download_permissions' ).unblock(); + }); + + return false; + }, + + revoke_access: function () { + if ( window.confirm( woocommerce_admin_meta_boxes.i18n_permission_revoke ) ) { + var el = $( this ).parent().parent(); + var product = $( this ).attr( 'rel' ).split( ',' )[0]; + var file = $( this ).attr( 'rel' ).split( ',' )[1]; + var permission_id = $( this ).data( 'permission_id' ); + + if ( product > 0 ) { + $( el ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var data = { + action: 'woocommerce_revoke_access_to_download', + product_id: product, + download_id: file, + permission_id: permission_id, + order_id: woocommerce_admin_meta_boxes.post_id, + security: woocommerce_admin_meta_boxes.revoke_access_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function() { + // Success + $( el ).fadeOut( '300', function () { + $( el ).remove(); + }); + }); + + } else { + $( el ).fadeOut( '300', function () { + $( el ).remove(); + }); + } + } + return false; + }, + + /** + * Copy download link. + * + * @param {Object} evt Copy event. + */ + copy_link: function( evt ) { + wcClearClipboard(); + wcSetClipboard( $( this ).attr( 'href' ), $( this ) ); + evt.preventDefault(); + }, + + /** + * Display a "Copied!" tip when success copying + */ + copy_success: function() { + $( this ).tipTip({ + 'attribute': 'data-tip', + 'activation': 'focus', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 0 + }).trigger( 'focus' ); + }, + + /** + * Displays the copy error message when failure copying. + */ + copy_fail: function() { + $( this ).tipTip({ + 'attribute': 'data-tip-failed', + 'activation': 'focus', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 0 + }).trigger( 'focus' ); + } + }; + + wc_meta_boxes_order.init(); + wc_meta_boxes_order_items.init(); + wc_meta_boxes_order_notes.init(); + wc_meta_boxes_order_downloads.init(); +}); diff --git a/assets/js/admin/meta-boxes-order.min.js b/assets/js/admin/meta-boxes-order.min.js new file mode 100644 index 0000000..6142707 --- /dev/null +++ b/assets/js/admin/meta-boxes-order.min.js @@ -0,0 +1 @@ +jQuery(function(u){window.wcTracks=window.wcTracks||{},window.wcTracks.recordEvent=window.wcTracks.recordEvent||function(){};var p={states:null,init:function(){"undefined"!=typeof woocommerce_admin_meta_boxes_order&&"undefined"!=typeof woocommerce_admin_meta_boxes_order.countries&&(this.states=JSON.parse(woocommerce_admin_meta_boxes_order.countries.replace(/"/g,'"'))),u(".js_field-country").selectWoo().on("change",this.change_country),u(".js_field-country").trigger("change",[!0]),u(document.body).on("change","select.js_field-state",this.change_state),u("#woocommerce-order-actions input, #woocommerce-order-actions a").on("click",function(){window.onbeforeunload=""}),u("a.edit_address").on("click",this.edit_address),u("a.billing-same-as-shipping").on("click",this.copy_billing_to_shipping),u("a.load_customer_billing").on("click",this.load_billing),u("a.load_customer_shipping").on("click",this.load_shipping),u("#customer_user").on("change",this.change_customer_user)},change_country:function(e,o){var t,r,a,i,n,d,c,_,s,m,l;void 0===o&&(o=!1),null!==p.states&&(r=(t=u(this)).val(),m=(a=t.parents("div.edit_address").find(":input.js_field-state")).parent(),i=a.val(),n=a.attr("name"),d=a.attr("id"),c=t.data("woocommerce.stickState-"+r)?t.data("woocommerce.stickState-"+r):i,_=a.attr("placeholder"),o&&t.data("woocommerce.stickState-"+r,c),m.show().find(".select2-container").remove(),u.isEmptyObject(p.states[r])?(l=u('').prop("id",d).prop("name",n).prop("placeholder",_).addClass("js_field-state").val(i),a.replaceWith(l)):(s=p.states[r],m=u('').text(woocommerce_admin_meta_boxes_order.i18n_select_state_text),l=u("").prop("id",d).prop("name",n).prop("placeholder",_).addClass("js_field-state select short").append(m),u.each(s,function(e){var o=u("").prop("value",e).text(s[e]);e===i&&o.prop("selected"),l.append(o)}),l.val(c),a.replaceWith(l),l.show().selectWoo().hide().trigger("change")),u(document.body).trigger("contry-change.woocommerce",[r,u(this).closest("div")]),u(document.body).trigger("country-change.woocommerce",[r,u(this).closest("div")]))},change_state:function(){var e=u(this),o=e.val(),t=e.parents("div.edit_address").find(":input.js_field-country"),e=t.val();t.data("woocommerce.stickState-"+e,o)},init_tiptip:function(){u("#tiptip_holder").removeAttr("style"),u("#tiptip_arrow").removeAttr("style"),u(".tips").tipTip({attribute:"data-tip",fadeIn:50,fadeOut:50,delay:200,keepAlive:!0})},edit_address:function(e){e.preventDefault();var o=u(this),t=o.closest(".order_data_column"),r=t.find("div.edit_address"),a=t.find("div.address"),i=r.find(".js_field-country"),e=r.find(".js_field-state"),t=Boolean(r.find('input[name^="_billing_"]').length);a.hide(),o.parent().find("a").toggle(),i.val()||(i.val(woocommerce_admin_meta_boxes_order.default_country).trigger("change"),e.val(woocommerce_admin_meta_boxes_order.default_state).trigger("change")),r.show();t=t?"order_edit_billing_address_click":"order_edit_shipping_address_click";window.wcTracks.recordEvent(t,{order_id:woocommerce_admin_meta_boxes.post_id,status:u("#order_status").val()})},change_customer_user:function(){u("#_billing_country").val()||(u("a.edit_address").trigger("click"),p.load_billing(!0),p.load_shipping(!0))},load_billing:function(e){if(!0===e||window.confirm(woocommerce_admin_meta_boxes.load_billing)){e=u("#customer_user").val();if(!e)return window.alert(woocommerce_admin_meta_boxes.no_customer_selected),!1;e={user_id:e,action:"woocommerce_get_customer_details",security:woocommerce_admin_meta_boxes.get_customer_details_nonce};u(this).closest("div.edit_address").block({message:null,overlayCSS:{background:"#fff",opacity:.6}}),u.ajax({url:woocommerce_admin_meta_boxes.ajax_url,data:e,type:"POST",success:function(e){e&&e.billing&&u.each(e.billing,function(e,o){u(":input#_billing_"+e).val(o).trigger("change")}),u("div.edit_address").unblock()}})}return!1},load_shipping:function(e){if(!0===e||window.confirm(woocommerce_admin_meta_boxes.load_shipping)){e=u("#customer_user").val();if(!e)return window.alert(woocommerce_admin_meta_boxes.no_customer_selected),!1;e={user_id:e,action:"woocommerce_get_customer_details",security:woocommerce_admin_meta_boxes.get_customer_details_nonce};u(this).closest("div.edit_address").block({message:null,overlayCSS:{background:"#fff",opacity:.6}}),u.ajax({url:woocommerce_admin_meta_boxes.ajax_url,data:e,type:"POST",success:function(e){e&&e.billing&&u.each(e.shipping,function(e,o){u(":input#_shipping_"+e).val(o).trigger("change")}),u("div.edit_address").unblock()}})}return!1},copy_billing_to_shipping:function(){return window.confirm(woocommerce_admin_meta_boxes.copy_billing)&&u('.order_data_column :input[name^="_billing_"]').each(function(){var e=(e=u(this).attr("name")).replace("_billing_","_shipping_");u(":input#"+e).val(u(this).val()).trigger("change")}),!1}},d={init:function(){this.stupidtable.init(),u("#woocommerce-order-items").on("click","button.add-line-item",this.add_line_item).on("click","button.add-coupon",this.add_coupon).on("click","a.remove-coupon",this.remove_coupon).on("click","button.refund-items",this.refund_items).on("click",".cancel-action",this.cancel).on("click",".refund-actions .cancel-action",this.track_cancel).on("click","button.add-order-item",this.add_item).on("click","button.add-order-fee",this.add_fee).on("click","button.add-order-shipping",this.add_shipping).on("click","button.add-order-tax",this.add_tax).on("click","button.save-action",this.save_line_items).on("click","a.delete-order-tax",this.delete_tax).on("click","button.calculate-action",this.recalculate).on("click","a.edit-order-item",this.edit_item).on("click","a.delete-order-item",this.delete_item).on("click",".delete_refund",this.refunds.delete_refund).on("click","button.do-api-refund, button.do-manual-refund",this.refunds.do_refund).on("change",".refund input.refund_line_total, .refund input.refund_line_tax",this.refunds.input_changed).on("change keyup",".wc-order-refund-items #refund_amount",this.refunds.amount_changed).on("change","input.refund_order_item_qty",this.refunds.refund_quantity_changed).on("change","input.quantity",this.quantity_changed).on("keyup change",".split-input :input",function(){var e=u(this).parent().prev().find(":input");e&&(""===e.val()||e.is(".match-total"))&&e.val(u(this).val()).addClass("match-total")}).on("keyup",".split-input :input",function(){u(this).removeClass("match-total")}).on("click","button.add_order_item_meta",this.item_meta.add).on("click","button.remove_order_item_meta",this.item_meta.remove).on("wc_order_items_reload",this.reload_items).on("wc_order_items_reloaded",this.reloaded_items),u(document.body).on("wc_backbone_modal_loaded",this.backbone.init).on("wc_backbone_modal_response",this.backbone.response)},block:function(){u("#woocommerce-order-items").block({message:null,overlayCSS:{background:"#fff",opacity:.6}})},unblock:function(){u("#woocommerce-order-items").unblock()},reload_items:function(){var e={order_id:woocommerce_admin_meta_boxes.post_id,action:"woocommerce_load_order_items",security:woocommerce_admin_meta_boxes.order_item_nonce};d.block(),u.ajax({url:woocommerce_admin_meta_boxes.ajax_url,data:e,type:"POST",success:function(e){u("#woocommerce-order-items").find(".inside").empty(),u("#woocommerce-order-items").find(".inside").append(e),d.reloaded_items(),d.unblock()}})},reloaded_items:function(){p.init_tiptip(),d.stupidtable.init()},quantity_changed:function(){var i=u(this).closest("tr.item"),n=u(this).val(),d=u(this).attr("data-qty"),e=u("input.line_total",i),o=u("input.line_subtotal",i),t=accounting.unformat(e.attr("data-total"),woocommerce_admin.mon_decimal_point)/d;e.val(parseFloat(accounting.formatNumber(t*n,woocommerce_admin_meta_boxes.rounding_precision,"")).toString().replace(".",woocommerce_admin.mon_decimal_point));t=accounting.unformat(o.attr("data-subtotal"),woocommerce_admin.mon_decimal_point)/d;o.val(parseFloat(accounting.formatNumber(t*n,woocommerce_admin_meta_boxes.rounding_precision,"")).toString().replace(".",woocommerce_admin.mon_decimal_point)),u("input.line_tax",i).each(function(){var e=u(this),o=e.data("tax_id"),t=accounting.unformat(e.attr("data-total_tax"),woocommerce_admin.mon_decimal_point)/d,r=u('input.line_subtotal_tax[data-tax_id="'+o+'"]',i),a=accounting.unformat(r.attr("data-subtotal_tax"),woocommerce_admin.mon_decimal_point)/d,o="yes"===woocommerce_admin_meta_boxes.round_at_subtotal,o=woocommerce_admin_meta_boxes[o?"rounding_precision":"currency_format_num_decimals"];0';return o.append(t),!1},remove:function(){var e;return window.confirm(woocommerce_admin_meta_boxes.remove_item_meta)&&((e=u(this).closest("tr")).find(":input").val(""),e.hide()),!1}},backbone:{init:function(e,o){"wc-modal-add-products"===o&&(u(document.body).trigger("wc-enhanced-select-init"),u(this).on("change",".wc-product-search",function(){var e,o;u(this).closest("tr").is(":last-child")&&(o=(e=u(this).closest("table.widefat").find("tbody")).find("tr").length,o=e.data("row").replace(/\[0\]/g,"["+o+"]"),e.append(""+o+""),u(document.body).trigger("wc-enhanced-select-init"))}))},response:function(e,o,t){var r,a;if("wc-modal-add-tax"===o&&(r=t.add_order_tax,a="",t.manual_tax_rate_id&&(a=t.manual_tax_rate_id),d.backbone.add_tax(r,a)),"wc-modal-add-products"===o){var o=u(this).find("table.widefat").find("tbody").find("tr"),i=[];return u(o).each(function(){var e=u(this).find(':input[name="item_id"]').val(),o=u(this).find(':input[name="item_qty"]').val();i.push({id:e,qty:o||1})}),d.backbone.add_items(i)}},add_items:function(e){d.block();var o={action:"woocommerce_add_order_item",order_id:woocommerce_admin_meta_boxes.post_id,security:woocommerce_admin_meta_boxes.order_item_nonce,data:e};"true"===u("button.cancel-action").attr("data-reload")&&(o.items=u("table.woocommerce_order_items :input[name], .wc-order-totals-items :input[name]").serialize()),u.ajax({type:"POST",url:woocommerce_admin_meta_boxes.ajax_url,data:o,success:function(e){e.success?(u("#woocommerce-order-items").find(".inside").empty(),u("#woocommerce-order-items").find(".inside").append(e.data.html),e.data.notes_html&&(u("ul.order_notes").empty(),u("ul.order_notes").append(u(e.data.notes_html).find("li"))),d.reloaded_items(),d.unblock()):(d.unblock(),window.alert(e.data.error))},complete:function(){window.wcTracks.recordEvent("order_edit_add_products",{order_id:o.post_id,status:u("#order_status").val()})},dataType:"json"})},add_tax:function(e,o){if(o&&(e=o),!e)return!1;var t,o=u(".order-tax-id").map(function(){return u(this).val()}).get();-1===u.inArray(e,o)?(d.block(),t={action:"woocommerce_add_order_tax",rate_id:e,order_id:woocommerce_admin_meta_boxes.post_id,security:woocommerce_admin_meta_boxes.order_item_nonce},u.ajax({url:woocommerce_admin_meta_boxes.ajax_url,data:t,dataType:"json",type:"POST",success:function(e){e.success?(u("#woocommerce-order-items").find(".inside").empty(),u("#woocommerce-order-items").find(".inside").append(e.data.html),d.reloaded_items()):window.alert(e.data.error),d.unblock()},complete:function(){window.wcTracks.recordEvent("order_edit_add_tax",{order_id:t.post_id,status:u("#order_status").val()})}})):window.alert(woocommerce_admin_meta_boxes.i18n_tax_rate_already_exists)}},stupidtable:{init:function(){u(".woocommerce_order_items").stupidtable(),u(".woocommerce_order_items").on("aftertablesort",this.add_arrows)},add_arrows:function(e,o){var t=u(this).find("th"),r="asc"===o.direction?"↑":"↓",o=o.column;t.find(".wc-arrow").remove(),t.eq(o).append(''+r+"")}}},e={init:function(){u("#woocommerce-order-notes").on("click","button.add_note",this.add_order_note).on("click","a.delete_note",this.delete_order_note)},add_order_note:function(){if(u("textarea#add_order_note").val()){u("#woocommerce-order-notes").block({message:null,overlayCSS:{background:"#fff",opacity:.6}});var o={action:"woocommerce_add_order_note",post_id:woocommerce_admin_meta_boxes.post_id,note:u("textarea#add_order_note").val(),note_type:u("select#order_note_type").val(),security:woocommerce_admin_meta_boxes.add_order_note_nonce};return u.post(woocommerce_admin_meta_boxes.ajax_url,o,function(e){u("ul.order_notes .no-items").remove(),u("ul.order_notes").prepend(e),u("#woocommerce-order-notes").unblock(),u("#add_order_note").val(""),window.wcTracks.recordEvent("order_edit_add_order_note",{order_id:o.post_id,note_type:o.note_type||"private",status:u("#order_status").val()})}),!1}},delete_order_note:function(){var e,o;return window.confirm(woocommerce_admin_meta_boxes.i18n_delete_note)&&(e=u(this).closest("li.note"),u(e).block({message:null,overlayCSS:{background:"#fff",opacity:.6}}),o={action:"woocommerce_delete_order_note",note_id:u(e).attr("rel"),security:woocommerce_admin_meta_boxes.delete_order_note_nonce},u.post(woocommerce_admin_meta_boxes.ajax_url,o,function(){u(e).remove()})),!1}},o={init:function(){u(".order_download_permissions").on("click","button.grant_access",this.grant_access).on("click","button.revoke_access",this.revoke_access).on("click","#copy-download-link",this.copy_link).on("aftercopy","#copy-download-link",this.copy_success).on("aftercopyfailure","#copy-download-link",this.copy_fail)},grant_access:function(){var e=u("#grant_access_id").val();if(e){u(".order_download_permissions").block({message:null,overlayCSS:{background:"#fff",opacity:.6}});e={action:"woocommerce_grant_access_to_download",product_ids:e,loop:u(".order_download_permissions .wc-metabox").length,order_id:woocommerce_admin_meta_boxes.post_id,security:woocommerce_admin_meta_boxes.grant_access_nonce};return u.post(woocommerce_admin_meta_boxes.ajax_url,e,function(e){e?u(".order_download_permissions .wc-metaboxes").append(e):window.alert(woocommerce_admin_meta_boxes.i18n_download_permission_fail),u(document.body).trigger("wc-init-datepickers"),u("#grant_access_id").val("").trigger("change"),u(".order_download_permissions").unblock()}),!1}},revoke_access:function(){var e,o,t,r;return window.confirm(woocommerce_admin_meta_boxes.i18n_permission_revoke)&&(e=u(this).parent().parent(),o=u(this).attr("rel").split(",")[0],t=u(this).attr("rel").split(",")[1],r=u(this).data("permission_id"),0' ); + + $( this ).closest( '.woocommerce_variation' ) + .append( '' ); + + wc_meta_boxes_product_variations_ajax.save_variations(); + } + }, + + /** + * Set menu order + */ + variation_row_indexes: function() { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), + offset = parseInt( ( current_page - 1 ) * woocommerce_admin_meta_boxes_variations.variations_per_page, 10 ); + + $( '.woocommerce_variations .woocommerce_variation' ).each( function ( index, el ) { + $( '.variation_menu_order', el ) + .val( parseInt( $( el ) + .index( '.woocommerce_variations .woocommerce_variation' ), 10 ) + 1 + offset ) + .trigger( 'change' ); + }); + } + }; + + /** + * Variations media actions + */ + var wc_meta_boxes_product_variations_media = { + + /** + * wp.media frame object + * + * @type {Object} + */ + variable_image_frame: null, + + /** + * Variation image ID + * + * @type {Int} + */ + setting_variation_image_id: null, + + /** + * Variation image object + * + * @type {Object} + */ + setting_variation_image: null, + + /** + * wp.media post ID + * + * @type {Int} + */ + wp_media_post_id: wp.media.model.settings.post.id, + + /** + * Initialize media actions + */ + init: function() { + $( '#variable_product_options' ).on( 'click', '.upload_image_button', this.add_image ); + $( 'a.add_media' ).on( 'click', this.restore_wp_media_post_id ); + }, + + /** + * Added new image + * + * @param {Object} event + */ + add_image: function( event ) { + var $button = $( this ), + post_id = $button.attr( 'rel' ), + $parent = $button.closest( '.upload_image' ); + + wc_meta_boxes_product_variations_media.setting_variation_image = $parent; + wc_meta_boxes_product_variations_media.setting_variation_image_id = post_id; + + event.preventDefault(); + + if ( $button.is( '.remove' ) ) { + + $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( '' ).trigger( 'change' ); + wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ) + .attr( 'src', woocommerce_admin_meta_boxes_variations.woocommerce_placeholder_img_src ); + wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).removeClass( 'remove' ); + + } else { + + // If the media frame already exists, reopen it. + if ( wc_meta_boxes_product_variations_media.variable_image_frame ) { + wc_meta_boxes_product_variations_media.variable_image_frame.uploader.uploader + .param( 'post_id', wc_meta_boxes_product_variations_media.setting_variation_image_id ); + wc_meta_boxes_product_variations_media.variable_image_frame.open(); + return; + } else { + wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.setting_variation_image_id; + } + + // Create the media frame. + wc_meta_boxes_product_variations_media.variable_image_frame = wp.media.frames.variable_image = wp.media({ + // Set the title of the modal. + title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, + button: { + text: woocommerce_admin_meta_boxes_variations.i18n_set_image + }, + states: [ + new wp.media.controller.Library({ + title: woocommerce_admin_meta_boxes_variations.i18n_choose_image, + filterable: 'all' + }) + ] + }); + + // When an image is selected, run a callback. + wc_meta_boxes_product_variations_media.variable_image_frame.on( 'select', function () { + + var attachment = wc_meta_boxes_product_variations_media.variable_image_frame.state() + .get( 'selection' ).first().toJSON(), + url = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; + + $( '.upload_image_id', wc_meta_boxes_product_variations_media.setting_variation_image ).val( attachment.id ) + .trigger( 'change' ); + wc_meta_boxes_product_variations_media.setting_variation_image.find( '.upload_image_button' ).addClass( 'remove' ); + wc_meta_boxes_product_variations_media.setting_variation_image.find( 'img' ).eq( 0 ).attr( 'src', url ); + + wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; + }); + + // Finally, open the modal. + wc_meta_boxes_product_variations_media.variable_image_frame.open(); + } + }, + + /** + * Restore wp.media post ID. + */ + restore_wp_media_post_id: function() { + wp.media.model.settings.post.id = wc_meta_boxes_product_variations_media.wp_media_post_id; + } + }; + + /** + * Product variations metabox ajax methods + */ + var wc_meta_boxes_product_variations_ajax = { + + /** + * Initialize variations ajax methods + */ + init: function() { + $( 'li.variations_tab a' ).on( 'click', this.initial_load ); + + $( '#variable_product_options' ) + .on( 'click', 'button.save-variation-changes', this.save_variations ) + .on( 'click', 'button.cancel-variation-changes', this.cancel_variations ) + .on( 'click', '.remove_variation', this.remove_variation ) + .on( 'click','.downloadable_files a.delete', this.input_changed ); + + $( document.body ) + .on( 'change', '#variable_product_options .woocommerce_variations :input', this.input_changed ) + .on( 'change', '.variations-defaults select', this.defaults_changed ); + + var postForm = $( 'form#post' ); + + postForm.on( 'submit', this.save_on_submit ); + + $( 'input:submit', postForm ).on( 'click keypress', function() { + postForm.data( 'callerid', this.id ); + }); + + $( '.wc-metaboxes-wrapper' ).on( 'click', 'a.do_variation_action', this.do_variation_action ); + }, + + /** + * Check if have some changes before leave the page + * + * @return {Bool} + */ + check_for_changes: function() { + var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); + + if ( 0 < need_update.length ) { + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_edited_variations ) ) { + wc_meta_boxes_product_variations_ajax.save_changes(); + } else { + need_update.removeClass( 'variation-needs-update' ); + return false; + } + } + + return true; + }, + + /** + * Block edit screen + */ + block: function() { + $( '#woocommerce-product-data' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + }, + + /** + * Unblock edit screen + */ + unblock: function() { + $( '#woocommerce-product-data' ).unblock(); + }, + + /** + * Initial load variations + * + * @return {Bool} + */ + initial_load: function() { + if ( 0 === $( '#variable_product_options' ).find( '.woocommerce_variations .woocommerce_variation' ).length ) { + wc_meta_boxes_product_variations_pagenav.go_to_page(); + } + }, + + /** + * Load variations via Ajax + * + * @param {Int} page (default: 1) + * @param {Int} per_page (default: 10) + */ + load_variations: function( page, per_page ) { + page = page || 1; + per_page = per_page || woocommerce_admin_meta_boxes_variations.variations_per_page; + + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); + + wc_meta_boxes_product_variations_ajax.block(); + + $.ajax({ + url: woocommerce_admin_meta_boxes_variations.ajax_url, + data: { + action: 'woocommerce_load_variations', + security: woocommerce_admin_meta_boxes_variations.load_variations_nonce, + product_id: woocommerce_admin_meta_boxes_variations.post_id, + attributes: wrapper.data( 'attributes' ), + page: page, + per_page: per_page + }, + type: 'POST', + success: function( response ) { + wrapper.empty().append( response ).attr( 'data-page', page ); + + $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_loaded' ); + + wc_meta_boxes_product_variations_ajax.unblock(); + } + }); + }, + + /** + * Ger variations fields and convert to object + * + * @param {Object} fields + * + * @return {Object} + */ + get_variations_fields: function( fields ) { + var data = $( ':input', fields ).serializeJSON(); + + $( '.variations-defaults select' ).each( function( index, element ) { + var select = $( element ); + data[ select.attr( 'name' ) ] = select.val(); + }); + + return data; + }, + + /** + * Save variations changes + * + * @param {Function} callback Called once saving is complete + */ + save_changes: function( callback ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + need_update = $( '.variation-needs-update', wrapper ), + data = {}; + + // Save only with products need update. + if ( 0 < need_update.length ) { + wc_meta_boxes_product_variations_ajax.block(); + + data = wc_meta_boxes_product_variations_ajax.get_variations_fields( need_update ); + data.action = 'woocommerce_save_variations'; + data.security = woocommerce_admin_meta_boxes_variations.save_variations_nonce; + data.product_id = woocommerce_admin_meta_boxes_variations.post_id; + data['product-type'] = $( '#product-type' ).val(); + + $.ajax({ + url: woocommerce_admin_meta_boxes_variations.ajax_url, + data: data, + type: 'POST', + success: function( response ) { + // Allow change page, delete and add new variations + need_update.removeClass( 'variation-needs-update' ); + $( 'button.cancel-variation-changes, button.save-variation-changes' ).attr( 'disabled', 'disabled' ); + + $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_saved' ); + + if ( typeof callback === 'function' ) { + callback( response ); + } + + wc_meta_boxes_product_variations_ajax.unblock(); + } + }); + } + }, + + /** + * Save variations + * + * @return {Bool} + */ + save_variations: function() { + $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_button' ); + + wc_meta_boxes_product_variations_ajax.save_changes( function( error ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + current = wrapper.attr( 'data-page' ); + + $( '#variable_product_options' ).find( '#woocommerce_errors' ).remove(); + + if ( error ) { + wrapper.before( error ); + } + + $( '.variations-defaults select' ).each( function() { + $( this ).attr( 'data-current', $( this ).val() ); + }); + + wc_meta_boxes_product_variations_pagenav.go_to_page( current ); + }); + + return false; + }, + + /** + * Save on post form submit + */ + save_on_submit: function( e ) { + var need_update = $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ); + + if ( 0 < need_update.length ) { + e.preventDefault(); + $( '#variable_product_options' ).trigger( 'woocommerce_variations_save_variations_on_submit' ); + wc_meta_boxes_product_variations_ajax.save_changes( wc_meta_boxes_product_variations_ajax.save_on_submit_done ); + } + }, + + /** + * After saved, continue with form submission + */ + save_on_submit_done: function() { + var postForm = $( 'form#post' ), + callerid = postForm.data( 'callerid' ); + + if ( 'publish' === callerid ) { + postForm.append('').trigger( 'submit' ); + } else { + postForm.append('').trigger( 'submit' ); + } + }, + + /** + * Discart changes. + * + * @return {Bool} + */ + cancel_variations: function() { + var current = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-page' ), 10 ); + + $( '#variable_product_options' ).find( '.woocommerce_variations .variation-needs-update' ) + .removeClass( 'variation-needs-update' ); + $( '.variations-defaults select' ).each( function() { + $( this ).val( $( this ).attr( 'data-current' ) ); + }); + + wc_meta_boxes_product_variations_pagenav.go_to_page( current ); + + return false; + }, + + /** + * Add variation + * + * @return {Bool} + */ + add_variation: function() { + wc_meta_boxes_product_variations_ajax.block(); + + var data = { + action: 'woocommerce_add_variation', + post_id: woocommerce_admin_meta_boxes_variations.post_id, + loop: $( '.woocommerce_variation' ).length, + security: woocommerce_admin_meta_boxes_variations.add_variation_nonce + }; + + $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { + var variation = $( response ); + variation.addClass( 'variation-needs-update' ); + + $( '.woocommerce-notice-invalid-variation' ).remove(); + $( '#variable_product_options' ).find( '.woocommerce_variations' ).prepend( variation ); + $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', 1 ); + wc_meta_boxes_product_variations_ajax.unblock(); + }); + + return false; + }, + + /** + * Remove variation + * + * @return {Bool} + */ + remove_variation: function() { + wc_meta_boxes_product_variations_ajax.check_for_changes(); + + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_remove_variation ) ) { + var variation = $( this ).attr( 'rel' ), + variation_ids = [], + data = { + action: 'woocommerce_remove_variations' + }; + + wc_meta_boxes_product_variations_ajax.block(); + + if ( 0 < variation ) { + variation_ids.push( variation ); + + data.variation_ids = variation_ids; + data.security = woocommerce_admin_meta_boxes_variations.delete_variations_nonce; + + $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function() { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + current_page = parseInt( wrapper.attr( 'data-page' ), 10 ), + total_pages = Math.ceil( ( + parseInt( wrapper.attr( 'data-total' ), 10 ) - 1 + ) / woocommerce_admin_meta_boxes_variations.variations_per_page ), + page = 1; + + $( '#woocommerce-product-data' ).trigger( 'woocommerce_variations_removed' ); + + if ( current_page === total_pages || current_page <= total_pages ) { + page = current_page; + } else if ( current_page > total_pages && 0 !== total_pages ) { + page = total_pages; + } + + wc_meta_boxes_product_variations_pagenav.go_to_page( page, -1 ); + }); + + } else { + wc_meta_boxes_product_variations_ajax.unblock(); + } + } + + return false; + }, + + /** + * Link all variations (or at least try :p) + * + * @return {Bool} + */ + link_all_variations: function() { + wc_meta_boxes_product_variations_ajax.check_for_changes(); + + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_link_all_variations ) ) { + wc_meta_boxes_product_variations_ajax.block(); + + var data = { + action: 'woocommerce_link_all_variations', + post_id: woocommerce_admin_meta_boxes_variations.post_id, + security: woocommerce_admin_meta_boxes_variations.link_variation_nonce + }; + + $.post( woocommerce_admin_meta_boxes_variations.ajax_url, data, function( response ) { + var count = parseInt( response, 10 ); + + if ( 1 === count ) { + window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variation_added ); + } else if ( 0 === count || count > 1 ) { + window.alert( count + ' ' + woocommerce_admin_meta_boxes_variations.i18n_variations_added ); + } else { + window.alert( woocommerce_admin_meta_boxes_variations.i18n_no_variations_added ); + } + + if ( count > 0 ) { + wc_meta_boxes_product_variations_pagenav.go_to_page( 1, count ); + $( '#variable_product_options' ).trigger( 'woocommerce_variations_added', count ); + } else { + wc_meta_boxes_product_variations_ajax.unblock(); + } + }); + } + + return false; + }, + + /** + * Add new class when have changes in some input + */ + input_changed: function() { + $( this ) + .closest( '.woocommerce_variation' ) + .addClass( 'variation-needs-update' ); + + $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + + $( '#variable_product_options' ).trigger( 'woocommerce_variations_input_changed' ); + }, + + /** + * Added new .variation-needs-update class when defaults is changed + */ + defaults_changed: function() { + $( this ) + .closest( '#variable_product_options' ) + .find( '.woocommerce_variation:first' ) + .addClass( 'variation-needs-update' ); + + $( 'button.cancel-variation-changes, button.save-variation-changes' ).prop( 'disabled', false ); + + $( '#variable_product_options' ).trigger( 'woocommerce_variations_defaults_changed' ); + }, + + /** + * Actions + */ + do_variation_action: function() { + var do_variation_action = $( 'select.variation_actions' ).val(), + data = {}, + changes = 0, + value; + + switch ( do_variation_action ) { + case 'add_variation' : + wc_meta_boxes_product_variations_ajax.add_variation(); + return; + case 'link_all_variations' : + wc_meta_boxes_product_variations_ajax.link_all_variations(); + return; + case 'delete_all' : + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_delete_all_variations ) ) { + if ( window.confirm( woocommerce_admin_meta_boxes_variations.i18n_last_warning ) ) { + data.allowed = true; + changes = parseInt( $( '#variable_product_options' ).find( '.woocommerce_variations' ) + .attr( 'data-total' ), 10 ) * -1; + } + } + break; + case 'variable_regular_price_increase' : + case 'variable_regular_price_decrease' : + case 'variable_sale_price_increase' : + case 'variable_sale_price_decrease' : + value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value_fixed_or_percent ); + + if ( value != null ) { + if ( value.indexOf( '%' ) >= 0 ) { + data.value = accounting.unformat( value.replace( /\%/, '' ), woocommerce_admin.mon_decimal_point ) + '%'; + } else { + data.value = accounting.unformat( value, woocommerce_admin.mon_decimal_point ); + } + } else { + return; + } + break; + case 'variable_regular_price' : + case 'variable_sale_price' : + case 'variable_stock' : + case 'variable_low_stock_amount' : + case 'variable_weight' : + case 'variable_length' : + case 'variable_width' : + case 'variable_height' : + case 'variable_download_limit' : + case 'variable_download_expiry' : + value = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_enter_a_value ); + + if ( value != null ) { + data.value = value; + } else { + return; + } + break; + case 'variable_sale_schedule' : + data.date_from = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_start ); + data.date_to = window.prompt( woocommerce_admin_meta_boxes_variations.i18n_scheduled_sale_end ); + + if ( null === data.date_from ) { + data.date_from = false; + } + + if ( null === data.date_to ) { + data.date_to = false; + } + + if ( false === data.date_to && false === data.date_from ) { + return; + } + break; + default : + $( 'select.variation_actions' ).trigger( do_variation_action ); + data = $( 'select.variation_actions' ).triggerHandler( do_variation_action + '_ajax_data', data ); + break; + } + + if ( 'delete_all' === do_variation_action && data.allowed ) { + $( '#variable_product_options' ).find( '.variation-needs-update' ).removeClass( 'variation-needs-update' ); + } else { + wc_meta_boxes_product_variations_ajax.check_for_changes(); + } + + wc_meta_boxes_product_variations_ajax.block(); + + $.ajax({ + url: woocommerce_admin_meta_boxes_variations.ajax_url, + data: { + action: 'woocommerce_bulk_edit_variations', + security: woocommerce_admin_meta_boxes_variations.bulk_edit_variations_nonce, + product_id: woocommerce_admin_meta_boxes_variations.post_id, + product_type: $( '#product-type' ).val(), + bulk_action: do_variation_action, + data: data + }, + type: 'POST', + success: function() { + wc_meta_boxes_product_variations_pagenav.go_to_page( 1, changes ); + } + }); + } + }; + + /** + * Product variations pagenav + */ + var wc_meta_boxes_product_variations_pagenav = { + + /** + * Initialize products variations meta box + */ + init: function() { + $( document.body ) + .on( 'woocommerce_variations_added', this.update_single_quantity ) + .on( 'change', '.variations-pagenav .page-selector', this.page_selector ) + .on( 'click', '.variations-pagenav .first-page', this.first_page ) + .on( 'click', '.variations-pagenav .prev-page', this.prev_page ) + .on( 'click', '.variations-pagenav .next-page', this.next_page ) + .on( 'click', '.variations-pagenav .last-page', this.last_page ); + }, + + /** + * Set variations count + * + * @param {Int} qty + * + * @return {Int} + */ + update_variations_count: function( qty ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + total = parseInt( wrapper.attr( 'data-total' ), 10 ) + qty, + displaying_num = $( '.variations-pagenav .displaying-num' ); + + // Set the new total of variations + wrapper.attr( 'data-total', total ); + + if ( 1 === total ) { + displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_single.replace( '%qty%', total ) ); + } else { + displaying_num.text( woocommerce_admin_meta_boxes_variations.i18n_variation_count_plural.replace( '%qty%', total ) ); + } + + return total; + }, + + /** + * Update variations quantity when add a new variation + * + * @param {Object} event + * @param {Int} qty + */ + update_single_quantity: function( event, qty ) { + if ( 1 === qty ) { + var page_nav = $( '.variations-pagenav' ); + + wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ); + + if ( page_nav.is( ':hidden' ) ) { + $( 'option, optgroup', '.variation_actions' ).show(); + $( '.variation_actions' ).val( 'add_variation' ); + $( '#variable_product_options' ).find( '.toolbar' ).show(); + page_nav.show(); + $( '.pagination-links', page_nav ).hide(); + } + } + }, + + /** + * Set the pagenav fields + * + * @param {Int} qty + */ + set_paginav: function( qty ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + new_qty = wc_meta_boxes_product_variations_pagenav.update_variations_count( qty ), + toolbar = $( '#variable_product_options' ).find( '.toolbar' ), + variation_action = $( '.variation_actions' ), + page_nav = $( '.variations-pagenav' ), + displaying_links = $( '.pagination-links', page_nav ), + total_pages = Math.ceil( new_qty / woocommerce_admin_meta_boxes_variations.variations_per_page ), + options = ''; + + // Set the new total of pages + wrapper.attr( 'data-total_pages', total_pages ); + + $( '.total-pages', page_nav ).text( total_pages ); + + // Set the new pagenav options + for ( var i = 1; i <= total_pages; i++ ) { + options += ''; + } + + $( '.page-selector', page_nav ).empty().html( options ); + + // Show/hide pagenav + if ( 0 === new_qty ) { + toolbar.not( '.toolbar-top, .toolbar-buttons' ).hide(); + page_nav.hide(); + $( 'option, optgroup', variation_action ).hide(); + $( '.variation_actions' ).val( 'add_variation' ); + $( 'option[data-global="true"]', variation_action ).show(); + + } else { + toolbar.show(); + page_nav.show(); + $( 'option, optgroup', variation_action ).show(); + $( '.variation_actions' ).val( 'add_variation' ); + + // Show/hide links + if ( 1 === total_pages ) { + displaying_links.hide(); + } else { + displaying_links.show(); + } + } + }, + + /** + * Check button if enabled and if don't have changes + * + * @return {Bool} + */ + check_is_enabled: function( current ) { + return ! $( current ).hasClass( 'disabled' ); + }, + + /** + * Change "disabled" class on pagenav + */ + change_classes: function( selected, total ) { + var first_page = $( '.variations-pagenav .first-page' ), + prev_page = $( '.variations-pagenav .prev-page' ), + next_page = $( '.variations-pagenav .next-page' ), + last_page = $( '.variations-pagenav .last-page' ); + + if ( 1 === selected ) { + first_page.addClass( 'disabled' ); + prev_page.addClass( 'disabled' ); + } else { + first_page.removeClass( 'disabled' ); + prev_page.removeClass( 'disabled' ); + } + + if ( total === selected ) { + next_page.addClass( 'disabled' ); + last_page.addClass( 'disabled' ); + } else { + next_page.removeClass( 'disabled' ); + last_page.removeClass( 'disabled' ); + } + }, + + /** + * Set page + */ + set_page: function( page ) { + $( '.variations-pagenav .page-selector' ).val( page ).first().trigger( 'change' ); + }, + + /** + * Navigate on variations pages + * + * @param {Int} page + * @param {Int} qty + */ + go_to_page: function( page, qty ) { + page = page || 1; + qty = qty || 0; + + wc_meta_boxes_product_variations_pagenav.set_paginav( qty ); + wc_meta_boxes_product_variations_pagenav.set_page( page ); + }, + + /** + * Paginav pagination selector + */ + page_selector: function() { + var selected = parseInt( $( this ).val(), 10 ), + wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ); + + $( '.variations-pagenav .page-selector' ).val( selected ); + + wc_meta_boxes_product_variations_ajax.check_for_changes(); + wc_meta_boxes_product_variations_pagenav.change_classes( selected, parseInt( wrapper.attr( 'data-total_pages' ), 10 ) ); + wc_meta_boxes_product_variations_ajax.load_variations( selected ); + }, + + /** + * Go to first page + * + * @return {Bool} + */ + first_page: function() { + if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + wc_meta_boxes_product_variations_pagenav.set_page( 1 ); + } + + return false; + }, + + /** + * Go to previous page + * + * @return {Bool} + */ + prev_page: function() { + if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + prev_page = parseInt( wrapper.attr( 'data-page' ), 10 ) - 1, + new_page = ( 0 < prev_page ) ? prev_page : 1; + + wc_meta_boxes_product_variations_pagenav.set_page( new_page ); + } + + return false; + }, + + /** + * Go to next page + * + * @return {Bool} + */ + next_page: function() { + if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + var wrapper = $( '#variable_product_options' ).find( '.woocommerce_variations' ), + total_pages = parseInt( wrapper.attr( 'data-total_pages' ), 10 ), + next_page = parseInt( wrapper.attr( 'data-page' ), 10 ) + 1, + new_page = ( total_pages >= next_page ) ? next_page : total_pages; + + wc_meta_boxes_product_variations_pagenav.set_page( new_page ); + } + + return false; + }, + + /** + * Go to last page + * + * @return {Bool} + */ + last_page: function() { + if ( wc_meta_boxes_product_variations_pagenav.check_is_enabled( this ) ) { + var last_page = $( '#variable_product_options' ).find( '.woocommerce_variations' ).attr( 'data-total_pages' ); + + wc_meta_boxes_product_variations_pagenav.set_page( last_page ); + } + + return false; + } + }; + + wc_meta_boxes_product_variations_actions.init(); + wc_meta_boxes_product_variations_media.init(); + wc_meta_boxes_product_variations_ajax.init(); + wc_meta_boxes_product_variations_pagenav.init(); + +}); diff --git a/assets/js/admin/meta-boxes-product-variation.min.js b/assets/js/admin/meta-boxes-product-variation.min.js new file mode 100644 index 0000000..ac5b629 --- /dev/null +++ b/assets/js/admin/meta-boxes-product-variation.min.js @@ -0,0 +1 @@ +jQuery(function(c){"use strict";var o={init:function(){c("#variable_product_options").on("change","input.variable_is_downloadable",this.variable_is_downloadable).on("change","input.variable_is_virtual",this.variable_is_virtual).on("change","input.variable_manage_stock",this.variable_manage_stock).on("click","button.notice-dismiss",this.notice_dismiss).on("click","h3 .sort",this.set_menu_order).on("reload",this.reload),c("input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock").trigger("change"),c("#woocommerce-product-data").on("woocommerce_variations_loaded",this.variations_loaded),c(document.body).on("woocommerce_variations_added",this.variation_added)},reload:function(){n.load_variations(1),d.set_paginav(0)},variable_is_downloadable:function(){c(this).closest(".woocommerce_variation").find(".show_if_variation_downloadable").hide(),c(this).is(":checked")&&c(this).closest(".woocommerce_variation").find(".show_if_variation_downloadable").show()},variable_is_virtual:function(){c(this).closest(".woocommerce_variation").find(".hide_if_variation_virtual").show(),c(this).is(":checked")&&c(this).closest(".woocommerce_variation").find(".hide_if_variation_virtual").hide()},variable_manage_stock:function(){c(this).closest(".woocommerce_variation").find(".show_if_variation_manage_stock").hide(),c(this).closest(".woocommerce_variation").find(".variable_stock_status").show(),c(this).is(":checked")&&(c(this).closest(".woocommerce_variation").find(".show_if_variation_manage_stock").show(),c(this).closest(".woocommerce_variation").find(".variable_stock_status").hide()),c("input#_manage_stock:checked").length&&c(this).closest(".woocommerce_variation").find(".variable_stock_status").hide()},notice_dismiss:function(){c(this).closest("div.notice").remove()},variations_loaded:function(a,e){e=e||!1;var i=c("#woocommerce-product-data");e||(c("input.variable_is_downloadable, input.variable_is_virtual, input.variable_manage_stock",i).trigger("change"),c(".woocommerce_variation",i).each(function(a,e){var i=c(e),o=c(".sale_price_dates_from",i).val(),e=c(".sale_price_dates_to",i).val();""===o&&""===e||c("a.sale_schedule",i).trigger("click")}),c(".woocommerce_variations .variation-needs-update",i).removeClass("variation-needs-update"),c("button.cancel-variation-changes, button.save-variation-changes",i).attr("disabled","disabled")),c("#tiptip_holder").removeAttr("style"),c("#tiptip_arrow").removeAttr("style"),c(".woocommerce_variations .tips, .woocommerce_variations .help_tip, .woocommerce_variations .woocommerce-help-tip",i).tipTip({attribute:"data-tip",fadeIn:50,fadeOut:50,delay:200}),c(".sale_price_dates_fields",i).find("input").datepicker({defaultDate:"",dateFormat:"yy-mm-dd",numberOfMonths:1,showButtonPanel:!0,onSelect:function(){var a=c(this).is(".sale_price_dates_from")?"minDate":"maxDate",e=c(this).closest(".sale_price_dates_fields").find("input"),i=c(this).datepicker("getDate");e.not(this).datepicker("option",a,i),c(this).trigger("change")}}),c(".woocommerce_variations",i).sortable({items:".woocommerce_variation",cursor:"move",axis:"y",handle:".sort",scrollSensitivity:40,forcePlaceholderSize:!0,helper:"clone",opacity:.65,stop:function(){o.variation_row_indexes()}}),c(document.body).trigger("wc-enhanced-select-init")},variation_added:function(a,e){1===e&&o.variations_loaded(null,!0)},set_menu_order:function(a){a.preventDefault();var e=c(this).closest(".woocommerce_variation").find(".variation_menu_order"),i=c(this).closest(".woocommerce_variation").find(".variable_post_id").val(),a=window.prompt(woocommerce_admin_meta_boxes_variations.i18n_enter_menu_order,e.val());null!=a&&(e.val(parseInt(a,10)).trigger("change"),c(this).closest(".woocommerce_variation").append(''),c(this).closest(".woocommerce_variation").append(''),n.save_variations())},variation_row_indexes:function(){var a=c("#variable_product_options").find(".woocommerce_variations"),a=parseInt(a.attr("data-page"),10),i=parseInt((a-1)*woocommerce_admin_meta_boxes_variations.variations_per_page,10);c(".woocommerce_variations .woocommerce_variation").each(function(a,e){c(".variation_menu_order",e).val(parseInt(c(e).index(".woocommerce_variations .woocommerce_variation"),10)+1+i).trigger("change")})}},t={variable_image_frame:null,setting_variation_image_id:null,setting_variation_image:null,wp_media_post_id:wp.media.model.settings.post.id,init:function(){c("#variable_product_options").on("click",".upload_image_button",this.add_image),c("a.add_media").on("click",this.restore_wp_media_post_id)},add_image:function(a){var e=c(this),i=e.attr("rel"),o=e.closest(".upload_image");if(t.setting_variation_image=o,t.setting_variation_image_id=i,a.preventDefault(),e.is(".remove"))c(".upload_image_id",t.setting_variation_image).val("").trigger("change"),t.setting_variation_image.find("img").eq(0).attr("src",woocommerce_admin_meta_boxes_variations.woocommerce_placeholder_img_src),t.setting_variation_image.find(".upload_image_button").removeClass("remove");else{if(t.variable_image_frame)return t.variable_image_frame.uploader.uploader.param("post_id",t.setting_variation_image_id),void t.variable_image_frame.open();wp.media.model.settings.post.id=t.setting_variation_image_id,t.variable_image_frame=wp.media.frames.variable_image=wp.media({title:woocommerce_admin_meta_boxes_variations.i18n_choose_image,button:{text:woocommerce_admin_meta_boxes_variations.i18n_set_image},states:[new wp.media.controller.Library({title:woocommerce_admin_meta_boxes_variations.i18n_choose_image,filterable:"all"})]}),t.variable_image_frame.on("select",function(){var a=t.variable_image_frame.state().get("selection").first().toJSON(),e=(a.sizes&&a.sizes.thumbnail?a.sizes.thumbnail:a).url;c(".upload_image_id",t.setting_variation_image).val(a.id).trigger("change"),t.setting_variation_image.find(".upload_image_button").addClass("remove"),t.setting_variation_image.find("img").eq(0).attr("src",e),wp.media.model.settings.post.id=t.wp_media_post_id}),t.variable_image_frame.open()}},restore_wp_media_post_id:function(){wp.media.model.settings.post.id=t.wp_media_post_id}},n={init:function(){c("li.variations_tab a").on("click",this.initial_load),c("#variable_product_options").on("click","button.save-variation-changes",this.save_variations).on("click","button.cancel-variation-changes",this.cancel_variations).on("click",".remove_variation",this.remove_variation).on("click",".downloadable_files a.delete",this.input_changed),c(document.body).on("change","#variable_product_options .woocommerce_variations :input",this.input_changed).on("change",".variations-defaults select",this.defaults_changed);var a=c("form#post");a.on("submit",this.save_on_submit),c("input:submit",a).on("click keypress",function(){a.data("callerid",this.id)}),c(".wc-metaboxes-wrapper").on("click","a.do_variation_action",this.do_variation_action)},check_for_changes:function(){var a=c("#variable_product_options").find(".woocommerce_variations .variation-needs-update");if(0'):a.append('')).trigger("submit")},cancel_variations:function(){var a=parseInt(c("#variable_product_options").find(".woocommerce_variations").attr("data-page"),10);return c("#variable_product_options").find(".woocommerce_variations .variation-needs-update").removeClass("variation-needs-update"),c(".variations-defaults select").each(function(){c(this).val(c(this).attr("data-current"))}),d.go_to_page(a),!1},add_variation:function(){n.block();var a={action:"woocommerce_add_variation",post_id:woocommerce_admin_meta_boxes_variations.post_id,loop:c(".woocommerce_variation").length,security:woocommerce_admin_meta_boxes_variations.add_variation_nonce};return c.post(woocommerce_admin_meta_boxes_variations.ajax_url,a,function(a){a=c(a);a.addClass("variation-needs-update"),c(".woocommerce-notice-invalid-variation").remove(),c("#variable_product_options").find(".woocommerce_variations").prepend(a),c("button.cancel-variation-changes, button.save-variation-changes").prop("disabled",!1),c("#variable_product_options").trigger("woocommerce_variations_added",1),n.unblock()}),!1},remove_variation:function(){var a,e,i;return n.check_for_changes(),window.confirm(woocommerce_admin_meta_boxes_variations.i18n_remove_variation)&&(a=c(this).attr("rel"),e=[],i={action:"woocommerce_remove_variations"},n.block(),0'+s+"";c(".page-selector",n).empty().html(_),0===i?(o.not(".toolbar-top, .toolbar-buttons").hide(),n.hide(),c("option, optgroup",t).hide(),c(".variation_actions").val("add_variation"),c('option[data-global="true"]',t).show()):(o.show(),n.show(),c("option, optgroup",t).show(),c(".variation_actions").val("add_variation"),1===r?a.hide():a.show())},check_is_enabled:function(a){return!c(a).hasClass("disabled")},change_classes:function(a,e){var i=c(".variations-pagenav .first-page"),o=c(".variations-pagenav .prev-page"),t=c(".variations-pagenav .next-page"),n=c(".variations-pagenav .last-page");1===a?(i.addClass("disabled"),o.addClass("disabled")):(i.removeClass("disabled"),o.removeClass("disabled")),e===a?(t.addClass("disabled"),n.addClass("disabled")):(t.removeClass("disabled"),n.removeClass("disabled"))},set_page:function(a){c(".variations-pagenav .page-selector").val(a).first().trigger("change")},go_to_page:function(a,e){a=a||1,e=e||0,d.set_paginav(e),d.set_page(a)},page_selector:function(){var a=parseInt(c(this).val(),10),e=c("#variable_product_options").find(".woocommerce_variations");c(".variations-pagenav .page-selector").val(a),n.check_for_changes(),d.change_classes(a,parseInt(e.attr("data-total_pages"),10)),n.load_variations(a)},first_page:function(){return d.check_is_enabled(this)&&d.set_page(1),!1},prev_page:function(){var a;return d.check_is_enabled(this)&&(a=c("#variable_product_options").find(".woocommerce_variations"),a=0<(a=parseInt(a.attr("data-page"),10)-1)?a:1,d.set_page(a)),!1},next_page:function(){var a,e;return d.check_is_enabled(this)&&(a=c("#variable_product_options").find(".woocommerce_variations"),e=parseInt(a.attr("data-total_pages"),10),e=(a=parseInt(a.attr("data-page"),10)+1)<=e?a:e,d.set_page(e)),!1},last_page:function(){var a;return d.check_is_enabled(this)&&(a=c("#variable_product_options").find(".woocommerce_variations").attr("data-total_pages"),d.set_page(a)),!1}};o.init(),t.init(),n.init(),d.init()}); \ No newline at end of file diff --git a/assets/js/admin/meta-boxes-product.js b/assets/js/admin/meta-boxes-product.js new file mode 100644 index 0000000..e9cad28 --- /dev/null +++ b/assets/js/admin/meta-boxes-product.js @@ -0,0 +1,697 @@ +/*global woocommerce_admin_meta_boxes */ +jQuery( function( $ ) { + + // Scroll to first checked category + // https://github.com/scribu/wp-category-checklist-tree/blob/d1c3c1f449e1144542efa17dde84a9f52ade1739/category-checklist-tree.php + $( function() { + $( '[id$="-all"] > ul.categorychecklist' ).each( function() { + var $list = $( this ); + var $firstChecked = $list.find( ':checked' ).first(); + + if ( ! $firstChecked.length ) { + return; + } + + var pos_first = $list.find( 'input' ).position().top; + var pos_checked = $firstChecked.position().top; + + $list.closest( '.tabs-panel' ).scrollTop( pos_checked - pos_first + 5 ); + }); + }); + + // Prevent enter submitting post form. + $( '#upsell_product_data' ).on( 'keypress', function( e ) { + if ( e.keyCode === 13 ) { + return false; + } + }); + + // Type box. + if ( $( 'body' ).hasClass( 'wc-wp-version-gte-55' ) ) { + $( '.type_box' ).appendTo( '#woocommerce-product-data .hndle' ); + } else { + $( '.type_box' ).appendTo( '#woocommerce-product-data .hndle span' ); + } + + $( function() { + var woocommerce_product_data = $( '#woocommerce-product-data' ); + + // Prevent inputs in meta box headings opening/closing contents. + woocommerce_product_data.find( '.hndle' ).off( 'click.postboxes' ); + + woocommerce_product_data.on( 'click', '.hndle', function( event ) { + + // If the user clicks on some form input inside the h3 the box should not be toggled. + if ( $( event.target ).filter( 'input, option, label, select' ).length ) { + return; + } + + if ( woocommerce_product_data.hasClass( 'closed' ) ) { + woocommerce_product_data.removeClass( 'closed' ); + } else { + woocommerce_product_data.addClass( 'closed' ); + } + }); + }); + + // Catalog Visibility. + $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).on( 'click', function() { + if ( $( '#catalog-visibility-select' ).is( ':hidden' ) ) { + $( '#catalog-visibility-select' ).slideDown( 'fast' ); + $( this ).hide(); + } + return false; + }); + $( '#catalog-visibility' ).find( '.save-post-visibility' ).on( 'click', function() { + $( '#catalog-visibility-select' ).slideUp( 'fast' ); + $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).show(); + + var label = $( 'input[name=_visibility]:checked' ).attr( 'data-label' ); + + if ( $( 'input[name=_featured]' ).is( ':checked' ) ) { + label = label + ', ' + woocommerce_admin_meta_boxes.featured_label; + $( 'input[name=_featured]' ).attr( 'checked', 'checked' ); + } + + $( '#catalog-visibility-display' ).text( label ); + return false; + }); + $( '#catalog-visibility' ).find( '.cancel-post-visibility' ).on( 'click', function() { + $( '#catalog-visibility-select' ).slideUp( 'fast' ); + $( '#catalog-visibility' ).find( '.edit-catalog-visibility' ).show(); + + var current_visibility = $( '#current_visibility' ).val(); + var current_featured = $( '#current_featured' ).val(); + + $( 'input[name=_visibility]' ).prop( 'checked', false ); + $( 'input[name=_visibility][value=' + current_visibility + ']' ).attr( 'checked', 'checked' ); + + var label = $( 'input[name=_visibility]:checked' ).attr( 'data-label' ); + + if ( 'yes' === current_featured ) { + label = label + ', ' + woocommerce_admin_meta_boxes.featured_label; + $( 'input[name=_featured]' ).attr( 'checked', 'checked' ); + } else { + $( 'input[name=_featured]' ).prop( 'checked', false ); + } + + $( '#catalog-visibility-display' ).text( label ); + return false; + }); + + // Product type specific options. + $( 'select#product-type' ).on( 'change', function() { + + // Get value. + var select_val = $( this ).val(); + + if ( 'variable' === select_val ) { + $( 'input#_manage_stock' ).trigger( 'change' ); + $( 'input#_downloadable' ).prop( 'checked', false ); + $( 'input#_virtual' ).prop( 'checked', false ); + } else if ( 'grouped' === select_val ) { + $( 'input#_downloadable' ).prop( 'checked', false ); + $( 'input#_virtual' ).prop( 'checked', false ); + } else if ( 'external' === select_val ) { + $( 'input#_downloadable' ).prop( 'checked', false ); + $( 'input#_virtual' ).prop( 'checked', false ); + } + + show_and_hide_panels(); + + $( 'ul.wc-tabs li:visible' ).eq( 0 ).find( 'a' ).trigger( 'click' ); + + $( document.body ).trigger( 'woocommerce-product-type-change', select_val, $( this ) ); + + }).trigger( 'change' ); + + $( 'input#_downloadable, input#_virtual' ).on( 'change', function() { + show_and_hide_panels(); + }); + + function show_and_hide_panels() { + var product_type = $( 'select#product-type' ).val(); + var is_virtual = $( 'input#_virtual:checked' ).length; + var is_downloadable = $( 'input#_downloadable:checked' ).length; + + // Hide/Show all with rules. + var hide_classes = '.hide_if_downloadable, .hide_if_virtual'; + var show_classes = '.show_if_downloadable, .show_if_virtual'; + + $.each( woocommerce_admin_meta_boxes.product_types, function( index, value ) { + hide_classes = hide_classes + ', .hide_if_' + value; + show_classes = show_classes + ', .show_if_' + value; + }); + + $( hide_classes ).show(); + $( show_classes ).hide(); + + // Shows rules. + if ( is_downloadable ) { + $( '.show_if_downloadable' ).show(); + } + if ( is_virtual ) { + $( '.show_if_virtual' ).show(); + + // If user enables virtual while on shipping tab, switch to general tab. + if ( $( '.shipping_options.shipping_tab' ).hasClass( 'active' ) ) { + $( '.general_options.general_tab > a' ).trigger( 'click' ); + } + } + + $( '.show_if_' + product_type ).show(); + + // Hide rules. + if ( is_downloadable ) { + $( '.hide_if_downloadable' ).hide(); + } + if ( is_virtual ) { + $( '.hide_if_virtual' ).hide(); + } + + $( '.hide_if_' + product_type ).hide(); + + $( 'input#_manage_stock' ).trigger( 'change' ); + + // Hide empty panels/tabs after display. + $( '.woocommerce_options_panel' ).each( function() { + var $children = $( this ).children( '.options_group' ); + + if ( 0 === $children.length ) { + return; + } + + var $invisble = $children.filter( function() { + return 'none' === $( this ).css( 'display' ); + }); + + // Hide panel. + if ( $invisble.length === $children.length ) { + var $id = $( this ).prop( 'id' ); + $( '.product_data_tabs' ).find( 'li a[href="#' + $id + '"]' ).parent().hide(); + } + }); + } + + // Sale price schedule. + $( '.sale_price_dates_fields' ).each( function() { + var $these_sale_dates = $( this ); + var sale_schedule_set = false; + var $wrap = $these_sale_dates.closest( 'div, table' ); + + $these_sale_dates.find( 'input' ).each( function() { + if ( '' !== $( this ).val() ) { + sale_schedule_set = true; + } + }); + + if ( sale_schedule_set ) { + $wrap.find( '.sale_schedule' ).hide(); + $wrap.find( '.sale_price_dates_fields' ).show(); + } else { + $wrap.find( '.sale_schedule' ).show(); + $wrap.find( '.sale_price_dates_fields' ).hide(); + } + }); + + $( '#woocommerce-product-data' ).on( 'click', '.sale_schedule', function() { + var $wrap = $( this ).closest( 'div, table' ); + + $( this ).hide(); + $wrap.find( '.cancel_sale_schedule' ).show(); + $wrap.find( '.sale_price_dates_fields' ).show(); + + return false; + }); + $( '#woocommerce-product-data' ).on( 'click', '.cancel_sale_schedule', function() { + var $wrap = $( this ).closest( 'div, table' ); + + $( this ).hide(); + $wrap.find( '.sale_schedule' ).show(); + $wrap.find( '.sale_price_dates_fields' ).hide(); + $wrap.find( '.sale_price_dates_fields' ).find( 'input' ).val(''); + + return false; + }); + + // File inputs. + $( '#woocommerce-product-data' ).on( 'click','.downloadable_files a.insert', function() { + $( this ).closest( '.downloadable_files' ).find( 'tbody' ).append( $( this ).data( 'row' ) ); + return false; + }); + $( '#woocommerce-product-data' ).on( 'click','.downloadable_files a.delete',function() { + $( this ).closest( 'tr' ).remove(); + return false; + }); + + // Stock options. + $( 'input#_manage_stock' ).on( 'change', function() { + if ( $( this ).is( ':checked' ) ) { + $( 'div.stock_fields' ).show(); + $( 'p.stock_status_field' ).hide(); + } else { + var product_type = $( 'select#product-type' ).val(); + + $( 'div.stock_fields' ).hide(); + $( 'p.stock_status_field:not( .hide_if_' + product_type + ' )' ).show(); + } + + $( 'input.variable_manage_stock' ).trigger( 'change' ); + }).trigger( 'change' ); + + // Date picker fields. + function date_picker_select( datepicker ) { + var option = $( datepicker ).next().is( '.hasDatepicker' ) ? 'minDate' : 'maxDate', + otherDateField = 'minDate' === option ? $( datepicker ).next() : $( datepicker ).prev(), + date = $( datepicker ).datepicker( 'getDate' ); + + $( otherDateField ).datepicker( 'option', option, date ); + $( datepicker ).trigger( 'change' ); + } + + $( '.sale_price_dates_fields' ).each( function() { + $( this ).find( 'input' ).datepicker({ + defaultDate: '', + dateFormat: 'yy-mm-dd', + numberOfMonths: 1, + showButtonPanel: true, + onSelect: function() { + date_picker_select( $( this ) ); + } + }); + $( this ).find( 'input' ).each( function() { date_picker_select( $( this ) ); } ); + }); + + // Attribute Tables. + + // Initial order. + var woocommerce_attribute_items = $( '.product_attributes' ).find( '.woocommerce_attribute' ).get(); + + woocommerce_attribute_items.sort( function( a, b ) { + var compA = parseInt( $( a ).attr( 'rel' ), 10 ); + var compB = parseInt( $( b ).attr( 'rel' ), 10 ); + return ( compA < compB ) ? -1 : ( compA > compB ) ? 1 : 0; + }); + $( woocommerce_attribute_items ).each( function( index, el ) { + $( '.product_attributes' ).append( el ); + }); + + function attribute_row_indexes() { + $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { + $( '.attribute_position', el ).val( parseInt( $( el ).index( '.product_attributes .woocommerce_attribute' ), 10 ) ); + }); + } + + $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { + if ( $( el ).css( 'display' ) !== 'none' && $( el ).is( '.taxonomy' ) ) { + $( 'select.attribute_taxonomy' ).find( 'option[value="' + $( el ).data( 'taxonomy' ) + '"]' ).attr( 'disabled', 'disabled' ); + } + }); + + // Add rows. + $( 'button.add_attribute' ).on( 'click', function() { + var size = $( '.product_attributes .woocommerce_attribute' ).length; + var attribute = $( 'select.attribute_taxonomy' ).val(); + var $wrapper = $( this ).closest( '#product_attributes' ); + var $attributes = $wrapper.find( '.product_attributes' ); + var product_type = $( 'select#product-type' ).val(); + var data = { + action: 'woocommerce_add_attribute', + taxonomy: attribute, + i: size, + security: woocommerce_admin_meta_boxes.add_attribute_nonce + }; + + $wrapper.block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + $attributes.append( response ); + + if ( 'variable' !== product_type ) { + $attributes.find( '.enable_variation' ).hide(); + } + + $( document.body ).trigger( 'wc-enhanced-select-init' ); + + attribute_row_indexes(); + + $attributes.find( '.woocommerce_attribute' ).last().find( 'h3' ).trigger( 'click' ); + + $wrapper.unblock(); + + $( document.body ).trigger( 'woocommerce_added_attribute' ); + }); + + if ( attribute ) { + $( 'select.attribute_taxonomy' ).find( 'option[value="' + attribute + '"]' ).attr( 'disabled','disabled' ); + $( 'select.attribute_taxonomy' ).val( '' ); + } + + return false; + }); + + $( '.product_attributes' ).on( 'blur', 'input.attribute_name', function() { + $( this ).closest( '.woocommerce_attribute' ).find( 'strong.attribute_name' ).text( $( this ).val() ); + }); + + $( '.product_attributes' ).on( 'click', 'button.select_all_attributes', function() { + $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', 'selected' ); + $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); + return false; + }); + + $( '.product_attributes' ).on( 'click', 'button.select_no_attributes', function() { + $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', false ); + $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); + return false; + }); + + $( '.product_attributes' ).on( 'click', '.remove_row', function() { + if ( window.confirm( woocommerce_admin_meta_boxes.remove_attribute ) ) { + var $parent = $( this ).parent().parent(); + + if ( $parent.is( '.taxonomy' ) ) { + $parent.find( 'select, input[type=text]' ).val( '' ); + $parent.hide(); + $( 'select.attribute_taxonomy' ).find( 'option[value="' + $parent.data( 'taxonomy' ) + '"]' ).prop( 'disabled', false ); + } else { + $parent.find( 'select, input[type=text]' ).val( '' ); + $parent.hide(); + attribute_row_indexes(); + } + } + return false; + }); + + // Attribute ordering. + $( '.product_attributes' ).sortable({ + items: '.woocommerce_attribute', + cursor: 'move', + axis: 'y', + handle: 'h3', + scrollSensitivity: 40, + forcePlaceholderSize: true, + helper: 'clone', + opacity: 0.65, + placeholder: 'wc-metabox-sortable-placeholder', + start: function( event, ui ) { + ui.item.css( 'background-color', '#f6f6f6' ); + }, + stop: function( event, ui ) { + ui.item.removeAttr( 'style' ); + attribute_row_indexes(); + } + }); + + // Add a new attribute (via ajax). + $( '.product_attributes' ).on( 'click', 'button.add_new_attribute', function() { + + $( '.product_attributes' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + + var $wrapper = $( this ).closest( '.woocommerce_attribute' ); + var attribute = $wrapper.data( 'taxonomy' ); + var new_attribute_name = window.prompt( woocommerce_admin_meta_boxes.new_attribute_prompt ); + + if ( new_attribute_name ) { + + var data = { + action: 'woocommerce_add_new_attribute', + taxonomy: attribute, + term: new_attribute_name, + security: woocommerce_admin_meta_boxes.add_attribute_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + + if ( response.error ) { + // Error. + window.alert( response.error ); + } else if ( response.slug ) { + // Success. + $wrapper.find( 'select.attribute_values' ) + .append( '' ); + $wrapper.find( 'select.attribute_values' ).trigger( 'change' ); + } + + $( '.product_attributes' ).unblock(); + }); + + } else { + $( '.product_attributes' ).unblock(); + } + + return false; + }); + + // Save attributes and update variations. + $( '.save_attributes' ).on( 'click', function() { + + $( '.product_attributes' ).block({ + message: null, + overlayCSS: { + background: '#fff', + opacity: 0.6 + } + }); + var original_data = $( '.product_attributes' ).find( 'input, select, textarea' ); + var data = { + post_id : woocommerce_admin_meta_boxes.post_id, + product_type: $( '#product-type' ).val(), + data : original_data.serialize(), + action : 'woocommerce_save_attributes', + security : woocommerce_admin_meta_boxes.save_attributes_nonce + }; + + $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + if ( response.error ) { + // Error. + window.alert( response.error ); + } else if ( response.data ) { + // Success. + $( '.product_attributes' ).html( response.data.html ); + $( '.product_attributes' ).unblock(); + + // Hide the 'Used for variations' checkbox if not viewing a variable product + show_and_hide_panels(); + + // Make sure the dropdown is not disabled for empty value attributes. + $( 'select.attribute_taxonomy' ).find( 'option' ).prop( 'disabled', false ); + + $( '.product_attributes .woocommerce_attribute' ).each( function( index, el ) { + if ( $( el ).css( 'display' ) !== 'none' && $( el ).is( '.taxonomy' ) ) { + $( 'select.attribute_taxonomy' ) + .find( 'option[value="' + $( el ).data( 'taxonomy' ) + '"]' ) + .prop( 'disabled', true ); + } + }); + + // Reload variations panel. + var this_page = window.location.toString(); + this_page = this_page.replace( 'post-new.php?', 'post.php?post=' + woocommerce_admin_meta_boxes.post_id + '&action=edit&' ); + + $( '#variable_product_options' ).load( this_page + ' #variable_product_options_inner', function() { + $( '#variable_product_options' ).trigger( 'reload' ); + } ); + } + }); + }); + + // Uploading files. + var downloadable_file_frame; + var file_path_field; + + $( document.body ).on( 'click', '.upload_file_button', function( event ) { + var $el = $( this ); + + file_path_field = $el.closest( 'tr' ).find( 'td.file_url input' ); + + event.preventDefault(); + + // If the media frame already exists, reopen it. + if ( downloadable_file_frame ) { + downloadable_file_frame.open(); + return; + } + + var downloadable_file_states = [ + // Main states. + new wp.media.controller.Library({ + library: wp.media.query(), + multiple: true, + title: $el.data('choose'), + priority: 20, + filterable: 'uploaded' + }) + ]; + + // Create the media frame. + downloadable_file_frame = wp.media.frames.downloadable_file = wp.media({ + // Set the title of the modal. + title: $el.data('choose'), + library: { + type: '' + }, + button: { + text: $el.data('update') + }, + multiple: true, + states: downloadable_file_states + }); + + // When an image is selected, run a callback. + downloadable_file_frame.on( 'select', function() { + var file_path = ''; + var selection = downloadable_file_frame.state().get( 'selection' ); + + selection.map( function( attachment ) { + attachment = attachment.toJSON(); + if ( attachment.url ) { + file_path = attachment.url; + } + }); + + file_path_field.val( file_path ).trigger( 'change' ); + }); + + // Set post to 0 and set our custom type. + downloadable_file_frame.on( 'ready', function() { + downloadable_file_frame.uploader.options.uploader.params = { + type: 'downloadable_product' + }; + }); + + // Finally, open the modal. + downloadable_file_frame.open(); + }); + + // Download ordering. + $( '.downloadable_files tbody' ).sortable({ + items: 'tr', + cursor: 'move', + axis: 'y', + handle: 'td.sort', + scrollSensitivity: 40, + forcePlaceholderSize: true, + helper: 'clone', + opacity: 0.65 + }); + + // Product gallery file uploads. + var product_gallery_frame; + var $image_gallery_ids = $( '#product_image_gallery' ); + var $product_images = $( '#product_images_container' ).find( 'ul.product_images' ); + + $( '.add_product_images' ).on( 'click', 'a', function( event ) { + var $el = $( this ); + + event.preventDefault(); + + // If the media frame already exists, reopen it. + if ( product_gallery_frame ) { + product_gallery_frame.open(); + return; + } + + // Create the media frame. + product_gallery_frame = wp.media.frames.product_gallery = wp.media({ + // Set the title of the modal. + title: $el.data( 'choose' ), + button: { + text: $el.data( 'update' ) + }, + states: [ + new wp.media.controller.Library({ + title: $el.data( 'choose' ), + filterable: 'all', + multiple: true + }) + ] + }); + + // When an image is selected, run a callback. + product_gallery_frame.on( 'select', function() { + var selection = product_gallery_frame.state().get( 'selection' ); + var attachment_ids = $image_gallery_ids.val(); + + selection.map( function( attachment ) { + attachment = attachment.toJSON(); + + if ( attachment.id ) { + attachment_ids = attachment_ids ? attachment_ids + ',' + attachment.id : attachment.id; + var attachment_image = attachment.sizes && attachment.sizes.thumbnail ? attachment.sizes.thumbnail.url : attachment.url; + + $product_images.append( + '
  • ' + ); + } + }); + + $image_gallery_ids.val( attachment_ids ); + }); + + // Finally, open the modal. + product_gallery_frame.open(); + }); + + // Image ordering. + $product_images.sortable({ + items: 'li.image', + cursor: 'move', + scrollSensitivity: 40, + forcePlaceholderSize: true, + forceHelperSize: false, + helper: 'clone', + opacity: 0.65, + placeholder: 'wc-metabox-sortable-placeholder', + start: function( event, ui ) { + ui.item.css( 'background-color', '#f6f6f6' ); + }, + stop: function( event, ui ) { + ui.item.removeAttr( 'style' ); + }, + update: function() { + var attachment_ids = ''; + + $( '#product_images_container' ).find( 'ul li.image' ).css( 'cursor', 'default' ).each( function() { + var attachment_id = $( this ).attr( 'data-attachment_id' ); + attachment_ids = attachment_ids + attachment_id + ','; + }); + + $image_gallery_ids.val( attachment_ids ); + } + }); + + // Remove images. + $( '#product_images_container' ).on( 'click', 'a.delete', function() { + $( this ).closest( 'li.image' ).remove(); + + var attachment_ids = ''; + + $( '#product_images_container' ).find( 'ul li.image' ).css( 'cursor', 'default' ).each( function() { + var attachment_id = $( this ).attr( 'data-attachment_id' ); + attachment_ids = attachment_ids + attachment_id + ','; + }); + + $image_gallery_ids.val( attachment_ids ); + + // Remove any lingering tooltips. + $( '#tiptip_holder' ).removeAttr( 'style' ); + $( '#tiptip_arrow' ).removeAttr( 'style' ); + + return false; + }); +}); diff --git a/assets/js/admin/meta-boxes-product.min.js b/assets/js/admin/meta-boxes-product.min.js new file mode 100644 index 0000000..4d0741c --- /dev/null +++ b/assets/js/admin/meta-boxes-product.min.js @@ -0,0 +1 @@ +jQuery(function(c){function e(){var t=c("select#product-type").val(),e=c("input#_virtual:checked").length,i=c("input#_downloadable:checked").length,o=".hide_if_downloadable, .hide_if_virtual",a=".show_if_downloadable, .show_if_virtual";c.each(woocommerce_admin_meta_boxes.product_types,function(t,e){o=o+", .hide_if_"+e,a=a+", .show_if_"+e}),c(o).show(),c(a).hide(),i&&c(".show_if_downloadable").show(),e&&(c(".show_if_virtual").show(),c(".shipping_options.shipping_tab").hasClass("active")&&c(".general_options.general_tab > a").trigger("click")),c(".show_if_"+t).show(),i&&c(".hide_if_downloadable").hide(),e&&c(".hide_if_virtual").hide(),c(".hide_if_"+t).hide(),c("input#_manage_stock").trigger("change"),c(".woocommerce_options_panel").each(function(){var t=c(this).children(".options_group");0!==t.length&&t.filter(function(){return"none"===c(this).css("display")}).length===t.length&&(t=c(this).prop("id"),c(".product_data_tabs").find('li a[href="#'+t+'"]').parent().hide())})}function t(t){var e=c(t).next().is(".hasDatepicker")?"minDate":"maxDate",i="minDate"==e?c(t).next():c(t).prev(),o=c(t).datepicker("getDate");c(i).datepicker("option",e,o),c(t).trigger("change")}c(function(){c('[id$="-all"] > ul.categorychecklist').each(function(){var t,e=c(this),i=e.find(":checked").first();i.length&&(t=e.find("input").position().top,i=i.position().top,e.closest(".tabs-panel").scrollTop(i-t+5))})}),c("#upsell_product_data").on("keypress",function(t){if(13===t.keyCode)return!1}),c("body").hasClass("wc-wp-version-gte-55")?c(".type_box").appendTo("#woocommerce-product-data .hndle"):c(".type_box").appendTo("#woocommerce-product-data .hndle span"),c(function(){var e=c("#woocommerce-product-data");e.find(".hndle").off("click.postboxes"),e.on("click",".hndle",function(t){c(t.target).filter("input, option, label, select").length||(e.hasClass("closed")?e.removeClass("closed"):e.addClass("closed"))})}),c("#catalog-visibility").find(".edit-catalog-visibility").on("click",function(){return c("#catalog-visibility-select").is(":hidden")&&(c("#catalog-visibility-select").slideDown("fast"),c(this).hide()),!1}),c("#catalog-visibility").find(".save-post-visibility").on("click",function(){c("#catalog-visibility-select").slideUp("fast"),c("#catalog-visibility").find(".edit-catalog-visibility").show();var t=c("input[name=_visibility]:checked").attr("data-label");return c("input[name=_featured]").is(":checked")&&(t=t+", "+woocommerce_admin_meta_boxes.featured_label,c("input[name=_featured]").attr("checked","checked")),c("#catalog-visibility-display").text(t),!1}),c("#catalog-visibility").find(".cancel-post-visibility").on("click",function(){c("#catalog-visibility-select").slideUp("fast"),c("#catalog-visibility").find(".edit-catalog-visibility").show();var t=c("#current_visibility").val(),e=c("#current_featured").val();c("input[name=_visibility]").prop("checked",!1),c("input[name=_visibility][value="+t+"]").attr("checked","checked");t=c("input[name=_visibility]:checked").attr("data-label");return"yes"===e?(t=t+", "+woocommerce_admin_meta_boxes.featured_label,c("input[name=_featured]").attr("checked","checked")):c("input[name=_featured]").prop("checked",!1),c("#catalog-visibility-display").text(t),!1}),c("select#product-type").on("change",function(){var t=c(this).val();"variable"===t?(c("input#_manage_stock").trigger("change"),c("input#_downloadable").prop("checked",!1),c("input#_virtual").prop("checked",!1)):"grouped"!==t&&"external"!==t||(c("input#_downloadable").prop("checked",!1),c("input#_virtual").prop("checked",!1)),e(),c("ul.wc-tabs li:visible").eq(0).find("a").trigger("click"),c(document.body).trigger("woocommerce-product-type-change",t,c(this))}).trigger("change"),c("input#_downloadable, input#_virtual").on("change",function(){e()}),c(".sale_price_dates_fields").each(function(){var t=c(this),e=!1,i=t.closest("div, table");t.find("input").each(function(){""!==c(this).val()&&(e=!0)}),e?(i.find(".sale_schedule").hide(),i.find(".sale_price_dates_fields").show()):(i.find(".sale_schedule").show(),i.find(".sale_price_dates_fields").hide())}),c("#woocommerce-product-data").on("click",".sale_schedule",function(){var t=c(this).closest("div, table");return c(this).hide(),t.find(".cancel_sale_schedule").show(),t.find(".sale_price_dates_fields").show(),!1}),c("#woocommerce-product-data").on("click",".cancel_sale_schedule",function(){var t=c(this).closest("div, table");return c(this).hide(),t.find(".sale_schedule").show(),t.find(".sale_price_dates_fields").hide(),t.find(".sale_price_dates_fields").find("input").val(""),!1}),c("#woocommerce-product-data").on("click",".downloadable_files a.insert",function(){return c(this).closest(".downloadable_files").find("tbody").append(c(this).data("row")),!1}),c("#woocommerce-product-data").on("click",".downloadable_files a.delete",function(){return c(this).closest("tr").remove(),!1}),c("input#_manage_stock").on("change",function(){var t;c(this).is(":checked")?(c("div.stock_fields").show(),c("p.stock_status_field").hide()):(t=c("select#product-type").val(),c("div.stock_fields").hide(),c("p.stock_status_field:not( .hide_if_"+t+" )").show()),c("input.variable_manage_stock").trigger("change")}).trigger("change"),c(".sale_price_dates_fields").each(function(){c(this).find("input").datepicker({defaultDate:"",dateFormat:"yy-mm-dd",numberOfMonths:1,showButtonPanel:!0,onSelect:function(){t(c(this))}}),c(this).find("input").each(function(){t(c(this))})});var i,o,a,n=c(".product_attributes").find(".woocommerce_attribute").get();function r(){c(".product_attributes .woocommerce_attribute").each(function(t,e){c(".attribute_position",e).val(parseInt(c(e).index(".product_attributes .woocommerce_attribute"),10))})}n.sort(function(t,e){t=parseInt(c(t).attr("rel"),10),e=parseInt(c(e).attr("rel"),10);return t'+t.name+""),e.find("select.attribute_values").trigger("change")),c(".product_attributes").unblock()})):c(".product_attributes").unblock(),!1}),c(".save_attributes").on("click",function(){c(".product_attributes").block({message:null,overlayCSS:{background:"#fff",opacity:.6}});var t=c(".product_attributes").find("input, select, textarea"),t={post_id:woocommerce_admin_meta_boxes.post_id,product_type:c("#product-type").val(),data:t.serialize(),action:"woocommerce_save_attributes",security:woocommerce_admin_meta_boxes.save_attributes_nonce};c.post(woocommerce_admin_meta_boxes.ajax_url,t,function(t){t.error?window.alert(t.error):t.data&&(c(".product_attributes").html(t.data.html),c(".product_attributes").unblock(),e(),c("select.attribute_taxonomy").find("option").prop("disabled",!1),c(".product_attributes .woocommerce_attribute").each(function(t,e){"none"!==c(e).css("display")&&c(e).is(".taxonomy")&&c("select.attribute_taxonomy").find('option[value="'+c(e).data("taxonomy")+'"]').prop("disabled",!0)}),t=(t=window.location.toString()).replace("post-new.php?","post.php?post="+woocommerce_admin_meta_boxes.post_id+"&action=edit&"),c("#variable_product_options").load(t+" #variable_product_options_inner",function(){c("#variable_product_options").trigger("reload")}))})}),c(document.body).on("click",".upload_file_button",function(t){var e=c(this);o=e.closest("tr").find("td.file_url input"),t.preventDefault(),i||(t=[new wp.media.controller.Library({library:wp.media.query(),multiple:!0,title:e.data("choose"),priority:20,filterable:"uploaded"})],(i=wp.media.frames.downloadable_file=wp.media({title:e.data("choose"),library:{type:""},button:{text:e.data("update")},multiple:!0,states:t})).on("select",function(){var e="";i.state().get("selection").map(function(t){(t=t.toJSON()).url&&(e=t.url)}),o.val(e).trigger("change")}),i.on("ready",function(){i.uploader.options.uploader.params={type:"downloadable_product"}})),i.open()}),c(".downloadable_files tbody").sortable({items:"tr",cursor:"move",axis:"y",handle:"td.sort",scrollSensitivity:40,forcePlaceholderSize:!0,helper:"clone",opacity:.65});var l=c("#product_image_gallery"),s=c("#product_images_container").find("ul.product_images");c(".add_product_images").on("click","a",function(t){var o=c(this);t.preventDefault(),a||(a=wp.media.frames.product_gallery=wp.media({title:o.data("choose"),button:{text:o.data("update")},states:[new wp.media.controller.Library({title:o.data("choose"),filterable:"all",multiple:!0})]})).on("select",function(){var t=a.state().get("selection"),i=l.val();t.map(function(t){var e;(t=t.toJSON()).id&&(i=i?i+","+t.id:t.id,e=(t.sizes&&t.sizes.thumbnail?t.sizes.thumbnail:t).url,s.append('
  • "))}),l.val(i)}),a.open()}),s.sortable({items:"li.image",cursor:"move",scrollSensitivity:40,forcePlaceholderSize:!0,forceHelperSize:!1,helper:"clone",opacity:.65,placeholder:"wc-metabox-sortable-placeholder",start:function(t,e){e.item.css("background-color","#f6f6f6")},stop:function(t,e){e.item.removeAttr("style")},update:function(){var e="";c("#product_images_container").find("ul li.image").css("cursor","default").each(function(){var t=c(this).attr("data-attachment_id");e=e+t+","}),l.val(e)}}),c("#product_images_container").on("click","a.delete",function(){c(this).closest("li.image").remove();var e="";return c("#product_images_container").find("ul li.image").css("cursor","default").each(function(){var t=c(this).attr("data-attachment_id");e=e+t+","}),l.val(e),c("#tiptip_holder").removeAttr("style"),c("#tiptip_arrow").removeAttr("style"),!1})}); \ No newline at end of file diff --git a/assets/js/admin/meta-boxes.js b/assets/js/admin/meta-boxes.js new file mode 100644 index 0000000..a815b38 --- /dev/null +++ b/assets/js/admin/meta-boxes.js @@ -0,0 +1,80 @@ +jQuery( function ( $ ) { + + // Run tipTip + function runTipTip() { + // Remove any lingering tooltips + $( '#tiptip_holder' ).removeAttr( 'style' ); + $( '#tiptip_arrow' ).removeAttr( 'style' ); + $( '.tips' ).tipTip({ + 'attribute': 'data-tip', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 200, + 'keepAlive': true + }); + } + + runTipTip(); + + $( '.wc-metaboxes-wrapper' ).on( 'click', '.wc-metabox > h3', function() { + var metabox = $( this ).parent( '.wc-metabox' ); + + if ( metabox.hasClass( 'closed' ) ) { + metabox.removeClass( 'closed' ); + } else { + metabox.addClass( 'closed' ); + } + + if ( metabox.hasClass( 'open' ) ) { + metabox.removeClass( 'open' ); + } else { + metabox.addClass( 'open' ); + } + }); + + // Tabbed Panels + $( document.body ).on( 'wc-init-tabbed-panels', function() { + $( 'ul.wc-tabs' ).show(); + $( 'ul.wc-tabs a' ).on( 'click', function( e ) { + e.preventDefault(); + var panel_wrap = $( this ).closest( 'div.panel-wrap' ); + $( 'ul.wc-tabs li', panel_wrap ).removeClass( 'active' ); + $( this ).parent().addClass( 'active' ); + $( 'div.panel', panel_wrap ).hide(); + $( $( this ).attr( 'href' ) ).show(); + }); + $( 'div.panel-wrap' ).each( function() { + $( this ).find( 'ul.wc-tabs li' ).eq( 0 ).find( 'a' ).trigger( 'click' ); + }); + }).trigger( 'wc-init-tabbed-panels' ); + + // Date Picker + $( document.body ).on( 'wc-init-datepickers', function() { + $( '.date-picker-field, .date-picker' ).datepicker({ + dateFormat: 'yy-mm-dd', + numberOfMonths: 1, + showButtonPanel: true + }); + }).trigger( 'wc-init-datepickers' ); + + // Meta-Boxes - Open/close + $( '.wc-metaboxes-wrapper' ).on( 'click', '.wc-metabox h3', function( event ) { + // If the user clicks on some form input inside the h3, like a select list (for variations), the box should not be toggled + if ( $( event.target ).filter( ':input, option, .sort' ).length ) { + return; + } + + $( this ).next( '.wc-metabox-content' ).stop().slideToggle(); + }) + .on( 'click', '.expand_all', function() { + $( this ).closest( '.wc-metaboxes-wrapper' ).find( '.wc-metabox > .wc-metabox-content' ).show(); + return false; + }) + .on( 'click', '.close_all', function() { + $( this ).closest( '.wc-metaboxes-wrapper' ).find( '.wc-metabox > .wc-metabox-content' ).hide(); + return false; + }); + $( '.wc-metabox.closed' ).each( function() { + $( this ).find( '.wc-metabox-content' ).hide(); + }); +}); diff --git a/assets/js/admin/meta-boxes.min.js b/assets/js/admin/meta-boxes.min.js new file mode 100644 index 0000000..c8e59ec --- /dev/null +++ b/assets/js/admin/meta-boxes.min.js @@ -0,0 +1 @@ +jQuery(function(t){t("#tiptip_holder").removeAttr("style"),t("#tiptip_arrow").removeAttr("style"),t(".tips").tipTip({attribute:"data-tip",fadeIn:50,fadeOut:50,delay:200,keepAlive:!0}),t(".wc-metaboxes-wrapper").on("click",".wc-metabox > h3",function(){var e=t(this).parent(".wc-metabox");e.hasClass("closed")?e.removeClass("closed"):e.addClass("closed"),e.hasClass("open")?e.removeClass("open"):e.addClass("open")}),t(document.body).on("wc-init-tabbed-panels",function(){t("ul.wc-tabs").show(),t("ul.wc-tabs a").on("click",function(e){e.preventDefault();e=t(this).closest("div.panel-wrap");t("ul.wc-tabs li",e).removeClass("active"),t(this).parent().addClass("active"),t("div.panel",e).hide(),t(t(this).attr("href")).show()}),t("div.panel-wrap").each(function(){t(this).find("ul.wc-tabs li").eq(0).find("a").trigger("click")})}).trigger("wc-init-tabbed-panels"),t(document.body).on("wc-init-datepickers",function(){t(".date-picker-field, .date-picker").datepicker({dateFormat:"yy-mm-dd",numberOfMonths:1,showButtonPanel:!0})}).trigger("wc-init-datepickers"),t(".wc-metaboxes-wrapper").on("click",".wc-metabox h3",function(e){t(e.target).filter(":input, option, .sort").length||t(this).next(".wc-metabox-content").stop().slideToggle()}).on("click",".expand_all",function(){return t(this).closest(".wc-metaboxes-wrapper").find(".wc-metabox > .wc-metabox-content").show(),!1}).on("click",".close_all",function(){return t(this).closest(".wc-metaboxes-wrapper").find(".wc-metabox > .wc-metabox-content").hide(),!1}),t(".wc-metabox.closed").each(function(){t(this).find(".wc-metabox-content").hide()})}); \ No newline at end of file diff --git a/assets/js/admin/network-orders.js b/assets/js/admin/network-orders.js new file mode 100644 index 0000000..aa3a2ac --- /dev/null +++ b/assets/js/admin/network-orders.js @@ -0,0 +1,90 @@ +/*global woocommerce_network_orders */ +(function( $, _, undefined ) { + + if ( 'undefined' === typeof woocommerce_network_orders ) { + return; + } + + var orders = [], + promises = [], // Track completion (pass or fail) of ajax requests. + deferred = [], // Tracks the ajax deferreds. + $tbody = $( document.getElementById( 'network-orders-tbody' ) ), + template = _.template( $( document.getElementById( 'network-orders-row-template') ).text() ), + $loadingIndicator = $( document.getElementById( 'woocommerce-network-order-table-loading' ) ), + $orderTable = $( document.getElementById( 'woocommerce-network-order-table' ) ), + $noneFound = $( document.getElementById( 'woocommerce-network-orders-no-orders' ) ); + + // No sites, so bail. + if ( ! woocommerce_network_orders.sites.length ) { + $loadingIndicator.removeClass( 'is-active' ); + $orderTable.removeClass( 'is-active' ); + $noneFound.addClass( 'is-active' ); + return; + } + + $.each( woocommerce_network_orders.sites, function( index, value ) { + promises[ index ] = $.Deferred(); + deferred.push( $.ajax( { + url : woocommerce_network_orders.order_endpoint, + data: { + _wpnonce: woocommerce_network_orders.nonce, + network_orders: true, + blog_id: value + }, + type: 'GET' + } ).success(function( response ) { + var orderindex; + + for ( orderindex in response ) { + orders.push( response[ orderindex ] ); + } + + promises[ index ].resolve(); + }).fail(function (){ + promises[ index ].resolve(); + }) ); + } ); + + if ( promises.length > 0 ) { + $.when.apply( $, promises ).done( function() { + var orderindex, + currentOrder; + + // Sort orders, newest first + orders.sort(function( a, b ) { + var adate, bdate; + + adate = Date.parse( a.date_created_gmt ); + bdate = Date.parse( b.date_created_gmt ); + + if ( adate === bdate ) { + return 0; + } + + if ( adate < bdate ) { + return 1; + } else { + return -1; + } + }); + + if ( orders.length > 0 ) { + for ( orderindex in orders ) { + currentOrder = orders[ orderindex ]; + + $tbody.append( template( currentOrder ) ); + } + + $noneFound.removeClass( 'is-active' ); + $loadingIndicator.removeClass( 'is-active' ); + $orderTable.addClass( 'is-active' ); + } else { + $noneFound.addClass( 'is-active' ); + $loadingIndicator.removeClass( 'is-active' ); + $orderTable.removeClass( 'is-active' ); + } + + } ); + } + +})( jQuery, _ ); diff --git a/assets/js/admin/network-orders.min.js b/assets/js/admin/network-orders.min.js new file mode 100644 index 0000000..ee4168a --- /dev/null +++ b/assets/js/admin/network-orders.min.js @@ -0,0 +1 @@ +!function(o,e){if("undefined"!=typeof woocommerce_network_orders){var t=[],n=[],s=[],r=o(document.getElementById("network-orders-tbody")),a=e.template(o(document.getElementById("network-orders-row-template")).text()),c=o(document.getElementById("woocommerce-network-order-table-loading")),d=o(document.getElementById("woocommerce-network-order-table")),i=o(document.getElementById("woocommerce-network-orders-no-orders"));if(!woocommerce_network_orders.sites.length)return c.removeClass("is-active"),d.removeClass("is-active"),i.addClass("is-active");o.each(woocommerce_network_orders.sites,function(r,e){n[r]=o.Deferred(),s.push(o.ajax({url:woocommerce_network_orders.order_endpoint,data:{_wpnonce:woocommerce_network_orders.nonce,network_orders:!0,blog_id:e},type:"GET"}).success(function(e){for(var o in e)t.push(e[o]);n[r].resolve()}).fail(function(){n[r].resolve()}))}),0' ); + + // Go do the sorting stuff via ajax + $.post( + ajaxurl, + { action: 'woocommerce_product_ordering', id: postid, previd: prevpostid, nextid: nextpostid }, + function( response ) { + $.each( response, function( key, value ) { + $( '#inline_' + key + ' .menu_order' ).html( value ); + }); + ui.item.find( '.check-column input' ).show().siblings( 'img' ).remove(); + $( 'table.widefat tbody th, table.widefat tbody td' ).css( 'cursor', 'move' ); + $( 'table.widefat tbody' ).sortable( 'enable' ); + } + ); + + // fix cell colors + $( 'table.widefat tbody tr' ).each( function() { + var i = $( 'table.widefat tbody tr' ).index( this ); + if ( i%2 === 0 ) { + $( this ).addClass( 'alternate' ); + } else { + $( this ).removeClass( 'alternate' ); + } + }); + } + }); +}); diff --git a/assets/js/admin/product-ordering.min.js b/assets/js/admin/product-ordering.min.js new file mode 100644 index 0000000..54db47f --- /dev/null +++ b/assets/js/admin/product-ordering.min.js @@ -0,0 +1 @@ +jQuery(function(d){d("table.widefat tbody th, table.widefat tbody td").css("cursor","move"),d("table.widefat tbody").sortable({items:"tr:not(.inline-edit-row)",cursor:"move",axis:"y",containment:"table.widefat",scrollSensitivity:40,helper:function(t,e){return e.each(function(){d(this).width(d(this).width())}),e},start:function(t,e){e.item.css("background-color","#ffffff"),e.item.children("td, th").css("border-bottom-width","0"),e.item.css("outline","1px solid #dfdfdf")},stop:function(t,e){e.item.removeAttr("style"),e.item.children("td,th").css("border-bottom-width","1px")},update:function(t,e){d("table.widefat tbody th, table.widefat tbody td").css("cursor","default"),d("table.widefat tbody").sortable("disable");var i=e.item.find(".check-column input").val(),o=e.item.prev().find(".check-column input").val(),n=e.item.next().find(".check-column input").val();e.item.find(".check-column input").hide().after('processing'),d.post(ajaxurl,{action:"woocommerce_product_ordering",id:i,previd:o,nextid:n},function(t){d.each(t,function(t,e){d("#inline_"+t+" .menu_order").html(e)}),e.item.find(".check-column input").show().siblings("img").remove(),d("table.widefat tbody th, table.widefat tbody td").css("cursor","move"),d("table.widefat tbody").sortable("enable")}),d("table.widefat tbody tr").each(function(){d("table.widefat tbody tr").index(this)%2==0?d(this).addClass("alternate"):d(this).removeClass("alternate")})}})}); \ No newline at end of file diff --git a/assets/js/admin/quick-edit.js b/assets/js/admin/quick-edit.js new file mode 100644 index 0000000..8e2222f --- /dev/null +++ b/assets/js/admin/quick-edit.js @@ -0,0 +1,167 @@ +/*global inlineEditPost, woocommerce_admin, woocommerce_quick_edit */ +jQuery( + function( $ ) { + $( '#the-list' ).on( + 'click', + '.editinline', + function() { + + inlineEditPost.revert(); + + var post_id = $( this ).closest( 'tr' ).attr( 'id' ); + + post_id = post_id.replace( 'post-', '' ); + + var $wc_inline_data = $( '#woocommerce_inline_' + post_id ); + + var sku = $wc_inline_data.find( '.sku' ).text(), + regular_price = $wc_inline_data.find( '.regular_price' ).text(), + sale_price = $wc_inline_data.find( '.sale_price ' ).text(), + weight = $wc_inline_data.find( '.weight' ).text(), + length = $wc_inline_data.find( '.length' ).text(), + width = $wc_inline_data.find( '.width' ).text(), + height = $wc_inline_data.find( '.height' ).text(), + shipping_class = $wc_inline_data.find( '.shipping_class' ).text(), + visibility = $wc_inline_data.find( '.visibility' ).text(), + stock_status = $wc_inline_data.find( '.stock_status' ).text(), + stock = $wc_inline_data.find( '.stock' ).text(), + featured = $wc_inline_data.find( '.featured' ).text(), + manage_stock = $wc_inline_data.find( '.manage_stock' ).text(), + menu_order = $wc_inline_data.find( '.menu_order' ).text(), + tax_status = $wc_inline_data.find( '.tax_status' ).text(), + tax_class = $wc_inline_data.find( '.tax_class' ).text(), + backorders = $wc_inline_data.find( '.backorders' ).text(), + product_type = $wc_inline_data.find( '.product_type' ).text(); + + var formatted_regular_price = regular_price.replace( '.', woocommerce_admin.mon_decimal_point ), + formatted_sale_price = sale_price.replace( '.', woocommerce_admin.mon_decimal_point ); + + $( 'input[name="_sku"]', '.inline-edit-row' ).val( sku ); + $( 'input[name="_regular_price"]', '.inline-edit-row' ).val( formatted_regular_price ); + $( 'input[name="_sale_price"]', '.inline-edit-row' ).val( formatted_sale_price ); + $( 'input[name="_weight"]', '.inline-edit-row' ).val( weight ); + $( 'input[name="_length"]', '.inline-edit-row' ).val( length ); + $( 'input[name="_width"]', '.inline-edit-row' ).val( width ); + $( 'input[name="_height"]', '.inline-edit-row' ).val( height ); + + $( 'select[name="_shipping_class"] option:selected', '.inline-edit-row' ).attr( 'selected', false ).trigger( 'change' ); + $( 'select[name="_shipping_class"] option[value="' + shipping_class + '"]' ).attr( 'selected', 'selected' ) + .trigger( 'change' ); + + $( 'input[name="_stock"]', '.inline-edit-row' ).val( stock ); + $( 'input[name="menu_order"]', '.inline-edit-row' ).val( menu_order ); + + $( + 'select[name="_tax_status"] option, ' + + 'select[name="_tax_class"] option, ' + + 'select[name="_visibility"] option, ' + + 'select[name="_stock_status"] option, ' + + 'select[name="_backorders"] option' + ).prop( 'selected', false ).removeAttr( 'selected' ); + + var is_variable_product = 'variable' === product_type; + $( 'select[name="_stock_status"] ~ .wc-quick-edit-warning', '.inline-edit-row' ).toggle( is_variable_product ); + $( 'select[name="_stock_status"] option[value="' + (is_variable_product ? '' : stock_status) + '"]', '.inline-edit-row' ) + .attr( 'selected', 'selected' ); + + $( 'select[name="_tax_status"] option[value="' + tax_status + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); + $( 'select[name="_tax_class"] option[value="' + tax_class + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); + $( 'select[name="_visibility"] option[value="' + visibility + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); + $( 'select[name="_backorders"] option[value="' + backorders + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' ); + + if ( 'yes' === featured ) { + $( 'input[name="_featured"]', '.inline-edit-row' ).prop( 'checked', true ); + } else { + $( 'input[name="_featured"]', '.inline-edit-row' ).prop( 'checked', false ); + } + + // Conditional display. + var product_is_virtual = $wc_inline_data.find( '.product_is_virtual' ).text(); + + var product_supports_stock_status = 'external' !== product_type; + var product_supports_stock_fields = 'external' !== product_type && 'grouped' !== product_type; + + $( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).show(); + + if ( product_supports_stock_fields ) { + if ( 'yes' === manage_stock ) { + $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' ); + $( '.stock_status_field' ).hide(); + $( '.manage_stock_field input' ).prop( 'checked', true ); + } else { + $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide(); + $( '.stock_status_field' ).show().removeAttr( 'style' ); + $( '.manage_stock_field input' ).prop( 'checked', false ); + } + } else if ( product_supports_stock_status ) { + $( '.stock_fields, .manage_stock_field, .backorder_field' ).hide(); + } else { + $( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).hide(); + } + + if ( 'simple' === product_type || 'external' === product_type ) { + $( '.price_fields', '.inline-edit-row' ).show().removeAttr( 'style' ); + } else { + $( '.price_fields', '.inline-edit-row' ).hide(); + } + + if ( 'yes' === product_is_virtual ) { + $( '.dimension_fields', '.inline-edit-row' ).hide(); + } else { + $( '.dimension_fields', '.inline-edit-row' ).show().removeAttr( 'style' ); + } + + // Rename core strings. + $( 'input[name="comment_status"]' ).parent().find( '.checkbox-title' ).text( woocommerce_quick_edit.strings.allow_reviews ); + } + ); + + $( '#the-list' ).on( + 'change', + '.inline-edit-row input[name="_manage_stock"]', + function() { + + if ( $( this ).is( ':checked' ) ) { + $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' ); + $( '.stock_status_field' ).hide(); + } else { + $( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide(); + $( '.stock_status_field' ).show().removeAttr( 'style' ); + } + + } + ); + + $( '#wpbody' ).on( + 'click', + '#doaction, #doaction2', + function() { + $( 'input.text', '.inline-edit-row' ).val( '' ); + $( '#woocommerce-fields' ).find( 'select' ).prop( 'selectedIndex', 0 ); + $( '#woocommerce-fields-bulk' ).find( '.inline-edit-group .change-input' ).hide(); + } + ); + + $( '#wpbody' ).on( + 'change', + '#woocommerce-fields-bulk .inline-edit-group .change_to', + function() { + + if ( 0 < $( this ).val() ) { + $( this ).closest( 'div' ).find( '.change-input' ).show(); + } else { + $( this ).closest( 'div' ).find( '.change-input' ).hide(); + } + + } + ); + + $( '#wpbody' ).on( + 'click', + '.trash-product', + function() { + return window.confirm( woocommerce_admin.i18n_delete_product_notice ); + } + ); + } +); diff --git a/assets/js/admin/quick-edit.min.js b/assets/js/admin/quick-edit.min.js new file mode 100644 index 0000000..77000b2 --- /dev/null +++ b/assets/js/admin/quick-edit.min.js @@ -0,0 +1 @@ +jQuery(function(g){g("#the-list").on("click",".editinline",function(){inlineEditPost.revert();var e=(e=g(this).closest("tr").attr("id")).replace("post-",""),t=g("#woocommerce_inline_"+e),i=t.find(".sku").text(),n=t.find(".regular_price").text(),o=t.find(".sale_price ").text(),d=t.find(".weight").text(),s=t.find(".length").text(),l=t.find(".width").text(),c=t.find(".height").text(),a=t.find(".shipping_class").text(),r=t.find(".visibility").text(),_=t.find(".stock_status").text(),p=t.find(".stock").text(),m=t.find(".featured").text(),u=t.find(".manage_stock").text(),f=t.find(".menu_order").text(),w=t.find(".tax_status").text(),h=t.find(".tax_class").text(),k=t.find(".backorders").text(),e=t.find(".product_type").text(),n=n.replace(".",woocommerce_admin.mon_decimal_point),o=o.replace(".",woocommerce_admin.mon_decimal_point);g('input[name="_sku"]',".inline-edit-row").val(i),g('input[name="_regular_price"]',".inline-edit-row").val(n),g('input[name="_sale_price"]',".inline-edit-row").val(o),g('input[name="_weight"]',".inline-edit-row").val(d),g('input[name="_length"]',".inline-edit-row").val(s),g('input[name="_width"]',".inline-edit-row").val(l),g('input[name="_height"]',".inline-edit-row").val(c),g('select[name="_shipping_class"] option:selected',".inline-edit-row").attr("selected",!1).trigger("change"),g('select[name="_shipping_class"] option[value="'+a+'"]').attr("selected","selected").trigger("change"),g('input[name="_stock"]',".inline-edit-row").val(p),g('input[name="menu_order"]',".inline-edit-row").val(f),g('select[name="_tax_status"] option, select[name="_tax_class"] option, select[name="_visibility"] option, select[name="_stock_status"] option, select[name="_backorders"] option').prop("selected",!1).removeAttr("selected");f="variable"===e;g('select[name="_stock_status"] ~ .wc-quick-edit-warning',".inline-edit-row").toggle(f),g('select[name="_stock_status"] option[value="'+(f?"":_)+'"]',".inline-edit-row").attr("selected","selected"),g('select[name="_tax_status"] option[value="'+w+'"]',".inline-edit-row").attr("selected","selected"),g('select[name="_tax_class"] option[value="'+h+'"]',".inline-edit-row").attr("selected","selected"),g('select[name="_visibility"] option[value="'+r+'"]',".inline-edit-row").attr("selected","selected"),g('select[name="_backorders"] option[value="'+k+'"]',".inline-edit-row").attr("selected","selected"),"yes"===m?g('input[name="_featured"]',".inline-edit-row").prop("checked",!0):g('input[name="_featured"]',".inline-edit-row").prop("checked",!1);k=t.find(".product_is_virtual").text(),m="external"!==e,t="external"!==e&&"grouped"!==e;g(".stock_fields, .manage_stock_field, .stock_status_field, .backorder_field").show(),t?"yes"===u?(g(".stock_qty_field, .backorder_field",".inline-edit-row").show().removeAttr("style"),g(".stock_status_field").hide(),g(".manage_stock_field input").prop("checked",!0)):(g(".stock_qty_field, .backorder_field",".inline-edit-row").hide(),g(".stock_status_field").show().removeAttr("style"),g(".manage_stock_field input").prop("checked",!1)):g(m?".stock_fields, .manage_stock_field, .backorder_field":".stock_fields, .manage_stock_field, .stock_status_field, .backorder_field").hide(),"simple"===e||"external"===e?g(".price_fields",".inline-edit-row").show().removeAttr("style"):g(".price_fields",".inline-edit-row").hide(),"yes"===k?g(".dimension_fields",".inline-edit-row").hide():g(".dimension_fields",".inline-edit-row").show().removeAttr("style"),g('input[name="comment_status"]').parent().find(".checkbox-title").text(woocommerce_quick_edit.strings.allow_reviews)}),g("#the-list").on("change",'.inline-edit-row input[name="_manage_stock"]',function(){g(this).is(":checked")?(g(".stock_qty_field, .backorder_field",".inline-edit-row").show().removeAttr("style"),g(".stock_status_field").hide()):(g(".stock_qty_field, .backorder_field",".inline-edit-row").hide(),g(".stock_status_field").show().removeAttr("style"))}),g("#wpbody").on("click","#doaction, #doaction2",function(){g("input.text",".inline-edit-row").val(""),g("#woocommerce-fields").find("select").prop("selectedIndex",0),g("#woocommerce-fields-bulk").find(".inline-edit-group .change-input").hide()}),g("#wpbody").on("change","#woocommerce-fields-bulk .inline-edit-group .change_to",function(){0' + contents + '' ).css( { + top: y - 16, + left: x + 20 + }).appendTo( 'body' ).fadeIn( 200 ); + } + + var prev_data_index = null; + var prev_series_index = null; + + $( '.chart-placeholder' ).on( 'plothover', function ( event, pos, item ) { + if ( item ) { + if ( prev_data_index !== item.dataIndex || prev_series_index !== item.seriesIndex ) { + prev_data_index = item.dataIndex; + prev_series_index = item.seriesIndex; + + $( '.chart-tooltip' ).remove(); + + if ( item.series.points.show || item.series.enable_tooltip ) { + + var y = item.series.data[item.dataIndex][1], + tooltip_content = ''; + + if ( item.series.prepend_label ) { + tooltip_content = tooltip_content + item.series.label + ': '; + } + + if ( item.series.prepend_tooltip ) { + tooltip_content = tooltip_content + item.series.prepend_tooltip; + } + + tooltip_content = tooltip_content + y; + + if ( item.series.append_tooltip ) { + tooltip_content = tooltip_content + item.series.append_tooltip; + } + + if ( item.series.pie.show ) { + showTooltip( pos.pageX, pos.pageY, tooltip_content ); + } else { + showTooltip( item.pageX, item.pageY, tooltip_content ); + } + } + } + } else { + $( '.chart-tooltip' ).remove(); + prev_data_index = null; + } + }); + + $( '.wc_sparkline.bars' ).each( function() { + var chart_data = $( this ).data( 'sparkline' ); + + var options = { + grid: { + show: false + } + }; + + // main series + var series = [{ + data: chart_data, + color: $( this ).data( 'color' ), + bars: { + fillColor: $( this ).data( 'color' ), + fill: true, + show: true, + lineWidth: 1, + barWidth: $( this ).data( 'barwidth' ), + align: 'center' + }, + shadowSize: 0 + }]; + + // draw the sparkline + $.plot( $( this ), series, options ); + }); + + $( '.wc_sparkline.lines' ).each( function() { + var chart_data = $( this ).data( 'sparkline' ); + + var options = { + grid: { + show: false + } + }; + + // main series + var series = [{ + data: chart_data, + color: $( this ).data( 'color' ), + lines: { + fill: false, + show: true, + lineWidth: 1, + align: 'center' + }, + shadowSize: 0 + }]; + + // draw the sparkline + $.plot( $( this ), series, options ); + }); + + var dates = $( '.range_datepicker' ).datepicker({ + changeMonth: true, + changeYear: true, + defaultDate: '', + dateFormat: 'yy-mm-dd', + numberOfMonths: 1, + minDate: '-20Y', + maxDate: '+1D', + showButtonPanel: true, + showOn: 'focus', + buttonImageOnly: true, + onSelect: function() { + var option = $( this ).is( '.from' ) ? 'minDate' : 'maxDate', + date = $( this ).datepicker( 'getDate' ); + + dates.not( this ).datepicker( 'option', option, date ); + } + }); + + var a = document.createElement( 'a' ); + + if ( typeof a.download === 'undefined' ) { + $( '.export_csv' ).hide(); + } + + // Export + $( '.export_csv' ).on( 'click', function() { + var exclude_series = $( this ).data( 'exclude_series' ) || ''; + exclude_series = exclude_series.toString(); + exclude_series = exclude_series.split( ',' ); + var xaxes_label = $( this ).data( 'xaxes' ); + var groupby = $( this ) .data( 'groupby' ); + var index_type = $( this ).data( 'index_type' ); + var export_format = $( this ).data( 'export' ); + var csv_data = ''; + var s, series_data, d; + + if ( 'table' === export_format ) { + + $( this ).offsetParent().find( 'thead tr,tbody tr' ).each( function() { + $( this ).find( 'th, td' ).each( function() { + var value = $( this ).text(); + value = value.replace( '[?]', '' ).replace( '#', '' ); + csv_data += '"' + value + '"' + ','; + }); + csv_data = csv_data.substring( 0, csv_data.length - 1 ); + csv_data += '\n'; + }); + + $( this ).offsetParent().find( 'tfoot tr' ).each( function() { + $( this ).find( 'th, td' ).each( function() { + var value = $( this ).text(); + value = value.replace( '[?]', '' ).replace( '#', '' ); + csv_data += '"' + value + '"' + ','; + if ( $( this ).attr( 'colspan' ) > 0 ) { + for ( i = 1; i < $(this).attr('colspan'); i++ ) { + csv_data += '"",'; + } + } + }); + csv_data = csv_data.substring( 0, csv_data.length - 1 ); + csv_data += '\n'; + }); + + } else { + + if ( ! window.main_chart ) { + return false; + } + + var the_series = window.main_chart.getData(); + var series = []; + csv_data += '"' + xaxes_label + '",'; + + $.each( the_series, function( index, value ) { + if ( ! exclude_series || $.inArray( index.toString(), exclude_series ) === -1 ) { + series.push( value ); + } + }); + + // CSV Headers + for ( s = 0; s < series.length; ++s ) { + csv_data += '"' + series[s].label + '",'; + } + + csv_data = csv_data.substring( 0, csv_data.length - 1 ); + csv_data += '\n'; + + // Get x axis values + var xaxis = {}; + + for ( s = 0; s < series.length; ++s ) { + series_data = series[s].data; + for ( d = 0; d < series_data.length; ++d ) { + xaxis[series_data[d][0]] = []; + // Zero values to start + for ( var i = 0; i < series.length; ++i ) { + xaxis[series_data[d][0]].push(0); + } + } + } + + // Add chart data + for ( s = 0; s < series.length; ++s ) { + series_data = series[s].data; + for ( d = 0; d < series_data.length; ++d ) { + xaxis[series_data[d][0]][s] = series_data[d][1]; + } + } + + // Loop data and output to csv string + $.each( xaxis, function( index, value ) { + var date = new Date( parseInt( index, 10 ) ); + + if ( 'none' === index_type ) { + csv_data += '"' + index + '",'; + } else { + if ( groupby === 'day' ) { + csv_data += '"' + + date.getUTCFullYear() + + '-' + + parseInt( date.getUTCMonth() + 1, 10 ) + + '-' + + date.getUTCDate() + + '",'; + } else { + csv_data += '"' + date.getUTCFullYear() + '-' + parseInt( date.getUTCMonth() + 1, 10 ) + '",'; + } + } + + for ( var d = 0; d < value.length; ++d ) { + var val = value[d]; + + if ( Math.round( val ) !== val ) { + val = parseFloat( val ); + val = val.toFixed( 2 ); + } + + csv_data += '"' + val + '",'; + } + csv_data = csv_data.substring( 0, csv_data.length - 1 ); + csv_data += '\n'; + } ); + } + + csv_data = 'data:text/csv;charset=utf-8,\uFEFF' + encodeURIComponent( csv_data ); + // Set data as href and return + $( this ).attr( 'href', csv_data ); + return true; + }); +}); diff --git a/assets/js/admin/reports.min.js b/assets/js/admin/reports.min.js new file mode 100644 index 0000000..8cf6d17 --- /dev/null +++ b/assets/js/admin/reports.min.js @@ -0,0 +1 @@ +jQuery(function(p){function r(t,e,a){p('
    '+a+"
    ").css({top:e-16,left:t+20}).appendTo("body").fadeIn(200)}var o=null,s=null;p(".chart-placeholder").on("plothover",function(t,e,a){var n,i;a?o===a.dataIndex&&s===a.seriesIndex||(o=a.dataIndex,s=a.seriesIndex,p(".chart-tooltip").remove(),(a.series.points.show||a.series.enable_tooltip)&&(n=a.series.data[a.dataIndex][1],i="",a.series.prepend_label&&(i=i+a.series.label+": "),a.series.prepend_tooltip&&(i+=a.series.prepend_tooltip),i+=n,a.series.append_tooltip&&(i+=a.series.append_tooltip),a.series.pie.show?r(e.pageX,e.pageY,i):r(a.pageX,a.pageY,i))):(p(".chart-tooltip").remove(),o=null)}),p(".wc_sparkline.bars").each(function(){var t=[{data:p(this).data("sparkline"),color:p(this).data("color"),bars:{fillColor:p(this).data("color"),fill:!0,show:!0,lineWidth:1,barWidth:p(this).data("barwidth"),align:"center"},shadowSize:0}];p.plot(p(this),t,{grid:{show:!1}})}),p(".wc_sparkline.lines").each(function(){var t=[{data:p(this).data("sparkline"),color:p(this).data("color"),lines:{fill:!1,show:!0,lineWidth:1,align:"center"},shadowSize:0}];p.plot(p(this),t,{grid:{show:!1}})});var a=p(".range_datepicker").datepicker({changeMonth:!0,changeYear:!0,defaultDate:"",dateFormat:"yy-mm-dd",numberOfMonths:1,minDate:"-20Y",maxDate:"+1D",showButtonPanel:!0,showOn:"focus",buttonImageOnly:!0,onSelect:function(){var t=p(this).is(".from")?"minDate":"maxDate",e=p(this).datepicker("getDate");a.not(this).datepicker("option",t,e)}});"undefined"==typeof document.createElement("a").download&&p(".export_csv").hide(),p(".export_csv").on("click",function(){var a=p(this).data("exclude_series")||"";a=(a=a.toString()).split(",");var t,e,n=p(this).data("xaxes"),r=p(this).data("groupby"),o=p(this).data("index_type"),i=p(this).data("export"),s="";if("table"===i)p(this).offsetParent().find("thead tr,tbody tr").each(function(){p(this).find("th, td").each(function(){var t=(t=p(this).text()).replace("[?]","").replace("#","");s+='"'+t+'",'}),s=s.substring(0,s.length-1),s+="\n"}),p(this).offsetParent().find("tfoot tr").each(function(){p(this).find("th, td").each(function(){var t=(t=p(this).text()).replace("[?]","").replace("#","");if(s+='"'+t+'",',0 0 ? '&' : '?' ) + 'action=woocommerce_tax_rates_save_changes', + data: { + current_class: data.current_class, + wc_tax_nonce: data.wc_tax_nonce, + changes: self.changes + }, + success: function( response, textStatus ) { + if ( 'success' === textStatus && response.success ) { + WCTaxTableModelInstance.set( 'rates', response.data.rates ); + WCTaxTableModelInstance.trigger( 'change:rates' ); + + WCTaxTableModelInstance.changes = {}; + WCTaxTableModelInstance.trigger( 'saved:rates' ); + + // Reload view. + WCTaxTableInstance.render(); + } + + self.unblock(); + } + }); + } + } ), + WCTaxTableViewConstructor = Backbone.View.extend({ + rowTemplate: rowTemplate, + per_page: data.limit, + page: data.page, + initialize: function() { + var qty_pages = Math.ceil( _.toArray( this.model.get( 'rates' ) ).length / this.per_page ); + + this.qty_pages = 0 === qty_pages ? 1 : qty_pages; + this.page = this.sanitizePage( data.page ); + + this.listenTo( this.model, 'change:rates', this.setUnloadConfirmation ); + this.listenTo( this.model, 'saved:rates', this.clearUnloadConfirmation ); + $tbody.on( 'change autocompletechange', ':input', { view: this }, this.updateModelOnChange ); + $search_field.on( 'keyup search', { view: this }, this.onSearchField ); + $pagination.on( 'click', 'a', { view: this }, this.onPageChange ); + $pagination.on( 'change', 'input', { view: this }, this.onPageChange ); + $( window ).on( 'beforeunload', { view: this }, this.unloadConfirmation ); + $submit.on( 'click', { view: this }, this.onSubmit ); + $save_button.prop( 'disabled', true ); + + // Can bind these directly to the buttons, as they won't get overwritten. + $table.find( '.insert' ).on( 'click', { view: this }, this.onAddNewRow ); + $table.find( '.remove_tax_rates' ).on( 'click', { view: this }, this.onDeleteRow ); + $table.find( '.export' ).on( 'click', { view: this }, this.onExport ); + }, + render: function() { + var rates = this.model.getFilteredRates(), + qty_rates = _.size( rates ), + qty_pages = Math.ceil( qty_rates / this.per_page ), + first_index = 0 === qty_rates ? 0 : this.per_page * ( this.page - 1 ), + last_index = this.per_page * this.page, + paged_rates = _.toArray( rates ).slice( first_index, last_index ), + view = this; + + // Blank out the contents. + this.$el.empty(); + + if ( paged_rates.length ) { + // Populate $tbody with the current page of results. + $.each( paged_rates, function( id, rowData ) { + view.$el.append( view.rowTemplate( rowData ) ); + } ); + } else { + view.$el.append( rowTemplateEmpty() ); + } + + // Initialize autocomplete for countries. + this.$el.find( 'td.country input' ).autocomplete({ + source: data.countries, + minLength: 2 + }); + + // Initialize autocomplete for states. + this.$el.find( 'td.state input' ).autocomplete({ + source: data.states, + minLength: 3 + }); + + // Postcode and city don't have `name` values by default. + // They're only created if the contents changes, to save on database queries (I think) + this.$el.find( 'td.postcode input, td.city input' ).on( 'change', function() { + $( this ).attr( 'name', $( this ).data( 'name' ) ); + }); + + if ( qty_pages > 1 ) { + // We've now displayed our initial page, time to render the pagination box. + $pagination.html( paginationTemplate( { + qty_rates: qty_rates, + current_page: this.page, + qty_pages: qty_pages + } ) ); + } else { + $pagination.empty(); + view.page = 1; + } + }, + updateUrl: function() { + if ( ! window.history.replaceState ) { + return; + } + + var url = data.base_url, + search = $search_field.val(); + + if ( 1 < this.page ) { + url += '&p=' + encodeURIComponent( this.page ); + } + + if ( search.length ) { + url += '&s=' + encodeURIComponent( search ); + } + + window.history.replaceState( {}, '', url ); + }, + onSubmit: function( event ) { + event.data.view.model.save(); + event.preventDefault(); + }, + onAddNewRow: function( event ) { + var view = event.data.view, + model = view.model, + rates = _.indexBy( model.get( 'rates' ), 'tax_rate_id' ), + changes = {}, + size = _.size( rates ), + newRow = _.extend( {}, data.default_rate, { + tax_rate_id: 'new-' + size + '-' + Date.now(), + newRow: true + } ), + $current, current_id, current_order, rates_to_reorder, reordered_rates; + + $current = $tbody.children( '.current' ); + + if ( $current.length ) { + current_id = $current.last().data( 'id' ); + current_order = parseInt( rates[ current_id ].tax_rate_order, 10 ); + newRow.tax_rate_order = 1 + current_order; + + rates_to_reorder = _.filter( rates, function( rate ) { + if ( parseInt( rate.tax_rate_order, 10 ) > current_order ) { + return true; + } + return false; + } ); + + reordered_rates = _.map( rates_to_reorder, function( rate ) { + rate.tax_rate_order++; + changes[ rate.tax_rate_id ] = _.extend( + changes[ rate.tax_rate_id ] || {}, { tax_rate_order : rate.tax_rate_order } + ); + return rate; + } ); + } else { + newRow.tax_rate_order = 1 + _.max( + _.pluck( rates, 'tax_rate_order' ), + function ( val ) { + // Cast them all to integers, because strings compare funky. Sighhh. + return parseInt( val, 10 ); + } + ); + // Move the last page + view.page = view.qty_pages; + } + + rates[ newRow.tax_rate_id ] = newRow; + changes[ newRow.tax_rate_id ] = newRow; + + model.set( 'rates', rates ); + model.logChanges( changes ); + + view.render(); + }, + onDeleteRow: function( event ) { + var view = event.data.view, + model = view.model, + rates = _.indexBy( model.get( 'rates' ), 'tax_rate_id' ), + changes = {}, + $current, current_id; + + event.preventDefault(); + + if ( $current = $tbody.children( '.current' ) ) { + $current.each(function(){ + current_id = $( this ).data('id'); + + delete rates[ current_id ]; + + changes[ current_id ] = _.extend( changes[ current_id ] || {}, { deleted : 'deleted' } ); + }); + + model.set( 'rates', rates ); + model.logChanges( changes ); + + view.render(); + } else { + window.alert( data.strings.no_rows_selected ); + } + }, + onSearchField: function( event ){ + event.data.view.updateUrl(); + event.data.view.render(); + }, + onPageChange: function( event ) { + var $target = $( event.currentTarget ); + + event.preventDefault(); + event.data.view.page = $target.data( 'goto' ) ? $target.data( 'goto' ) : $target.val(); + event.data.view.render(); + event.data.view.updateUrl(); + }, + onExport: function( event ) { + var csv_data = 'data:application/csv;charset=utf-8,' + data.strings.csv_data_cols.join(',') + '\n'; + + $.each( event.data.view.model.getFilteredRates(), function( id, rowData ) { + var row = ''; + + row += rowData.tax_rate_country + ','; + row += rowData.tax_rate_state + ','; + row += ( rowData.postcode ? rowData.postcode.join( '; ' ) : '' ) + ','; + row += ( rowData.city ? rowData.city.join( '; ' ) : '' ) + ','; + row += rowData.tax_rate + ','; + row += rowData.tax_rate_name + ','; + row += rowData.tax_rate_priority + ','; + row += rowData.tax_rate_compound + ','; + row += rowData.tax_rate_shipping + ','; + row += data.current_class; + + csv_data += row + '\n'; + }); + + $( this ).attr( 'href', encodeURI( csv_data ) ); + + return true; + }, + setUnloadConfirmation: function() { + this.needsUnloadConfirm = true; + $save_button.prop( 'disabled', false ); + }, + clearUnloadConfirmation: function() { + this.needsUnloadConfirm = false; + $save_button.prop( 'disabled', true ); + }, + unloadConfirmation: function( event ) { + if ( event.data.view.needsUnloadConfirm ) { + event.returnValue = data.strings.unload_confirmation_msg; + window.event.returnValue = data.strings.unload_confirmation_msg; + return data.strings.unload_confirmation_msg; + } + }, + updateModelOnChange: function( event ) { + var model = event.data.view.model, + $target = $( event.target ), + id = $target.closest( 'tr' ).data( 'id' ), + attribute = $target.data( 'attribute' ), + val = $target.val(); + + if ( 'city' === attribute || 'postcode' === attribute ) { + val = val.split( ';' ); + val = $.map( val, function( thing ) { + return thing.trim(); + }); + } + + if ( 'tax_rate_compound' === attribute || 'tax_rate_shipping' === attribute ) { + if ( $target.is( ':checked' ) ) { + val = 1; + } else { + val = 0; + } + } + + model.setRateAttribute( id, attribute, val ); + }, + sanitizePage: function( page_num ) { + page_num = parseInt( page_num, 10 ); + if ( page_num < 1 ) { + page_num = 1; + } else if ( page_num > this.qty_pages ) { + page_num = this.qty_pages; + } + return page_num; + } + } ), + WCTaxTableModelInstance = new WCTaxTableModelConstructor({ + rates: data.rates + } ), + WCTaxTableInstance = new WCTaxTableViewConstructor({ + model: WCTaxTableModelInstance, + el: '#rates' + } ); + + WCTaxTableInstance.render(); + + }); +})( jQuery, htmlSettingsTaxLocalizeScript, wp, ajaxurl ); diff --git a/assets/js/admin/settings-views-html-settings-tax.min.js b/assets/js/admin/settings-views-html-settings-tax.min.js new file mode 100644 index 0000000..41bb2a9 --- /dev/null +++ b/assets/js/admin/settings-views-html-settings-tax.min.js @@ -0,0 +1 @@ +!function(h,p,g,f){h(function(){String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")});var t=g.template("wc-tax-table-row"),o=g.template("wc-tax-table-row-empty"),s=g.template("wc-tax-table-pagination"),e=h(".wc_tax_rates"),c=h("#rates"),a=h(':input[name="save"]'),d=h("#rates-pagination"),n=h("#rates-search .wc-tax-rates-search-field"),i=h(".submit .button-primary[type=submit]"),r=Backbone.Model.extend({changes:{},setRateAttribute:function(t,e,a){var n=_.indexBy(this.get("rates"),"tax_rate_id"),i={};n[t][e]!==a&&(i[t]={},i[t][e]=a,n[t][e]=a),this.logChanges(i)},logChanges:function(t){var a=this.changes||{};_.each(t,function(t,e){a[e]=_.extend(a[e]||{tax_rate_id:e},t)}),this.changes=a,this.trigger("change:rates")},getFilteredRates:function(){var t=this.get("rates"),e=n.val().toLowerCase();return e.length&&(t=_.filter(t,function(t){return-1!==_.toArray(t).join(" ").toLowerCase().indexOf(e)})),t=_.sortBy(t,function(t){return parseInt(t.tax_rate_order,10)})},block:function(){h(".wc_tax_rates").block({message:null,overlayCSS:{background:"#fff",opacity:.6}})},unblock:function(){h(".wc_tax_rates").unblock()},save:function(){var a=this;a.block(),Backbone.ajax({method:"POST",dataType:"json",url:f+(0e}),_.map(o,function(t){return t.tax_rate_order++,r[t.tax_rate_id]=_.extend(r[t.tax_rate_id]||{},{tax_rate_order:t.tax_rate_order}),t})):(t.tax_rate_order=1+_.max(_.pluck(i,"tax_rate_order"),function(t){return parseInt(t,10)}),a.page=a.qty_pages),i[t.tax_rate_id]=t,r[t.tax_rate_id]=t,n.set("rates",i),n.logChanges(r),a.render()},onDeleteRow:function(t){var e,a=t.data.view,n=a.model,i=_.indexBy(n.get("rates"),"tax_rate_id"),r={};t.preventDefault(),(t=c.children(".current"))?(t.each(function(){e=h(this).data("id"),delete i[e],r[e]=_.extend(r[e]||{},{deleted:"deleted"})}),n.set("rates",i),n.logChanges(r),a.render()):window.alert(p.strings.no_rows_selected)},onSearchField:function(t){t.data.view.updateUrl(),t.data.view.render()},onPageChange:function(t){var e=h(t.currentTarget);t.preventDefault(),t.data.view.page=e.data("goto")?e.data("goto"):e.val(),t.data.view.render(),t.data.view.updateUrl()},onExport:function(t){var n="data:application/csv;charset=utf-8,"+p.strings.csv_data_cols.join(",")+"\n";return h.each(t.data.view.model.getFilteredRates(),function(t,e){var a="";a+=e.tax_rate_country+",",a+=e.tax_rate_state+",",a+=(e.postcode?e.postcode.join("; "):"")+",",a+=(e.city?e.city.join("; "):"")+",",a+=e.tax_rate+",",a+=e.tax_rate_name+",",a+=e.tax_rate_priority+",",a+=e.tax_rate_compound+",",a+=e.tax_rate_shipping+",",a+=p.current_class,n+=a+"\n"}),h(this).attr("href",encodeURI(n)),!0},setUnloadConfirmation:function(){this.needsUnloadConfirm=!0,a.prop("disabled",!1)},clearUnloadConfirmation:function(){this.needsUnloadConfirm=!1,a.prop("disabled",!0)},unloadConfirmation:function(t){if(t.data.view.needsUnloadConfirm)return t.returnValue=p.strings.unload_confirmation_msg,window.event.returnValue=p.strings.unload_confirmation_msg,p.strings.unload_confirmation_msg},updateModelOnChange:function(t){var e=t.data.view.model,a=h(t.target),n=a.closest("tr").data("id"),i=a.data("attribute"),t=a.val();"city"!==i&&"postcode"!==i||(t=t.split(";"),t=h.map(t,function(t){return t.trim()})),"tax_rate_compound"!==i&&"tax_rate_shipping"!==i||(t=a.is(":checked")?1:0),e.setRateAttribute(n,i,t)},sanitizePage:function(t){return(t=parseInt(t,10))<1?t=1:t>this.qty_pages&&(t=this.qty_pages),t}}),l=new r({rates:p.rates}),u=new t({model:l,el:"#rates"});u.render()})}(jQuery,htmlSettingsTaxLocalizeScript,wp,ajaxurl); \ No newline at end of file diff --git a/assets/js/admin/settings.js b/assets/js/admin/settings.js new file mode 100644 index 0000000..702b0ae --- /dev/null +++ b/assets/js/admin/settings.js @@ -0,0 +1,183 @@ +/* global woocommerce_settings_params, wp */ +( function( $, params, wp ) { + $( function() { + // Sell Countries + $( 'select#woocommerce_allowed_countries' ).on( 'change', function() { + if ( 'specific' === $( this ).val() ) { + $( this ).closest('tr').next( 'tr' ).hide(); + $( this ).closest('tr').next().next( 'tr' ).show(); + } else if ( 'all_except' === $( this ).val() ) { + $( this ).closest('tr').next( 'tr' ).show(); + $( this ).closest('tr').next().next( 'tr' ).hide(); + } else { + $( this ).closest('tr').next( 'tr' ).hide(); + $( this ).closest('tr').next().next( 'tr' ).hide(); + } + }).trigger( 'change' ); + + // Ship Countries + $( 'select#woocommerce_ship_to_countries' ).on( 'change', function() { + if ( 'specific' === $( this ).val() ) { + $( this ).closest('tr').next( 'tr' ).show(); + } else { + $( this ).closest('tr').next( 'tr' ).hide(); + } + }).trigger( 'change' ); + + // Stock management + $( 'input#woocommerce_manage_stock' ).on( 'change', function() { + if ( $( this ).is(':checked') ) { + $( this ).closest('tbody').find( '.manage_stock_field' ).closest( 'tr' ).show(); + } else { + $( this ).closest('tbody').find( '.manage_stock_field' ).closest( 'tr' ).hide(); + } + }).trigger( 'change' ); + + // Color picker + $( '.colorpick' ) + + .iris({ + change: function( event, ui ) { + $( this ).parent().find( '.colorpickpreview' ).css({ backgroundColor: ui.color.toString() }); + }, + hide: true, + border: true + }) + + .on( 'click focus', function( event ) { + event.stopPropagation(); + $( '.iris-picker' ).hide(); + $( this ).closest( 'td' ).find( '.iris-picker' ).show(); + $( this ).data( 'originalValue', $( this ).val() ); + }) + + .on( 'change', function() { + if ( $( this ).is( '.iris-error' ) ) { + var original_value = $( this ).data( 'originalValue' ); + + if ( original_value.match( /^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/ ) ) { + $( this ).val( $( this ).data( 'originalValue' ) ).trigger( 'change' ); + } else { + $( this ).val( '' ).trigger( 'change' ); + } + } + }); + + $( 'body' ).on( 'click', function() { + $( '.iris-picker' ).hide(); + }); + + // Edit prompt + $( function() { + var changed = false; + + $( 'input, textarea, select, checkbox' ).on( 'change', function() { + if ( ! changed ) { + window.onbeforeunload = function() { + return params.i18n_nav_warning; + }; + changed = true; + } + }); + + $( '.submit :input' ).on( 'click', function() { + window.onbeforeunload = ''; + }); + }); + + // Sorting + $( 'table.wc_gateways tbody, table.wc_shipping tbody' ).sortable({ + items: 'tr', + cursor: 'move', + axis: 'y', + handle: 'td.sort', + scrollSensitivity: 40, + helper: function( event, ui ) { + ui.children().each( function() { + $( this ).width( $( this ).width() ); + }); + ui.css( 'left', '0' ); + return ui; + }, + start: function( event, ui ) { + ui.item.css( 'background-color', '#f6f6f6' ); + }, + stop: function( event, ui ) { + ui.item.removeAttr( 'style' ); + ui.item.trigger( 'updateMoveButtons' ); + } + }); + + // Select all/none + $( '.woocommerce' ).on( 'click', '.select_all', function() { + $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', true ); + $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); + return false; + }); + + $( '.woocommerce' ).on( 'click', '.select_none', function() { + $( this ).closest( 'td' ).find( 'select option' ).prop( 'selected', false ); + $( this ).closest( 'td' ).find( 'select' ).trigger( 'change' ); + return false; + }); + + // Re-order buttons. + $( '.wc-item-reorder-nav').find( '.wc-move-up, .wc-move-down' ).on( 'click', function() { + var moveBtn = $( this ), + $row = moveBtn.closest( 'tr' ); + + moveBtn.trigger( 'focus' ); + + var isMoveUp = moveBtn.is( '.wc-move-up' ), + isMoveDown = moveBtn.is( '.wc-move-down' ); + + if ( isMoveUp ) { + var $previewRow = $row.prev( 'tr' ); + + if ( $previewRow && $previewRow.length ) { + $previewRow.before( $row ); + wp.a11y.speak( params.i18n_moved_up ); + } + } else if ( isMoveDown ) { + var $nextRow = $row.next( 'tr' ); + + if ( $nextRow && $nextRow.length ) { + $nextRow.after( $row ); + wp.a11y.speak( params.i18n_moved_down ); + } + } + + moveBtn.trigger( 'focus' ); // Re-focus after the container was moved. + moveBtn.closest( 'table' ).trigger( 'updateMoveButtons' ); + } ); + + $( '.wc-item-reorder-nav').closest( 'table' ).on( 'updateMoveButtons', function() { + var table = $( this ), + lastRow = $( this ).find( 'tbody tr:last' ), + firstRow = $( this ).find( 'tbody tr:first' ); + + table.find( '.wc-item-reorder-nav .wc-move-disabled' ).removeClass( 'wc-move-disabled' ) + .attr( { 'tabindex': '0', 'aria-hidden': 'false' } ); + firstRow.find( '.wc-item-reorder-nav .wc-move-up' ).addClass( 'wc-move-disabled' ) + .attr( { 'tabindex': '-1', 'aria-hidden': 'true' } ); + lastRow.find( '.wc-item-reorder-nav .wc-move-down' ).addClass( 'wc-move-disabled' ) + .attr( { 'tabindex': '-1', 'aria-hidden': 'true' } ); + } ); + + $( '.wc-item-reorder-nav').closest( 'table' ).trigger( 'updateMoveButtons' ); + + + $( '.submit button' ).on( 'click', function() { + if ( + $( 'select#woocommerce_allowed_countries' ).val() === 'specific' && + ! $( '[name="woocommerce_specific_allowed_countries[]"]' ).val() + ) { + if ( window.confirm( woocommerce_settings_params.i18n_no_specific_countries_selected ) ) { + return true; + } + return false; + } + } ); + + }); +})( jQuery, woocommerce_settings_params, wp ); diff --git a/assets/js/admin/settings.min.js b/assets/js/admin/settings.min.js new file mode 100644 index 0000000..0772e7f --- /dev/null +++ b/assets/js/admin/settings.min.js @@ -0,0 +1 @@ +!function(n,c,s){n(function(){n("select#woocommerce_allowed_countries").on("change",function(){"specific"===n(this).val()?(n(this).closest("tr").next("tr").hide(),n(this).closest("tr").next().next("tr").show()):("all_except"===n(this).val()?n(this).closest("tr").next("tr").show():n(this).closest("tr").next("tr").hide(),n(this).closest("tr").next().next("tr").hide())}).trigger("change"),n("select#woocommerce_ship_to_countries").on("change",function(){"specific"===n(this).val()?n(this).closest("tr").next("tr").show():n(this).closest("tr").next("tr").hide()}).trigger("change"),n("input#woocommerce_manage_stock").on("change",function(){n(this).is(":checked")?n(this).closest("tbody").find(".manage_stock_field").closest("tr").show():n(this).closest("tbody").find(".manage_stock_field").closest("tr").hide()}).trigger("change"),n(".colorpick").iris({change:function(e,t){n(this).parent().find(".colorpickpreview").css({backgroundColor:t.color.toString()})},hide:!0,border:!0}).on("click focus",function(e){e.stopPropagation(),n(".iris-picker").hide(),n(this).closest("td").find(".iris-picker").show(),n(this).data("originalValue",n(this).val())}).on("change",function(){n(this).is(".iris-error")&&(n(this).data("originalValue").match(/^\#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/)?n(this).val(n(this).data("originalValue")):n(this).val("")).trigger("change")}),n("body").on("click",function(){n(".iris-picker").hide()}),n(function(){var e=!1;n("input, textarea, select, checkbox").on("change",function(){e||(window.onbeforeunload=function(){return c.i18n_nav_warning},e=!0)}),n(".submit :input").on("click",function(){window.onbeforeunload=""})}),n("table.wc_gateways tbody, table.wc_shipping tbody").sortable({items:"tr",cursor:"move",axis:"y",handle:"td.sort",scrollSensitivity:40,helper:function(e,t){return t.children().each(function(){n(this).width(n(this).width())}),t.css("left","0"),t},start:function(e,t){t.item.css("background-color","#f6f6f6")},stop:function(e,t){t.item.removeAttr("style"),t.item.trigger("updateMoveButtons")}}),n(".woocommerce").on("click",".select_all",function(){return n(this).closest("td").find("select option").prop("selected",!0),n(this).closest("td").find("select").trigger("change"),!1}),n(".woocommerce").on("click",".select_none",function(){return n(this).closest("td").find("select option").prop("selected",!1),n(this).closest("td").find("select").trigger("change"),!1}),n(".wc-item-reorder-nav").find(".wc-move-up, .wc-move-down").on("click",function(){var e=n(this),t=e.closest("tr");e.trigger("focus");var i=e.is(".wc-move-up"),o=e.is(".wc-move-down");i?(i=t.prev("tr"))&&i.length&&(i.before(t),s.a11y.speak(c.i18n_moved_up)):!o||(o=t.next("tr"))&&o.length&&(o.after(t),s.a11y.speak(c.i18n_moved_down)),e.trigger("focus"),e.closest("table").trigger("updateMoveButtons")}),n(".wc-item-reorder-nav").closest("table").on("updateMoveButtons",function(){var e=n(this),t=n(this).find("tbody tr:last"),i=n(this).find("tbody tr:first");e.find(".wc-item-reorder-nav .wc-move-disabled").removeClass("wc-move-disabled").attr({tabindex:"0","aria-hidden":"false"}),i.find(".wc-item-reorder-nav .wc-move-up").addClass("wc-move-disabled").attr({tabindex:"-1","aria-hidden":"true"}),t.find(".wc-item-reorder-nav .wc-move-down").addClass("wc-move-disabled").attr({tabindex:"-1","aria-hidden":"true"})}),n(".wc-item-reorder-nav").closest("table").trigger("updateMoveButtons"),n(".submit button").on("click",function(){if("specific"===n("select#woocommerce_allowed_countries").val()&&!n('[name="woocommerce_specific_allowed_countries[]"]').val())return!!window.confirm(woocommerce_settings_params.i18n_no_specific_countries_selected)})})}(jQuery,woocommerce_settings_params,wp); \ No newline at end of file diff --git a/assets/js/admin/system-status.js b/assets/js/admin/system-status.js new file mode 100644 index 0000000..a245753 --- /dev/null +++ b/assets/js/admin/system-status.js @@ -0,0 +1,126 @@ +/* global jQuery, woocommerce_admin_system_status, wcSetClipboard, wcClearClipboard */ +jQuery( function ( $ ) { + + /** + * Users country and state fields + */ + var wcSystemStatus = { + init: function() { + $( document.body ) + .on( 'click', 'a.help_tip, a.woocommerce-help-tip', this.preventTipTipClick ) + .on( 'click', 'a.debug-report', this.generateReport ) + .on( 'click', '#copy-for-support', this.copyReport ) + .on( 'aftercopy', '#copy-for-support', this.copySuccess ) + .on( 'aftercopyfailure', '#copy-for-support', this.copyFail ); + }, + + /** + * Prevent anchor behavior when click on TipTip. + * + * @return {Bool} + */ + preventTipTipClick: function() { + return false; + }, + + /** + * Generate system status report. + * + * @return {Bool} + */ + generateReport: function() { + var report = ''; + + $( '.wc_status_table thead, .wc_status_table tbody' ).each( function() { + if ( $( this ).is( 'thead' ) ) { + var label = $( this ).find( 'th:eq(0)' ).data( 'exportLabel' ) || $( this ).text(); + report = report + '\n### ' + label.trim() + ' ###\n\n'; + } else { + $( 'tr', $( this ) ).each( function() { + var label = $( this ).find( 'td:eq(0)' ).data( 'exportLabel' ) || $( this ).find( 'td:eq(0)' ).text(); + var the_name = label.trim().replace( /(<([^>]+)>)/ig, '' ); // Remove HTML. + + // Find value + var $value_html = $( this ).find( 'td:eq(2)' ).clone(); + $value_html.find( '.private' ).remove(); + $value_html.find( '.dashicons-yes' ).replaceWith( '✔' ); + $value_html.find( '.dashicons-no-alt, .dashicons-warning' ).replaceWith( '❌' ); + + // Format value + var the_value = $value_html.text().trim(); + var value_array = the_value.split( ', ' ); + + if ( value_array.length > 1 ) { + // If value have a list of plugins ','. + // Split to add new line. + var temp_line =''; + $.each( value_array, function( key, line ) { + temp_line = temp_line + line + '\n'; + }); + + the_value = temp_line; + } + + report = report + '' + the_name + ': ' + the_value + '\n'; + }); + } + }); + + try { + $( '#debug-report' ).slideDown(); + $( '#debug-report' ).find( 'textarea' ).val( '`' + report + '`' ).trigger( 'focus' ).trigger( 'select' ); + $( this ).fadeOut(); + return false; + } catch ( e ) { + /* jshint devel: true */ + console.log( e ); + } + + return false; + }, + + /** + * Copy for report. + * + * @param {Object} evt Copy event. + */ + copyReport: function( evt ) { + wcClearClipboard(); + wcSetClipboard( $( '#debug-report' ).find( 'textarea' ).val(), $( this ) ); + evt.preventDefault(); + }, + + /** + * Display a "Copied!" tip when success copying + */ + copySuccess: function() { + $( '#copy-for-support' ).tipTip({ + 'attribute': 'data-tip', + 'activation': 'focus', + 'fadeIn': 50, + 'fadeOut': 50, + 'delay': 0 + }).trigger( 'focus' ); + }, + + /** + * Displays the copy error message when failure copying. + */ + copyFail: function() { + $( '.copy-error' ).removeClass( 'hidden' ); + $( '#debug-report' ).find( 'textarea' ).trigger( 'focus' ).trigger( 'select' ); + } + }; + + wcSystemStatus.init(); + + $( '.wc_status_table' ).on( 'click', '.run-tool .button', function( evt ) { + evt.stopImmediatePropagation(); + return window.confirm( woocommerce_admin_system_status.run_tool_confirmation ); + }); + + $( '#log-viewer-select' ).on( 'click', 'h2 a.page-title-action', function( evt ) { + evt.stopImmediatePropagation(); + return window.confirm( woocommerce_admin_system_status.delete_log_confirmation ); + }); +}); diff --git a/assets/js/admin/system-status.min.js b/assets/js/admin/system-status.min.js new file mode 100644 index 0000000..d406b0c --- /dev/null +++ b/assets/js/admin/system-status.min.js @@ -0,0 +1 @@ +jQuery(function(n){({init:function(){n(document.body).on("click","a.help_tip, a.woocommerce-help-tip",this.preventTipTipClick).on("click","a.debug-report",this.generateReport).on("click","#copy-for-support",this.copyReport).on("aftercopy","#copy-for-support",this.copySuccess).on("aftercopyfailure","#copy-for-support",this.copyFail)},preventTipTipClick:function(){return!1},generateReport:function(){var r="";n(".wc_status_table thead, .wc_status_table tbody").each(function(){var t;n(this).is("thead")?(t=n(this).find("th:eq(0)").data("exportLabel")||n(this).text(),r=r+"\n### "+t.trim()+" ###\n\n"):n("tr",n(this)).each(function(){var t=(n(this).find("td:eq(0)").data("exportLabel")||n(this).find("td:eq(0)").text()).trim().replace(/(<([^>]+)>)/gi,""),e=n(this).find("td:eq(2)").clone();e.find(".private").remove(),e.find(".dashicons-yes").replaceWith("✔"),e.find(".dashicons-no-alt, .dashicons-warning").replaceWith("❌");var o,i=e.text().trim(),e=i.split(", ");1 tr'); + var rows_with_handle = $( table_selector ).find('tbody > tr > td.column-handle').parent(); + if ( all_table_rows.length !== rows_with_handle.length ) { + all_table_rows.each(function(index, elem){ + if ( ! rows_with_handle.is( elem ) ) { + $( elem ).append( column_handle ); + } + }); + } + $( table_selector ).find( '.column-handle' ).show(); + }; + + $( document ).ajaxComplete( function( event, request, options ) { + if ( + request && + 4 === request.readyState && + 200 === request.status && + options.data && + ( 0 <= options.data.indexOf( '_inline_edit' ) || 0 <= options.data.indexOf( 'add-tag' ) ) + ) { + $.wc_add_missing_sort_handles(); + $( document.body ).trigger( 'init_tooltips' ); + } + } ); + + $( table_selector ).sortable({ + items: item_selector, + cursor: 'move', + handle: '.column-handle', + axis: 'y', + forcePlaceholderSize: true, + helper: 'clone', + opacity: 0.65, + placeholder: 'product-cat-placeholder', + scrollSensitivity: 40, + start: function( event, ui ) { + if ( ! ui.item.hasClass( 'alternate' ) ) { + ui.item.css( 'background-color', '#ffffff' ); + } + ui.item.children( 'td, th' ).css( 'border-bottom-width', '0' ); + ui.item.css( 'outline', '1px solid #aaa' ); + }, + stop: function( event, ui ) { + ui.item.removeAttr( 'style' ); + ui.item.children( 'td, th' ).css( 'border-bottom-width', '1px' ); + }, + update: function( event, ui ) { + var termid = ui.item.find( term_id_selector ).val(); // this post id + var termparent = ui.item.find( '.parent' ).html(); // post parent + + var prevtermid = ui.item.prev().find( term_id_selector ).val(); + var nexttermid = ui.item.next().find( term_id_selector ).val(); + + // Can only sort in same tree + var prevtermparent, nexttermparent; + if ( prevtermid !== undefined ) { + prevtermparent = ui.item.prev().find( '.parent' ).html(); + if ( prevtermparent !== termparent) { + prevtermid = undefined; + } + } + + if ( nexttermid !== undefined ) { + nexttermparent = ui.item.next().find( '.parent' ).html(); + if ( nexttermparent !== termparent) { + nexttermid = undefined; + } + } + + // If previous and next not at same tree level, or next not at same tree level and + // the previous is the parent of the next, or just moved item beneath its own children. + if ( + ( prevtermid === undefined && nexttermid === undefined ) || + ( nexttermid === undefined && nexttermparent === prevtermid ) || + ( nexttermid !== undefined && prevtermparent === termid ) + ) { + $( table_selector ).sortable( 'cancel' ); + return; + } + + // Show Spinner + ui.item.find( '.check-column input' ).hide(); + ui.item + .find( '.check-column' ) + .append( 'processing' ); + + // Go do the sorting stuff via ajax. + $.post( + ajaxurl, + { + action: 'woocommerce_term_ordering', + id: termid, + nextid: nexttermid, + thetaxonomy: woocommerce_term_ordering_params.taxonomy + }, + function(response) { + if ( response === 'children' ) { + window.location.reload(); + } else { + ui.item.find( '.check-column input' ).show(); + ui.item.find( '.check-column' ).find( 'img' ).remove(); + } + } + ); + + // Fix cell colors + $( 'table.widefat tbody tr' ).each( function() { + var i = jQuery( 'table.widefat tbody tr' ).index( this ); + if ( i%2 === 0 ) { + jQuery( this ).addClass( 'alternate' ); + } else { + jQuery( this ).removeClass( 'alternate' ); + } + }); + } + }); + +}); diff --git a/assets/js/admin/term-ordering.min.js b/assets/js/admin/term-ordering.min.js new file mode 100644 index 0000000..dc3f6bd --- /dev/null +++ b/assets/js/admin/term-ordering.min.js @@ -0,0 +1 @@ +jQuery(function(r){var c="table.wp-list-table",m='.column-handle input[name="term_id"]',i='';0===r(c).find(".column-handle").length&&(r(c).find("tr:not(.inline-edit-row)").append(i),m=".check-column input"),r(c).find(".column-handle").show(),r.wc_add_missing_sort_handles=function(){var e=r(c).find("tbody > tr"),n=r(c).find("tbody > tr > td.column-handle").parent();e.length!==n.length&&e.each(function(e,t){n.is(t)||r(t).append(i)}),r(c).find(".column-handle").show()},r(document).ajaxComplete(function(e,t,n){t&&4===t.readyState&&200===t.status&&n.data&&(0<=n.data.indexOf("_inline_edit")||0<=n.data.indexOf("add-tag"))&&(r.wc_add_missing_sort_handles(),r(document.body).trigger("init_tooltips"))}),r(c).sortable({items:"tbody tr:not(.inline-edit-row)",cursor:"move",handle:".column-handle",axis:"y",forcePlaceholderSize:!0,helper:"clone",opacity:.65,placeholder:"product-cat-placeholder",scrollSensitivity:40,start:function(e,t){t.item.hasClass("alternate")||t.item.css("background-color","#ffffff"),t.item.children("td, th").css("border-bottom-width","0"),t.item.css("outline","1px solid #aaa")},stop:function(e,t){t.item.removeAttr("style"),t.item.children("td, th").css("border-bottom-width","1px")},update:function(e,t){var n,i,d=t.item.find(m).val(),a=t.item.find(".parent").html(),o=t.item.prev().find(m).val(),l=t.item.next().find(m).val();o!==undefined&&(n=t.item.prev().find(".parent").html())!==a&&(o=undefined),l!==undefined&&(i=t.item.next().find(".parent").html())!==a&&(l=undefined),o===undefined&&l===undefined||l===undefined&&i===o||l!==undefined&&n===d?r(c).sortable("cancel"):(t.item.find(".check-column input").hide(),t.item.find(".check-column").append('processing'),r.post(ajaxurl,{action:"woocommerce_term_ordering",id:d,nextid:l,thetaxonomy:woocommerce_term_ordering_params.taxonomy},function(e){"children"===e?window.location.reload():(t.item.find(".check-column input").show(),t.item.find(".check-column").find("img").remove())}),r("table.widefat tbody tr").each(function(){jQuery("table.widefat tbody tr").index(this)%2==0?jQuery(this).addClass("alternate"):jQuery(this).removeClass("alternate")}))}})}); \ No newline at end of file diff --git a/assets/js/admin/users.js b/assets/js/admin/users.js new file mode 100644 index 0000000..2d6c1fa --- /dev/null +++ b/assets/js/admin/users.js @@ -0,0 +1,120 @@ +/*global wc_users_params */ +jQuery( function ( $ ) { + + /** + * Users country and state fields + */ + var wc_users_fields = { + states: null, + init: function() { + if ( typeof wc_users_params.countries !== 'undefined' ) { + /* State/Country select boxes */ + this.states = JSON.parse( wc_users_params.countries.replace( /"/g, '"' ) ); + } + + $( '.js_field-country' ).selectWoo().on( 'change', this.change_country ); + $( '.js_field-country' ).trigger( 'change', [ true ] ); + $( document.body ).on( 'change', 'select.js_field-state', this.change_state ); + + $( document.body ).on( 'click', 'button.js_copy-billing', this.copy_billing ); + }, + + change_country: function( e, stickValue ) { + // Check for stickValue before using it + if ( typeof stickValue === 'undefined' ) { + stickValue = false; + } + + // Prevent if we don't have the metabox data + if ( wc_users_fields.states === null ) { + return; + } + + var $this = $( this ), + country = $this.val(), + $state = $this.parents( '.form-table' ).find( ':input.js_field-state' ), + $parent = $state.parent(), + input_name = $state.attr( 'name' ), + input_id = $state.attr( 'id' ), + stickstatefield = 'woocommerce.stickState-' + country, + value = $this.data( stickstatefield ) ? $this.data( stickstatefield ) : $state.val(), + placeholder = $state.attr( 'placeholder' ), + $newstate; + + if ( stickValue ){ + $this.data( 'woocommerce.stickState-' + country, value ); + } + + // Remove the previous DOM element + $parent.show().find( '.select2-container' ).remove(); + + if ( ! $.isEmptyObject( wc_users_fields.states[ country ] ) ) { + var state = wc_users_fields.states[ country ], + $defaultOption = $( '' ) + .text( wc_users_fields.i18n_select_state_text ); + + $newstate = $( '' ) + .prop( 'id', input_id ) + .prop( 'name', input_name ) + .prop( 'placeholder', placeholder ) + .addClass( 'js_field-state' ) + .append( $defaultOption ); + + $.each( state, function( index ) { + var $option = $( '' ) + .prop( 'value', index ) + .text( state[ index ] ); + $newstate.append( $option ); + } ); + + $newstate.val( value ); + + $state.replaceWith( $newstate ); + + $newstate.show().selectWoo().hide().trigger( 'change' ); + } else { + $newstate = $( '' ) + .prop( 'id', input_id ) + .prop( 'name', input_name ) + .prop( 'placeholder', placeholder ) + .addClass( 'js_field-state regular-text' ) + .val( value ); + $state.replaceWith( $newstate ); + } + + // This event has a typo - deprecated in 2.5.0 + $( document.body ).trigger( 'contry-change.woocommerce', [country, $( this ).closest( 'div' )] ); + $( document.body ).trigger( 'country-change.woocommerce', [country, $( this ).closest( 'div' )] ); + }, + + change_state: function() { + // Here we will find if state value on a select has changed and stick it to the country data + var $this = $( this ), + state = $this.val(), + $country = $this.parents( '.form-table' ).find( ':input.js_field-country' ), + country = $country.val(); + + $country.data( 'woocommerce.stickState-' + country, state ); + }, + + copy_billing: function( event ) { + event.preventDefault(); + + $( '#fieldset-billing' ).find( 'input, select' ).each( function( i, el ) { + // The address keys match up, except for the prefix + var shipName = el.name.replace( /^billing_/, 'shipping_' ); + // Swap prefix, then check if there are any elements + var shipEl = $( '[name="' + shipName + '"]' ); + // No corresponding shipping field, skip this item + if ( ! shipEl.length ) { + return; + } + // Found a matching shipping element, update the value + shipEl.val( el.value ).trigger( 'change' ); + } ); + } + }; + + wc_users_fields.init(); + +}); diff --git a/assets/js/admin/users.min.js b/assets/js/admin/users.min.js new file mode 100644 index 0000000..9bceaff --- /dev/null +++ b/assets/js/admin/users.min.js @@ -0,0 +1 @@ +jQuery(function(u){var h={states:null,init:function(){"undefined"!=typeof wc_users_params.countries&&(this.states=JSON.parse(wc_users_params.countries.replace(/"/g,'"'))),u(".js_field-country").selectWoo().on("change",this.change_country),u(".js_field-country").trigger("change",[!0]),u(document.body).on("change","select.js_field-state",this.change_state),u(document.body).on("click","button.js_copy-billing",this.copy_billing)},change_country:function(e,t){var a,n,o,i,c,s,l,r,p,d;void 0===t&&(t=!1),null!==h.states&&(n=(a=u(this)).val(),p=(o=a.parents(".form-table").find(":input.js_field-state")).parent(),i=o.attr("name"),c=o.attr("id"),l="woocommerce.stickState-"+n,s=a.data(l)?a.data(l):o.val(),l=o.attr("placeholder"),t&&a.data("woocommerce.stickState-"+n,s),p.show().find(".select2-container").remove(),u.isEmptyObject(h.states[n])?(d=u('').prop("id",c).prop("name",i).prop("placeholder",l).addClass("js_field-state regular-text").val(s),o.replaceWith(d)):(r=h.states[n],p=u('').text(h.i18n_select_state_text),d=u('').prop("id",c).prop("name",i).prop("placeholder",l).addClass("js_field-state").append(p),u.each(r,function(e){e=u("").prop("value",e).text(r[e]);d.append(e)}),d.val(s),o.replaceWith(d),d.show().selectWoo().hide().trigger("change")),u(document.body).trigger("contry-change.woocommerce",[n,u(this).closest("div")]),u(document.body).trigger("country-change.woocommerce",[n,u(this).closest("div")]))},change_state:function(){var e=u(this),t=e.val(),a=e.parents(".form-table").find(":input.js_field-country"),e=a.val();a.data("woocommerce.stickState-"+e,t)},copy_billing:function(e){e.preventDefault(),u("#fieldset-billing").find("input, select").each(function(e,t){var a=t.name.replace(/^billing_/,"shipping_"),a=u('[name="'+a+'"]');a.length&&a.val(t.value).trigger("change")})}};h.init()}); \ No newline at end of file diff --git a/assets/js/admin/wc-clipboard.js b/assets/js/admin/wc-clipboard.js new file mode 100644 index 0000000..8a23912 --- /dev/null +++ b/assets/js/admin/wc-clipboard.js @@ -0,0 +1,38 @@ +/* exported wcSetClipboard, wcClearClipboard */ + +/** + * Simple text copy functions using native browser clipboard capabilities. + * @since 3.2.0 + */ + +/** + * Set the user's clipboard contents. + * + * @param string data: Text to copy to clipboard. + * @param object $el: jQuery element to trigger copy events on. (Default: document) + */ +function wcSetClipboard( data, $el ) { + if ( 'undefined' === typeof $el ) { + $el = jQuery( document ); + } + var $temp_input = jQuery( ' + get_description_html( $data ); // WPCS: XSS ok. ?> + + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'label' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + + if ( ! $data['label'] ) { + $data['label'] = $data['title']; + } + + ob_start(); + ?> + + + + + +
    + +
    + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + 'options' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + $value = $this->get_option( $key ); + + ob_start(); + ?> + + + + + +
    + + + get_description_html( $data ); // WPCS: XSS ok. ?> +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'disabled' => false, + 'class' => '', + 'css' => '', + 'placeholder' => '', + 'type' => 'text', + 'desc_tip' => false, + 'description' => '', + 'custom_attributes' => array(), + 'select_buttons' => false, + 'options' => array(), + ); + + $data = wp_parse_args( $data, $defaults ); + $value = (array) $this->get_option( $key, array() ); + + ob_start(); + ?> + + + + + +
    + + + get_description_html( $data ); // WPCS: XSS ok. ?> + +
    + +
    + + + get_field_key( $key ); + $defaults = array( + 'title' => '', + 'class' => '', + ); + + $data = wp_parse_args( $data, $defaults ); + + ob_start(); + ?> + +

    + +

    + + + array( + 'src' => true, + 'style' => true, + 'id' => true, + 'class' => true, + ), + ), + wp_kses_allowed_html( 'post' ) + ) + ); + } + + /** + * Validate Checkbox Field. + * + * If not set, return "no", otherwise return "yes". + * + * @param string $key Field key. + * @param string $value Posted Value. + * @return string + */ + public function validate_checkbox_field( $key, $value ) { + return ! is_null( $value ) ? 'yes' : 'no'; + } + + /** + * Validate Select Field. + * + * @param string $key Field key. + * @param string $value Posted Value. + * @return string + */ + public function validate_select_field( $key, $value ) { + $value = is_null( $value ) ? '' : $value; + return wc_clean( stripslashes( $value ) ); + } + + /** + * Validate Multiselect Field. + * + * @param string $key Field key. + * @param string $value Posted Value. + * @return string|array + */ + public function validate_multiselect_field( $key, $value ) { + return is_array( $value ) ? array_map( 'wc_clean', array_map( 'stripslashes', $value ) ) : ''; + } + + /** + * Validate the data on the "Settings" form. + * + * @deprecated 2.6.0 No longer used. + * @param array $form_fields Array of fields. + */ + public function validate_settings_fields( $form_fields = array() ) { + wc_deprecated_function( 'validate_settings_fields', '2.6' ); + } + + /** + * Format settings if needed. + * + * @deprecated 2.6.0 Unused. + * @param array $value Value to format. + * @return array + */ + public function format_settings( $value ) { + wc_deprecated_function( 'format_settings', '2.6' ); + return $value; + } +} diff --git a/includes/abstracts/abstract-wc-shipping-method.php b/includes/abstracts/abstract-wc-shipping-method.php new file mode 100644 index 0000000..c27b0ae --- /dev/null +++ b/includes/abstracts/abstract-wc-shipping-method.php @@ -0,0 +1,569 @@ +instance_id = absint( $instance_id ); + } + + /** + * Check if a shipping method supports a given feature. + * + * Methods should override this to declare support (or lack of support) for a feature. + * + * @param string $feature The name of a feature to test support for. + * @return bool True if the shipping method supports the feature, false otherwise. + */ + public function supports( $feature ) { + return apply_filters( 'woocommerce_shipping_method_supports', in_array( $feature, $this->supports ), $feature, $this ); + } + + /** + * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method. + * + * @param array $package Package array. + */ + public function calculate_shipping( $package = array() ) {} + + /** + * Whether or not we need to calculate tax on top of the shipping rate. + * + * @return boolean + */ + public function is_taxable() { + return wc_tax_enabled() && 'taxable' === $this->tax_status && ( WC()->customer && ! WC()->customer->get_is_vat_exempt() ); + } + + /** + * Whether or not this method is enabled in settings. + * + * @since 2.6.0 + * @return boolean + */ + public function is_enabled() { + return 'yes' === $this->enabled; + } + + /** + * Return the shipping method instance ID. + * + * @since 2.6.0 + * @return int + */ + public function get_instance_id() { + return $this->instance_id; + } + + /** + * Return the shipping method title. + * + * @since 2.6.0 + * @return string + */ + public function get_method_title() { + return apply_filters( 'woocommerce_shipping_method_title', $this->method_title, $this ); + } + + /** + * Return the shipping method description. + * + * @since 2.6.0 + * @return string + */ + public function get_method_description() { + return apply_filters( 'woocommerce_shipping_method_description', $this->method_description, $this ); + } + + /** + * Return the shipping title which is user set. + * + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_shipping_method_title', $this->title, $this->id ); + } + + /** + * Return calculated rates for a package. + * + * @since 2.6.0 + * @param array $package Package array. + * @return array + */ + public function get_rates_for_package( $package ) { + $this->rates = array(); + if ( $this->is_available( $package ) && ( empty( $package['ship_via'] ) || in_array( $this->id, $package['ship_via'] ) ) ) { + $this->calculate_shipping( $package ); + } + return $this->rates; + } + + /** + * Returns a rate ID based on this methods ID and instance, with an optional + * suffix if distinguishing between multiple rates. + * + * @since 2.6.0 + * @param string $suffix Suffix. + * @return string + */ + public function get_rate_id( $suffix = '' ) { + $rate_id = array( $this->id ); + + if ( $this->instance_id ) { + $rate_id[] = $this->instance_id; + } + + if ( $suffix ) { + $rate_id[] = $suffix; + } + + return implode( ':', $rate_id ); + } + + /** + * Add a shipping rate. If taxes are not set they will be calculated based on cost. + * + * @param array $args Arguments (default: array()). + */ + public function add_rate( $args = array() ) { + $args = apply_filters( + 'woocommerce_shipping_method_add_rate_args', + wp_parse_args( + $args, + array( + 'id' => $this->get_rate_id(), // ID for the rate. If not passed, this id:instance default will be used. + 'label' => '', // Label for the rate. + 'cost' => '0', // Amount or array of costs (per item shipping). + 'taxes' => '', // Pass taxes, or leave empty to have it calculated for you, or 'false' to disable calculations. + 'calc_tax' => 'per_order', // Calc tax per_order or per_item. Per item needs an array of costs. + 'meta_data' => array(), // Array of misc meta data to store along with this rate - key value pairs. + 'package' => false, // Package array this rate was generated for @since 2.6.0. + 'price_decimals' => wc_get_price_decimals(), + ) + ), + $this + ); + + // ID and label are required. + if ( ! $args['id'] || ! $args['label'] ) { + return; + } + + // Total up the cost. + $total_cost = is_array( $args['cost'] ) ? array_sum( $args['cost'] ) : $args['cost']; + $taxes = $args['taxes']; + + // Taxes - if not an array and not set to false, calc tax based on cost and passed calc_tax variable. This saves shipping methods having to do complex tax calculations. + if ( ! is_array( $taxes ) && false !== $taxes && $total_cost > 0 && $this->is_taxable() ) { + $taxes = 'per_item' === $args['calc_tax'] ? $this->get_taxes_per_item( $args['cost'] ) : WC_Tax::calc_shipping_tax( $total_cost, WC_Tax::get_shipping_tax_rates() ); + } + + // Round the total cost after taxes have been calculated. + $total_cost = wc_format_decimal( $total_cost, $args['price_decimals'] ); + + // Create rate object. + $rate = new WC_Shipping_Rate(); + $rate->set_id( $args['id'] ); + $rate->set_method_id( $this->id ); + $rate->set_instance_id( $this->instance_id ); + $rate->set_label( $args['label'] ); + $rate->set_cost( $total_cost ); + $rate->set_taxes( $taxes ); + + if ( ! empty( $args['meta_data'] ) ) { + foreach ( $args['meta_data'] as $key => $value ) { + $rate->add_meta_data( $key, $value ); + } + } + + // Store package data. + if ( $args['package'] ) { + $items_in_package = array(); + foreach ( $args['package']['contents'] as $item ) { + $product = $item['data']; + $items_in_package[] = $product->get_name() . ' × ' . $item['quantity']; + } + $rate->add_meta_data( __( 'Items', 'woocommerce' ), implode( ', ', $items_in_package ) ); + } + + $this->rates[ $args['id'] ] = apply_filters( 'woocommerce_shipping_method_add_rate', $rate, $args, $this ); + } + + /** + * Calc taxes per item being shipping in costs array. + * + * @since 2.6.0 + * @param array $costs Costs. + * @return array of taxes + */ + protected function get_taxes_per_item( $costs ) { + $taxes = array(); + + // If we have an array of costs we can look up each items tax class and add tax accordingly. + if ( is_array( $costs ) ) { + + $cart = WC()->cart->get_cart(); + + foreach ( $costs as $cost_key => $amount ) { + if ( ! isset( $cart[ $cost_key ] ) ) { + continue; + } + + $item_taxes = WC_Tax::calc_shipping_tax( $amount, WC_Tax::get_shipping_tax_rates( $cart[ $cost_key ]['data']->get_tax_class() ) ); + + // Sum the item taxes. + foreach ( array_keys( $taxes + $item_taxes ) as $key ) { + $taxes[ $key ] = ( isset( $item_taxes[ $key ] ) ? $item_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 ); + } + } + + // Add any cost for the order - order costs are in the key 'order'. + if ( isset( $costs['order'] ) ) { + $item_taxes = WC_Tax::calc_shipping_tax( $costs['order'], WC_Tax::get_shipping_tax_rates() ); + + // Sum the item taxes. + foreach ( array_keys( $taxes + $item_taxes ) as $key ) { + $taxes[ $key ] = ( isset( $item_taxes[ $key ] ) ? $item_taxes[ $key ] : 0 ) + ( isset( $taxes[ $key ] ) ? $taxes[ $key ] : 0 ); + } + } + } + + return $taxes; + } + + /** + * Is this method available? + * + * @param array $package Package. + * @return bool + */ + public function is_available( $package ) { + $available = $this->is_enabled(); + + // Country availability (legacy, for non-zone based methods). + if ( ! $this->instance_id && $available ) { + $countries = is_array( $this->countries ) ? $this->countries : array(); + + switch ( $this->availability ) { + case 'specific': + case 'including': + $available = in_array( $package['destination']['country'], array_intersect( $countries, array_keys( WC()->countries->get_shipping_countries() ) ) ); + break; + case 'excluding': + $available = in_array( $package['destination']['country'], array_diff( array_keys( WC()->countries->get_shipping_countries() ), $countries ) ); + break; + default: + $available = in_array( $package['destination']['country'], array_keys( WC()->countries->get_shipping_countries() ) ); + break; + } + } + + return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $available, $package, $this ); + } + + /** + * Get fee to add to shipping cost. + * + * @param string|float $fee Fee. + * @param float $total Total. + * @return float + */ + public function get_fee( $fee, $total ) { + if ( strstr( $fee, '%' ) ) { + $fee = ( $total / 100 ) * str_replace( '%', '', $fee ); + } + if ( ! empty( $this->minimum_fee ) && $this->minimum_fee > $fee ) { + $fee = $this->minimum_fee; + } + return $fee; + } + + /** + * Does this method have a settings page? + * + * @return bool + */ + public function has_settings() { + return $this->instance_id ? $this->supports( 'instance-settings' ) : $this->supports( 'settings' ); + } + + /** + * Return admin options as a html string. + * + * @return string + */ + public function get_admin_options_html() { + if ( $this->instance_id ) { + $settings_html = $this->generate_settings_html( $this->get_instance_form_fields(), false ); + } else { + $settings_html = $this->generate_settings_html( $this->get_form_fields(), false ); + } + + return '
    ' . $settings_html . '
    '; + } + + /** + * Output the shipping settings screen. + */ + public function admin_options() { + if ( ! $this->instance_id ) { + echo '

    ' . esc_html( $this->get_method_title() ) . '

    '; + } + echo wp_kses_post( wpautop( $this->get_method_description() ) ); + echo $this->get_admin_options_html(); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + } + + /** + * Get_option function. + * + * Gets and option from the settings API, using defaults if necessary to prevent undefined notices. + * + * @param string $key Key. + * @param mixed $empty_value Empty value. + * @return mixed The value specified for the option or a default value for the option. + */ + public function get_option( $key, $empty_value = null ) { + // Instance options take priority over global options. + if ( $this->instance_id && array_key_exists( $key, $this->get_instance_form_fields() ) ) { + return $this->get_instance_option( $key, $empty_value ); + } + + // Return global option. + $option = apply_filters( 'woocommerce_shipping_' . $this->id . '_option', parent::get_option( $key, $empty_value ), $key, $this ); + return $option; + } + + /** + * Gets an option from the settings API, using defaults if necessary to prevent undefined notices. + * + * @param string $key Key. + * @param mixed $empty_value Empty value. + * @return mixed The value specified for the option or a default value for the option. + */ + public function get_instance_option( $key, $empty_value = null ) { + if ( empty( $this->instance_settings ) ) { + $this->init_instance_settings(); + } + + // Get option default if unset. + if ( ! isset( $this->instance_settings[ $key ] ) ) { + $form_fields = $this->get_instance_form_fields(); + $this->instance_settings[ $key ] = $this->get_field_default( $form_fields[ $key ] ); + } + + if ( ! is_null( $empty_value ) && '' === $this->instance_settings[ $key ] ) { + $this->instance_settings[ $key ] = $empty_value; + } + + $instance_option = apply_filters( 'woocommerce_shipping_' . $this->id . '_instance_option', $this->instance_settings[ $key ], $key, $this ); + return $instance_option; + } + + /** + * Get settings fields for instances of this shipping method (within zones). + * Should be overridden by shipping methods to add options. + * + * @since 2.6.0 + * @return array + */ + public function get_instance_form_fields() { + return apply_filters( 'woocommerce_shipping_instance_form_fields_' . $this->id, array_map( array( $this, 'set_defaults' ), $this->instance_form_fields ) ); + } + + /** + * Return the name of the option in the WP DB. + * + * @since 2.6.0 + * @return string + */ + public function get_instance_option_key() { + return $this->instance_id ? $this->plugin_id . $this->id . '_' . $this->instance_id . '_settings' : ''; + } + + /** + * Initialise Settings for instances. + * + * @since 2.6.0 + */ + public function init_instance_settings() { + $this->instance_settings = get_option( $this->get_instance_option_key(), null ); + + // If there are no settings defined, use defaults. + if ( ! is_array( $this->instance_settings ) ) { + $form_fields = $this->get_instance_form_fields(); + $this->instance_settings = array_merge( array_fill_keys( array_keys( $form_fields ), '' ), wp_list_pluck( $form_fields, 'default' ) ); + } + } + + /** + * Processes and saves global shipping method options in the admin area. + * + * This method is usually attached to woocommerce_update_options_x hooks. + * + * @since 2.6.0 + * @return bool was anything saved? + */ + public function process_admin_options() { + if ( ! $this->instance_id ) { + return parent::process_admin_options(); + } + + // Check we are processing the correct form for this instance. + if ( ! isset( $_REQUEST['instance_id'] ) || absint( $_REQUEST['instance_id'] ) !== $this->instance_id ) { // WPCS: input var ok, CSRF ok. + return false; + } + + $this->init_instance_settings(); + + $post_data = $this->get_post_data(); + + foreach ( $this->get_instance_form_fields() as $key => $field ) { + if ( 'title' !== $this->get_field_type( $field ) ) { + try { + $this->instance_settings[ $key ] = $this->get_field_value( $key, $field, $post_data ); + } catch ( Exception $e ) { + $this->add_error( $e->getMessage() ); + } + } + } + + return update_option( $this->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $this->id . '_instance_settings_values', $this->instance_settings, $this ), 'yes' ); + } +} diff --git a/includes/abstracts/abstract-wc-widget.php b/includes/abstracts/abstract-wc-widget.php new file mode 100644 index 0000000..bb7222b --- /dev/null +++ b/includes/abstracts/abstract-wc-widget.php @@ -0,0 +1,408 @@ + $this->widget_cssclass, + 'description' => $this->widget_description, + 'customize_selective_refresh' => true, + 'show_instance_in_rest' => true, + ); + + parent::__construct( $this->widget_id, $this->widget_name, $widget_ops ); + + add_action( 'save_post', array( $this, 'flush_widget_cache' ) ); + add_action( 'deleted_post', array( $this, 'flush_widget_cache' ) ); + add_action( 'switch_theme', array( $this, 'flush_widget_cache' ) ); + } + + /** + * Get cached widget. + * + * @param array $args Arguments. + * @return bool true if the widget is cached otherwise false + */ + public function get_cached_widget( $args ) { + // Don't get cache if widget_id doesn't exists. + if ( empty( $args['widget_id'] ) ) { + return false; + } + + $cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' ); + + if ( ! is_array( $cache ) ) { + $cache = array(); + } + + if ( isset( $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] ) ) { + echo $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ]; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + return true; + } + + return false; + } + + /** + * Cache the widget. + * + * @param array $args Arguments. + * @param string $content Content. + * @return string the content that was cached + */ + public function cache_widget( $args, $content ) { + // Don't set any cache if widget_id doesn't exist. + if ( empty( $args['widget_id'] ) ) { + return $content; + } + + $cache = wp_cache_get( $this->get_widget_id_for_cache( $this->widget_id ), 'widget' ); + + if ( ! is_array( $cache ) ) { + $cache = array(); + } + + $cache[ $this->get_widget_id_for_cache( $args['widget_id'] ) ] = $content; + + wp_cache_set( $this->get_widget_id_for_cache( $this->widget_id ), $cache, 'widget' ); + + return $content; + } + + /** + * Flush the cache. + */ + public function flush_widget_cache() { + foreach ( array( 'https', 'http' ) as $scheme ) { + wp_cache_delete( $this->get_widget_id_for_cache( $this->widget_id, $scheme ), 'widget' ); + } + } + + /** + * Get this widgets title. + * + * @param array $instance Array of instance options. + * @return string + */ + protected function get_instance_title( $instance ) { + if ( isset( $instance['title'] ) ) { + return $instance['title']; + } + + if ( isset( $this->settings, $this->settings['title'], $this->settings['title']['std'] ) ) { + return $this->settings['title']['std']; + } + + return ''; + } + + /** + * Output the html at the start of a widget. + * + * @param array $args Arguments. + * @param array $instance Instance. + */ + public function widget_start( $args, $instance ) { + echo $args['before_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + + $title = apply_filters( 'widget_title', $this->get_instance_title( $instance ), $instance, $this->id_base ); + + if ( $title ) { + echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + } + } + + /** + * Output the html at the end of a widget. + * + * @param array $args Arguments. + */ + public function widget_end( $args ) { + echo $args['after_widget']; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + } + + /** + * Updates a particular instance of a widget. + * + * @see WP_Widget->update + * @param array $new_instance New instance. + * @param array $old_instance Old instance. + * @return array + */ + public function update( $new_instance, $old_instance ) { + + $instance = $old_instance; + + if ( empty( $this->settings ) ) { + return $instance; + } + + // Loop settings and get values to save. + foreach ( $this->settings as $key => $setting ) { + if ( ! isset( $setting['type'] ) ) { + continue; + } + + // Format the value based on settings type. + switch ( $setting['type'] ) { + case 'number': + $instance[ $key ] = absint( $new_instance[ $key ] ); + + if ( isset( $setting['min'] ) && '' !== $setting['min'] ) { + $instance[ $key ] = max( $instance[ $key ], $setting['min'] ); + } + + if ( isset( $setting['max'] ) && '' !== $setting['max'] ) { + $instance[ $key ] = min( $instance[ $key ], $setting['max'] ); + } + break; + case 'textarea': + $instance[ $key ] = wp_kses( trim( wp_unslash( $new_instance[ $key ] ) ), wp_kses_allowed_html( 'post' ) ); + break; + case 'checkbox': + $instance[ $key ] = empty( $new_instance[ $key ] ) ? 0 : 1; + break; + default: + $instance[ $key ] = isset( $new_instance[ $key ] ) ? sanitize_text_field( $new_instance[ $key ] ) : $setting['std']; + break; + } + + /** + * Sanitize the value of a setting. + */ + $instance[ $key ] = apply_filters( 'woocommerce_widget_settings_sanitize_option', $instance[ $key ], $new_instance, $key, $setting ); + } + + $this->flush_widget_cache(); + + return $instance; + } + + /** + * Outputs the settings update form. + * + * @see WP_Widget->form + * + * @param array $instance Instance. + */ + public function form( $instance ) { + + if ( empty( $this->settings ) ) { + return; + } + + foreach ( $this->settings as $key => $setting ) { + + $class = isset( $setting['class'] ) ? $setting['class'] : ''; + $value = isset( $instance[ $key ] ) ? $instance[ $key ] : $setting['std']; + + switch ( $setting['type'] ) { + + case 'text': + ?> +

    + + +

    + +

    + + +

    + +

    + + +

    + +

    + + + + + +

    + +

    + /> + +

    + slug, $queried_object->taxonomy ); + } + + // Min/Max. + if ( isset( $_GET['min_price'] ) ) { + $link = add_query_arg( 'min_price', wc_clean( wp_unslash( $_GET['min_price'] ) ), $link ); + } + + if ( isset( $_GET['max_price'] ) ) { + $link = add_query_arg( 'max_price', wc_clean( wp_unslash( $_GET['max_price'] ) ), $link ); + } + + // Order by. + if ( isset( $_GET['orderby'] ) ) { + $link = add_query_arg( 'orderby', wc_clean( wp_unslash( $_GET['orderby'] ) ), $link ); + } + + /** + * Search Arg. + * To support quote characters, first they are decoded from " entities, then URL encoded. + */ + if ( get_search_query() ) { + $link = add_query_arg( 's', rawurlencode( htmlspecialchars_decode( get_search_query() ) ), $link ); + } + + // Post Type Arg. + if ( isset( $_GET['post_type'] ) ) { + $link = add_query_arg( 'post_type', wc_clean( wp_unslash( $_GET['post_type'] ) ), $link ); + + // Prevent post type and page id when pretty permalinks are disabled. + if ( is_shop() ) { + $link = remove_query_arg( 'page_id', $link ); + } + } + + // Min Rating Arg. + if ( isset( $_GET['rating_filter'] ) ) { + $link = add_query_arg( 'rating_filter', wc_clean( wp_unslash( $_GET['rating_filter'] ) ), $link ); + } + + // All current filters. + if ( $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes() ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure, WordPress.CodeAnalysis.AssignmentInCondition.Found + foreach ( $_chosen_attributes as $name => $data ) { + $filter_name = wc_attribute_taxonomy_slug( $name ); + if ( ! empty( $data['terms'] ) ) { + $link = add_query_arg( 'filter_' . $filter_name, implode( ',', $data['terms'] ), $link ); + } + if ( 'or' === $data['query_type'] ) { + $link = add_query_arg( 'query_type_' . $filter_name, 'or', $link ); + } + } + } + + return apply_filters( 'woocommerce_widget_get_current_page_url', $link, $this ); + } + + /** + * Get widget id plus scheme/protocol to prevent serving mixed content from (persistently) cached widgets. + * + * @since 3.4.0 + * @param string $widget_id Id of the cached widget. + * @param string $scheme Scheme for the widget id. + * @return string Widget id including scheme/protocol. + */ + protected function get_widget_id_for_cache( $widget_id, $scheme = '' ) { + if ( $scheme ) { + $widget_id_for_cache = $widget_id . '-' . $scheme; + } else { + $widget_id_for_cache = $widget_id . '-' . ( is_ssl() ? 'https' : 'http' ); + } + + return apply_filters( 'woocommerce_cached_widget_id', $widget_id_for_cache ); + } +} diff --git a/includes/abstracts/class-wc-background-process.php b/includes/abstracts/class-wc-background-process.php new file mode 100644 index 0000000..a48fa58 --- /dev/null +++ b/includes/abstracts/class-wc-background-process.php @@ -0,0 +1,212 @@ +options; + $column = 'option_name'; + + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + } + + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + + $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$column} LIKE %s", $key ) ); // @codingStandardsIgnoreLine. + + return ! ( $count > 0 ); + } + + /** + * Get batch. + * + * @return stdClass Return the first batch from the queue. + */ + protected function get_batch() { + global $wpdb; + + $table = $wpdb->options; + $column = 'option_name'; + $key_column = 'option_id'; + $value_column = 'option_value'; + + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + $key_column = 'meta_id'; + $value_column = 'meta_value'; + } + + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + + $query = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE {$column} LIKE %s ORDER BY {$key_column} ASC LIMIT 1", $key ) ); // @codingStandardsIgnoreLine. + + $batch = new stdClass(); + $batch->key = $query->$column; + $batch->data = array_filter( (array) maybe_unserialize( $query->$value_column ) ); + + return $batch; + } + + /** + * See if the batch limit has been exceeded. + * + * @return bool + */ + protected function batch_limit_exceeded() { + return $this->time_exceeded() || $this->memory_exceeded(); + } + + /** + * Handle. + * + * Pass each queue item to the task handler, while remaining + * within server memory and time limit constraints. + */ + protected function handle() { + $this->lock_process(); + + do { + $batch = $this->get_batch(); + + foreach ( $batch->data as $key => $value ) { + $task = $this->task( $value ); + + if ( false !== $task ) { + $batch->data[ $key ] = $task; + } else { + unset( $batch->data[ $key ] ); + } + + if ( $this->batch_limit_exceeded() ) { + // Batch limits reached. + break; + } + } + + // Update or delete current batch. + if ( ! empty( $batch->data ) ) { + $this->update( $batch->key, $batch->data ); + } else { + $this->delete( $batch->key ); + } + } while ( ! $this->batch_limit_exceeded() && ! $this->is_queue_empty() ); + + $this->unlock_process(); + + // Start next batch or complete process. + if ( ! $this->is_queue_empty() ) { + $this->dispatch(); + } else { + $this->complete(); + } + } + + /** + * 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 = '32G'; + } + + return wp_convert_hr_to_bytes( $memory_limit ); + } + + /** + * Schedule cron healthcheck. + * + * @param array $schedules Schedules. + * @return array + */ + public function schedule_cron_healthcheck( $schedules ) { + $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); + + if ( property_exists( $this, 'cron_interval' ) ) { + $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); + } + + // Adds every 5 minutes to the existing schedules. + $schedules[ $this->identifier . '_cron_interval' ] = array( + 'interval' => MINUTE_IN_SECONDS * $interval, + /* translators: %d: interval */ + 'display' => sprintf( __( 'Every %d minutes', 'woocommerce' ), $interval ), + ); + + return $schedules; + } + + /** + * Delete all batches. + * + * @return WC_Background_Process + */ + public function delete_all_batches() { + global $wpdb; + + $table = $wpdb->options; + $column = 'option_name'; + + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + } + + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + + $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE {$column} LIKE %s", $key ) ); // @codingStandardsIgnoreLine. + + return $this; + } + + /** + * Kill process. + * + * Stop processing queue items, clear cronjob and delete all batches. + */ + public function kill_process() { + if ( ! $this->is_queue_empty() ) { + $this->delete_all_batches(); + wp_clear_scheduled_hook( $this->cron_hook_identifier ); + } + } +} diff --git a/includes/admin/class-wc-admin-addons.php b/includes/admin/class-wc-admin-addons.php new file mode 100644 index 0000000..114dd93 --- /dev/null +++ b/includes/admin/class-wc-admin-addons.php @@ -0,0 +1,1363 @@ + $headers, + 'user-agent' => 'WooCommerce Addons Page', + ) + ); + + if ( ! is_wp_error( $raw_featured ) ) { + $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); + if ( $featured ) { + set_transient( 'wc_addons_featured', $featured, DAY_IN_SECONDS ); + } + } + } + + if ( is_object( $featured ) ) { + self::output_featured_sections( $featured->sections ); + return $featured; + } + } + + /** + * Render featured products and banners using WCCOM's the Featured 2.0 Endpoint + * + * @return void + */ + public static function render_featured() { + $featured = get_transient( 'wc_addons_featured_2' ); + if ( false === $featured ) { + $headers = array(); + $auth = WC_Helper_Options::get( 'auth' ); + + if ( ! empty( $auth['access_token'] ) ) { + $headers['Authorization'] = 'Bearer ' . $auth['access_token']; + } + + $parameter_string = ''; + $country = WC()->countries->get_base_country(); + if ( ! empty( $country ) ) { + $parameter_string = '?' . http_build_query( array( 'country' => $country ) ); + } + + // Important: WCCOM Extensions API v2.0 is used. + $raw_featured = wp_safe_remote_get( + 'https://woocommerce.com/wp-json/wccom-extensions/2.0/featured' . $parameter_string, + array( + 'headers' => $headers, + 'user-agent' => 'WooCommerce Addons Page', + ) + ); + + if ( ! is_wp_error( $raw_featured ) ) { + $featured = json_decode( wp_remote_retrieve_body( $raw_featured ) ); + if ( $featured ) { + set_transient( 'wc_addons_featured_2', $featured, DAY_IN_SECONDS ); + } + } + } + + if ( ! empty( $featured ) ) { + self::output_featured( $featured ); + } + } + + /** + * Build url parameter string + * + * @param string $category Addon (sub) category. + * @param string $term Search terms. + * @param string $country Store country. + * + * @return string url parameter string + */ + public static function build_parameter_string( $category, $term, $country ) { + + $parameters = array( + 'category' => $category, + 'term' => $term, + 'country' => $country, + ); + + return '?' . http_build_query( $parameters ); + } + + /** + * Call API to get extensions + * + * @param string $category Addon (sub) category. + * @param string $term Search terms. + * @param string $country Store country. + * + * @return object of extensions and promotions. + */ + public static function get_extension_data( $category, $term, $country ) { + $parameters = self::build_parameter_string( $category, $term, $country ); + + $headers = array(); + $auth = WC_Helper_Options::get( 'auth' ); + + if ( ! empty( $auth['access_token'] ) ) { + $headers['Authorization'] = 'Bearer ' . $auth['access_token']; + } + + $raw_extensions = wp_safe_remote_get( + 'https://woocommerce.com/wp-json/wccom-extensions/1.0/search' . $parameters, + array( 'headers' => $headers ) + ); + + if ( ! is_wp_error( $raw_extensions ) ) { + $addons = json_decode( wp_remote_retrieve_body( $raw_extensions ) ); + } + return $addons; + } + + /** + * Get sections for the addons screen + * + * @return array of objects + */ + public static function get_sections() { + $addon_sections = get_transient( 'wc_addons_sections' ); + if ( false === ( $addon_sections ) ) { + $raw_sections = wp_safe_remote_get( + 'https://woocommerce.com/wp-json/wccom-extensions/1.0/categories' + ); + if ( ! is_wp_error( $raw_sections ) ) { + $addon_sections = json_decode( wp_remote_retrieve_body( $raw_sections ) ); + if ( $addon_sections ) { + set_transient( 'wc_addons_sections', $addon_sections, WEEK_IN_SECONDS ); + } + } + } + return apply_filters( 'woocommerce_addons_sections', $addon_sections ); + } + + /** + * Get section for the addons screen. + * + * @param string $section_id Required section ID. + * + * @return object|bool + */ + public static function get_section( $section_id ) { + $sections = self::get_sections(); + if ( isset( $sections[ $section_id ] ) ) { + return $sections[ $section_id ]; + } + return false; + } + + + /** + * Get section content for the addons screen. + * + * @deprecated 5.9.0 No longer used in In-App Marketplace + * + * @param string $section_id Required section ID. + * + * @return array + */ + public static function get_section_data( $section_id ) { + $section = self::get_section( $section_id ); + $section_data = ''; + + if ( ! empty( $section->endpoint ) ) { + $section_data = get_transient( 'wc_addons_section_' . $section_id ); + if ( false === $section_data ) { + $raw_section = wp_safe_remote_get( esc_url_raw( $section->endpoint ), array( 'user-agent' => 'WooCommerce Addons Page' ) ); + + if ( ! is_wp_error( $raw_section ) ) { + $section_data = json_decode( wp_remote_retrieve_body( $raw_section ) ); + + if ( ! empty( $section_data->products ) ) { + set_transient( 'wc_addons_section_' . $section_id, $section_data, WEEK_IN_SECONDS ); + } + } + } + } + + return apply_filters( 'woocommerce_addons_section_data', $section_data->products, $section_id ); + } + + /** + * Handles the outputting of a contextually aware Storefront link (points to child themes if Storefront is already active). + * + * @deprecated 5.9.0 No longer used in In-App Marketplace + */ + public static function output_storefront_button() { + $template = get_option( 'template' ); + $stylesheet = get_option( 'stylesheet' ); + + if ( 'storefront' === $template ) { + if ( 'storefront' === $stylesheet ) { + $url = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/'; + $text = __( 'Need a fresh look? Try Storefront child themes', 'woocommerce' ); + $utm_content = 'nostorefrontchildtheme'; + } else { + $url = 'https://woocommerce.com/product-category/themes/storefront-child-theme-themes/'; + $text = __( 'View more Storefront child themes', 'woocommerce' ); + $utm_content = 'hasstorefrontchildtheme'; + } + } else { + $url = 'https://woocommerce.com/storefront/'; + $text = __( 'Need a theme? Try Storefront', 'woocommerce' ); + $utm_content = 'nostorefront'; + } + + $url = add_query_arg( + array( + 'utm_source' => 'addons', + 'utm_medium' => 'product', + 'utm_campaign' => 'woocommerceplugin', + 'utm_content' => $utm_content, + ), + $url + ); + + echo '' . esc_html( $text ) . '' . "\n"; + } + + /** + * Handles the outputting of a banner block. + * + * @deprecated 5.9.0 No longer used in In-App Marketplace + * + * @param object $block Banner data. + */ + public static function output_banner_block( $block ) { + ?> +
    +

    title ); ?>

    +

    description ); ?>

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

    title ); ?>

    +

    description ); ?>

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

    title ); ?>

    +

    description ); ?>

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

    title ); ?>

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

    description ); ?>

    +
    +
    + + +
    + + +
    + +
    +

    title ); ?>

    +

    description ); ?>

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

    title ); ?>

    +

    description ); ?>

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

    +

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

    +

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

    +

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

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

    title ); ?>

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

      title ); ?>

      +

      description, array() ); ?>

      +
      +
      + buttons as $button ) { + $button_classes = array( 'button', 'addons-buttons-banner-button' ); + $type = $button->type ?? null; + if ( 'primary' === $type ) { + $button_classes[] = 'addons-buttons-banner-button-primary'; + } + ?> + + title ); ?> + + +
      +
      +
    • +
    + site_url(), + 'wccom-back' => rawurlencode( $back_admin_path ), + 'wccom-woo-version' => Constants::get_constant( 'WC_VERSION' ), + 'wccom-connect-nonce' => wp_create_nonce( 'connect' ), + ); + } + + /** + * Add in-app-purchase URL params to link. + * + * Adds various url parameters to a url to support a streamlined + * flow for obtaining and setting up WooCommerce extensons. + * + * @param string $url Destination URL. + */ + public static function add_in_app_purchase_url_params( $url ) { + return add_query_arg( + self::get_in_app_purchase_url_params(), + $url + ); + } + + /** + * Outputs a button. + * + * @param string $url Destination URL. + * @param string $text Button label text. + * @param string $style Button style class. + * @param string $plugin The plugin the button is promoting. + */ + public static function output_button( $url, $text, $style, $plugin = '' ) { + $style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style; + $style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style; + $text = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text; + $url = self::add_in_app_purchase_url_params( $url ); + ?> + + + + + + + + countries->get_base_country(); + $extension_data = self::get_extension_data( $category, $term, $country ); + $addons = $extension_data->products; + $promotions = ! empty( $extension_data->promotions ) ? $extension_data->promotions : array(); + } + + // We need Automattic\WooCommerce\Admin\RemoteInboxNotifications for the next part, if not remove all promotions. + if ( ! WC()->is_wc_admin_active() ) { + $promotions = array(); + } + // Check for existence of promotions and evaluate out if we should show them. + if ( ! empty( $promotions ) ) { + foreach ( $promotions as $promo_id => $promotion ) { + $evaluator = new PromotionRuleEngine\RuleEvaluator(); + $passed = $evaluator->evaluate( $promotion->rules ); + if ( ! $passed ) { + unset( $promotions[ $promo_id ] ); + } + } + // Transform promotions to the correct format ready for output. + $promotions = self::format_promotions( $promotions ); + } + + /** + * Addon page view. + * + * @uses $addons + * @uses $search + * @uses $sections + * @uses $theme + * @uses $current_section + */ + include_once dirname( __FILE__ ) . '/views/html-admin-page-addons.php'; + } + + /** + * Install WooCommerce Services from Extensions screens. + */ + public static function install_woocommerce_services_addon() { + check_admin_referer( 'install-addon_woocommerce-services' ); + + $services_plugin_id = 'woocommerce-services'; + $services_plugin = array( + 'name' => __( 'WooCommerce Services', 'woocommerce' ), + 'repo-slug' => 'woocommerce-services', + ); + + WC_Install::background_installer( $services_plugin_id, $services_plugin ); + + wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); + exit; + } + + /** + * Install WooCommerce Payments from the Extensions screens. + * + * @param string $section Optional. Extenstions tab. + * + * @return void + */ + public static function install_woocommerce_payments_addon( $section = '_featured' ) { + check_admin_referer( 'install-addon_woocommerce-payments' ); + + $wcpay_plugin_id = 'woocommerce-payments'; + $wcpay_plugin = array( + 'name' => __( 'WooCommerce Payments', 'woocommerce' ), + 'repo-slug' => 'woocommerce-payments', + ); + + WC_Install::background_installer( $wcpay_plugin_id, $wcpay_plugin ); + + do_action( 'woocommerce_addon_installed', $wcpay_plugin_id, $section ); + + wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) ); + exit; + } + + /** + * We're displaying page=wc-addons and page=wc-addons§ion=helper as two separate pages. + * When we're on those pages, add body classes to distinguishe them. + * + * @param string $admin_body_class Unfiltered body class. + * + * @return string Body class with added class for Marketplace or My Subscriptions page. + */ + public static function filter_admin_body_classes( string $admin_body_class = '' ): string { + if ( isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { + return " $admin_body_class woocommerce-page-wc-subscriptions "; + } + + return " $admin_body_class woocommerce-page-wc-marketplace "; + } + + /** + * Take an action object and return the URL based on properties of the action. + * + * @param object $action Action object. + * @return string URL. + */ + public static function get_action_url( $action ): string { + if ( ! isset( $action->url ) ) { + return ''; + } + + if ( isset( $action->url_is_admin_query ) && $action->url_is_admin_query ) { + return wc_admin_url( $action->url ); + } + + if ( isset( $action->url_is_admin_nonce_query ) && $action->url_is_admin_nonce_query ) { + if ( empty( $action->nonce ) ) { + return ''; + } + return wp_nonce_url( + admin_url( $action->url ), + $action->nonce + ); + } + + return $action->url; + } + + /** + * Format the promotion data ready for display, ie fetch locales and actions. + * + * @param array $promotions Array of promotoin objects. + * @return array Array of formatted promotions ready for output. + */ + public static function format_promotions( array $promotions ): array { + $formatted_promotions = array(); + foreach ( $promotions as $promotion ) { + // Get the matching locale or fall back to en-US. + $locale = PromotionRuleEngine\SpecRunner::get_locale( $promotion->locales ); + if ( null === $locale ) { + continue; + } + + $promotion_actions = array(); + if ( ! empty( $promotion->actions ) ) { + foreach ( $promotion->actions as $action ) { + $action_locale = PromotionRuleEngine\SpecRunner::get_action_locale( $action->locales ); + $url = self::get_action_url( $action ); + + $promotion_actions[] = array( + 'name' => $action->name, + 'label' => $action_locale->label, + 'url' => $url, + 'primary' => isset( $action->is_primary ) ? $action->is_primary : false, + ); + } + } + + $formatted_promotions[] = array( + 'title' => $locale->title, + 'description' => $locale->description, + 'image' => ( 'http' === substr( $locale->image, 0, 4 ) ) ? $locale->image : WC()->plugin_url() . $locale->image, + 'image_alt' => $locale->image_alt, + 'actions' => $promotion_actions, + ); + } + return $formatted_promotions; + } + + /** + * Map data from different endpoints to a universal format + * + * Search and featured products has a slightly different products' field names. + * Mapping converts different data structures into a universal one for further processing. + * + * @param mixed $data Product Card Data. + * + * @return object Converted data. + */ + public static function map_product_card_data( $data ) { + $mapped = (object) null; + + $type = $data->type ?? null; + + // Icon. + $mapped->icon = $data->icon ?? null; + if ( null === $mapped->icon && 'banner' === $type ) { + // For product-related banners icon is a product's image. + $mapped->icon = $data->image ?? null; + } + // URL. + $mapped->url = $data->link ?? null; + if ( empty( $mapped->url ) ) { + $mapped->url = $data->url ?? null; + } + // Title. + $mapped->title = $data->title ?? null; + // Vendor Name. + $mapped->vendor_name = $data->vendor_name ?? null; + if ( empty( $mapped->vendor_name ) ) { + $mapped->vendor_name = $data->vendorName ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + // Vendor URL. + $mapped->vendor_url = $data->vendor_url ?? null; + if ( empty( $mapped->vendor_url ) ) { + $mapped->vendor_url = $data->vendorUrl ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + // Description. + $mapped->description = $data->excerpt ?? null; + if ( empty( $mapped->description ) ) { + $mapped->description = $data->description ?? null; + } + $has_currency = ! empty( $data->currency ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + + // Is Free. + if ( $has_currency ) { + $mapped->is_free = 0 === (int) $data->price; + } else { + $mapped->is_free = '$0.00' === $data->price; + } + // Price. + if ( $has_currency ) { + $mapped->price = wc_price( $data->price, array( 'currency' => $data->currency ) ); + } else { + $mapped->price = $data->price; + } + // Rating. + $mapped->rating = $data->rating ?? null; + if ( null === $mapped->rating ) { + $mapped->rating = $data->averageRating ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + // Reviews Count. + $mapped->reviews_count = $data->reviews_count ?? null; + if ( null === $mapped->reviews_count ) { + $mapped->reviews_count = $data->reviewsCount ?? null; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + // Featured & Promoted product card. + // Label. + $mapped->label = $data->label ?? null; + // Primary color. + $mapped->primary_color = $data->primary_color ?? null; + // Text color. + $mapped->text_color = $data->text_color ?? null; + // Button text. + $mapped->button = $data->button ?? null; + + return $mapped; + } + + /** + * Render a product card + * + * There's difference in data structure (e.g. field names) between endpoints such as search and + * featured. Inner mapping helps to use universal field names for further work. + * + * @param mixed $data Product data. + * @param string $block_type Block type that's different from the default product card, e.g. a banner. + * + * @return void + */ + public static function render_product_card( $data, $block_type = null ) { + $mapped = self::map_product_card_data( $data ); + $product_url = self::add_in_app_purchase_url_params( $mapped->url ); + $class_names = array( 'product' ); + // Specify a class name according to $block_type (if it's specified). + if ( null !== $block_type ) { + $class_names[] = 'addons-product-' . $block_type; + } + + $product_details_classes = 'product-details'; + if ( 'banner' === $block_type ) { + $product_details_classes .= ' addon-product-banner-details'; + } + + if ( isset( $mapped->label ) && 'promoted' === $mapped->label ) { + $product_details_classes .= ' promoted'; + } elseif ( isset( $mapped->label ) && 'featured' === $mapped->label ) { + $product_details_classes .= ' featured'; + } + + if ( 'promoted' === $mapped->label + && ! empty( $mapped->primary_color ) + && ! empty( $mapped->text_color ) + && ! empty( $mapped->button ) ) { + // Promoted product card. + ?> +
  • +
    + + +

    title ); ?>

    +
    +

    description ); ?>

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

    title ); ?>

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

    description ); ?>

    +
    + icon ) ) : ?> + + + + + +
    + +
  • + = $index ) { + // Rating more that current star to show. + return 'fill'; + } elseif ( + abs( $index - 1 - floor( $rating ) ) < 0.0000001 && + 0 < ( $rating - floor( $rating ) ) + ) { + // For rating more than x.0 and less than x.5 or equal it will show a half star. + return 50 >= floor( ( $rating - floor( $rating ) ) * 100 ) + ? 'half-fill' + : 'fill'; + } + + // Don't show a golden star otherwise. + return 'no-fill'; + } +} diff --git a/includes/admin/class-wc-admin-api-keys-table-list.php b/includes/admin/class-wc-admin-api-keys-table-list.php new file mode 100644 index 0000000..000652c --- /dev/null +++ b/includes/admin/class-wc-admin-api-keys-table-list.php @@ -0,0 +1,278 @@ + 'key', + 'plural' => 'keys', + 'ajax' => false, + ) + ); + } + + /** + * No items found text. + */ + public function no_items() { + esc_html_e( 'No keys found.', 'woocommerce' ); + } + + /** + * Get list columns. + * + * @return array + */ + public function get_columns() { + return array( + 'cb' => '', + 'title' => __( 'Description', 'woocommerce' ), + 'truncated_key' => __( 'Consumer key ending in', 'woocommerce' ), + 'user' => __( 'User', 'woocommerce' ), + 'permissions' => __( 'Permissions', 'woocommerce' ), + 'last_access' => __( 'Last access', 'woocommerce' ), + ); + } + + /** + * Column cb. + * + * @param array $key Key data. + * @return string + */ + public function column_cb( $key ) { + return sprintf( '', $key['key_id'] ); + } + + /** + * Return title column. + * + * @param array $key Key data. + * @return string + */ + public function column_title( $key ) { + $url = admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys&edit-key=' . $key['key_id'] ); + $user_id = intval( $key['user_id'] ); + + // Check if current user can edit other users or if it's the same user. + $can_edit = current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id; + + $output = ''; + if ( $can_edit ) { + $output .= ''; + } + if ( empty( $key['description'] ) ) { + $output .= esc_html__( 'API key', 'woocommerce' ); + } else { + $output .= esc_html( $key['description'] ); + } + if ( $can_edit ) { + $output .= ''; + } + $output .= ''; + + // Get actions. + $actions = array( + /* translators: %s: API key ID. */ + 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $key['key_id'] ), + ); + + if ( $can_edit ) { + $actions['edit'] = '' . __( 'View/Edit', 'woocommerce' ) . ''; + $actions['trash'] = '' . esc_html__( 'Revoke', 'woocommerce' ) . ''; + } + + $row_actions = array(); + + foreach ( $actions as $action => $link ) { + $row_actions[] = '' . $link . ''; + } + + $output .= '
    ' . implode( ' | ', $row_actions ) . '
    '; + + return $output; + } + + /** + * Return truncated consumer key column. + * + * @param array $key Key data. + * @return string + */ + public function column_truncated_key( $key ) { + return '…' . esc_html( $key['truncated_key'] ) . ''; + } + + /** + * Return user column. + * + * @param array $key Key data. + * @return string + */ + public function column_user( $key ) { + $user = get_user_by( 'id', $key['user_id'] ); + + if ( ! $user ) { + return ''; + } + + if ( current_user_can( 'edit_user', $user->ID ) ) { + return '' . esc_html( $user->display_name ) . ''; + } + + return esc_html( $user->display_name ); + } + + /** + * Return permissions column. + * + * @param array $key Key data. + * @return string + */ + public function column_permissions( $key ) { + $permission_key = $key['permissions']; + $permissions = array( + 'read' => __( 'Read', 'woocommerce' ), + 'write' => __( 'Write', 'woocommerce' ), + 'read_write' => __( 'Read/Write', 'woocommerce' ), + ); + + if ( isset( $permissions[ $permission_key ] ) ) { + return esc_html( $permissions[ $permission_key ] ); + } else { + return ''; + } + } + + /** + * Return last access column. + * + * @param array $key Key data. + * @return string + */ + public function column_last_access( $key ) { + if ( ! empty( $key['last_access'] ) ) { + /* translators: 1: last access date 2: last access time */ + $date = sprintf( __( '%1$s at %2$s', 'woocommerce' ), date_i18n( wc_date_format(), strtotime( $key['last_access'] ) ), date_i18n( wc_time_format(), strtotime( $key['last_access'] ) ) ); + + return apply_filters( 'woocommerce_api_key_last_access_datetime', $date, $key['last_access'] ); + } + + return __( 'Unknown', 'woocommerce' ); + } + + /** + * Get bulk actions. + * + * @return array + */ + protected function get_bulk_actions() { + if ( ! current_user_can( 'remove_users' ) ) { + return array(); + } + + return array( + 'revoke' => __( 'Revoke', 'woocommerce' ), + ); + } + + /** + * Search box. + * + * @param string $text Button text. + * @param string $input_id Input ID. + */ + public function search_box( $text, $input_id ) { + if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok. + return; + } + + $input_id = $input_id . '-search-input'; + $search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok. + + echo ''; + } + + /** + * Prepare table list items. + */ + public function prepare_items() { + global $wpdb; + + $per_page = $this->get_items_per_page( 'woocommerce_keys_per_page' ); + $current_page = $this->get_pagenum(); + + if ( 1 < $current_page ) { + $offset = $per_page * ( $current_page - 1 ); + } else { + $offset = 0; + } + + $search = ''; + + if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok. + $search = "AND description LIKE '%" . esc_sql( $wpdb->esc_like( wc_clean( wp_unslash( $_REQUEST['s'] ) ) ) ) . "%' "; // WPCS: input var okay, CSRF ok. + } + + // Get the API keys. + $keys = $wpdb->get_results( + "SELECT key_id, user_id, description, permissions, truncated_key, last_access FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search}" . + $wpdb->prepare( 'ORDER BY key_id DESC LIMIT %d OFFSET %d;', $per_page, $offset ), + ARRAY_A + ); // WPCS: unprepared SQL ok. + + $count = $wpdb->get_var( "SELECT COUNT(key_id) FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1 {$search};" ); // WPCS: unprepared SQL ok. + + $this->items = $keys; + + // Set the pagination. + $this->set_pagination_args( + array( + 'total_items' => $count, + 'per_page' => $per_page, + 'total_pages' => ceil( $count / $per_page ), + ) + ); + } +} diff --git a/includes/admin/class-wc-admin-api-keys.php b/includes/admin/class-wc-admin-api-keys.php new file mode 100644 index 0000000..6c44032 --- /dev/null +++ b/includes/admin/class-wc-admin-api-keys.php @@ -0,0 +1,274 @@ +is_api_keys_settings_page() ) { // WPCS: input var okay, CSRF ok. + $keys_table_list = new WC_Admin_API_Keys_Table_List(); + + // Add screen option. + add_screen_option( + 'per_page', + array( + 'default' => 10, + 'option' => 'woocommerce_keys_per_page', + ) + ); + } + } + + /** + * Table list output. + */ + private static function table_list_output() { + global $wpdb, $keys_table_list; + + echo '

    ' . esc_html__( 'REST API', 'woocommerce' ) . ' ' . esc_html__( 'Add key', 'woocommerce' ) . '

    '; + + // Get the API keys count. + $count = $wpdb->get_var( "SELECT COUNT(key_id) FROM {$wpdb->prefix}woocommerce_api_keys WHERE 1 = 1;" ); + + if ( absint( $count ) && $count > 0 ) { + $keys_table_list->prepare_items(); + + echo ''; + echo ''; + echo ''; + + $keys_table_list->views(); + $keys_table_list->search_box( __( 'Search key', 'woocommerce' ), 'key' ); + $keys_table_list->display(); + } else { + echo '
    '; + ?> +

    + + + 0, + 'user_id' => '', + 'description' => '', + 'permissions' => '', + 'truncated_key' => '', + 'last_access' => '', + ); + + if ( 0 === $key_id ) { + return $empty; + } + + $key = $wpdb->get_row( + $wpdb->prepare( + "SELECT key_id, user_id, description, permissions, truncated_key, last_access + FROM {$wpdb->prefix}woocommerce_api_keys + WHERE key_id = %d", + $key_id + ), + ARRAY_A + ); + + if ( is_null( $key ) ) { + return $empty; + } + + return $key; + } + + /** + * API Keys admin actions. + */ + public function actions() { + if ( $this->is_api_keys_settings_page() ) { + // Revoke key. + if ( isset( $_REQUEST['revoke-key'] ) ) { // WPCS: input var okay, CSRF ok. + $this->revoke_key(); + } + + // Bulk actions. + if ( isset( $_REQUEST['action'] ) && isset( $_REQUEST['key'] ) ) { // WPCS: input var okay, CSRF ok. + $this->bulk_actions(); + } + } + } + + /** + * Notices. + */ + public static function notices() { + if ( isset( $_GET['revoked'] ) ) { // WPCS: input var okay, CSRF ok. + $revoked = absint( $_GET['revoked'] ); // WPCS: input var okay, CSRF ok. + + /* translators: %d: count */ + WC_Admin_Settings::add_message( sprintf( _n( '%d API key permanently revoked.', '%d API keys permanently revoked.', $revoked, 'woocommerce' ), $revoked ) ); + } + } + + /** + * Revoke key. + */ + private function revoke_key() { + global $wpdb; + + check_admin_referer( 'revoke' ); + + if ( isset( $_REQUEST['revoke-key'] ) ) { // WPCS: input var okay, CSRF ok. + $key_id = absint( $_REQUEST['revoke-key'] ); // WPCS: input var okay, CSRF ok. + $user_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT user_id FROM {$wpdb->prefix}woocommerce_api_keys WHERE key_id = %d", $key_id ) ); + + if ( $key_id && $user_id && ( current_user_can( 'edit_user', $user_id ) || get_current_user_id() === $user_id ) ) { + $this->remove_key( $key_id ); + } else { + wp_die( esc_html__( 'You do not have permission to revoke this API Key', 'woocommerce' ) ); + } + } + + wp_safe_redirect( esc_url_raw( add_query_arg( array( 'revoked' => 1 ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ) ) ); + exit(); + } + + /** + * Bulk actions. + */ + private function bulk_actions() { + check_admin_referer( 'woocommerce-settings' ); + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( esc_html__( 'You do not have permission to edit API Keys', 'woocommerce' ) ); + } + + if ( isset( $_REQUEST['action'] ) ) { // WPCS: input var okay, CSRF ok. + $action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) ); // WPCS: input var okay, CSRF ok. + $keys = isset( $_REQUEST['key'] ) ? array_map( 'absint', (array) $_REQUEST['key'] ) : array(); // WPCS: input var okay, CSRF ok. + + if ( 'revoke' === $action ) { + $this->bulk_revoke_key( $keys ); + } + } + } + + /** + * Bulk revoke key. + * + * @param array $keys API Keys. + */ + private function bulk_revoke_key( $keys ) { + if ( ! current_user_can( 'remove_users' ) ) { + wp_die( esc_html__( 'You do not have permission to revoke API Keys', 'woocommerce' ) ); + } + + $qty = 0; + foreach ( $keys as $key_id ) { + $result = $this->remove_key( $key_id ); + + if ( $result ) { + $qty++; + } + } + + // Redirect to webhooks page. + wp_safe_redirect( esc_url_raw( add_query_arg( array( 'revoked' => $qty ), admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=keys' ) ) ) ); + exit(); + } + + /** + * Remove key. + * + * @param int $key_id API Key ID. + * @return bool + */ + private function remove_key( $key_id ) { + global $wpdb; + + $delete = $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', array( 'key_id' => $key_id ), array( '%d' ) ); + + return $delete; + } +} + +new WC_Admin_API_Keys(); diff --git a/includes/admin/class-wc-admin-assets.php b/includes/admin/class-wc-admin-assets.php new file mode 100644 index 0000000..22222f2 --- /dev/null +++ b/includes/admin/class-wc-admin-assets.php @@ -0,0 +1,497 @@ +id : ''; + + // Register admin styles. + wp_register_style( 'woocommerce_admin_menu_styles', WC()->plugin_url() . '/assets/css/menu.css', array(), $version ); + wp_register_style( 'woocommerce_admin_styles', WC()->plugin_url() . '/assets/css/admin.css', array(), $version ); + wp_register_style( 'jquery-ui-style', WC()->plugin_url() . '/assets/css/jquery-ui/jquery-ui.min.css', array(), $version ); + wp_register_style( 'woocommerce_admin_dashboard_styles', WC()->plugin_url() . '/assets/css/dashboard.css', array(), $version ); + wp_register_style( 'woocommerce_admin_print_reports_styles', WC()->plugin_url() . '/assets/css/reports-print.css', array(), $version, 'print' ); + wp_register_style( 'woocommerce_admin_marketplace_styles', WC()->plugin_url() . '/assets/css/marketplace-suggestions.css', array(), $version ); + wp_register_style( 'woocommerce_admin_privacy_styles', WC()->plugin_url() . '/assets/css/privacy.css', array(), $version ); + + // Add RTL support for admin styles. + wp_style_add_data( 'woocommerce_admin_menu_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_dashboard_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_print_reports_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_marketplace_styles', 'rtl', 'replace' ); + wp_style_add_data( 'woocommerce_admin_privacy_styles', 'rtl', 'replace' ); + + if ( $screen && $screen->is_block_editor() ) { + wp_register_style( 'woocommerce-general', WC()->plugin_url() . '/assets/css/woocommerce.css', array(), $version ); + wp_style_add_data( 'woocommerce-general', 'rtl', 'replace' ); + } + + // Sitewide menu CSS. + wp_enqueue_style( 'woocommerce_admin_menu_styles' ); + + // Admin styles for WC pages only. + if ( in_array( $screen_id, wc_get_screen_ids() ) ) { + wp_enqueue_style( 'woocommerce_admin_styles' ); + wp_enqueue_style( 'jquery-ui-style' ); + wp_enqueue_style( 'wp-color-picker' ); + } + + if ( in_array( $screen_id, array( 'dashboard' ) ) ) { + wp_enqueue_style( 'woocommerce_admin_dashboard_styles' ); + } + + if ( in_array( $screen_id, array( 'woocommerce_page_wc-reports', 'toplevel_page_wc-reports' ) ) ) { + wp_enqueue_style( 'woocommerce_admin_print_reports_styles' ); + } + + // Privacy Policy Guide css for back-compat. + if ( isset( $_GET['wp-privacy-policy-guide'] ) || in_array( $screen_id, array( 'privacy-policy-guide' ) ) ) { + wp_enqueue_style( 'woocommerce_admin_privacy_styles' ); + } + + // @deprecated 2.3. + if ( has_action( 'woocommerce_admin_css' ) ) { + do_action( 'woocommerce_admin_css' ); + wc_deprecated_function( 'The woocommerce_admin_css action', '2.3', 'admin_enqueue_scripts' ); + } + + if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) { + wp_enqueue_style( 'woocommerce_admin_marketplace_styles' ); + } + } + + + /** + * Enqueue scripts. + */ + public function admin_scripts() { + global $wp_query, $post; + + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + // Register scripts. + wp_register_script( 'woocommerce_admin', WC()->plugin_url() . '/assets/js/admin/woocommerce_admin' . $suffix . '.js', array( 'jquery', 'jquery-blockui', 'jquery-ui-sortable', 'jquery-ui-widget', 'jquery-ui-core', 'jquery-tiptip' ), $version ); + wp_register_script( 'jquery-blockui', WC()->plugin_url() . '/assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js', array( 'jquery' ), '2.70', true ); + wp_register_script( 'jquery-tiptip', WC()->plugin_url() . '/assets/js/jquery-tiptip/jquery.tipTip' . $suffix . '.js', array( 'jquery' ), $version, true ); + wp_register_script( 'round', WC()->plugin_url() . '/assets/js/round/round' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'wc-admin-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker', 'jquery-ui-sortable', 'accounting', 'round', 'wc-enhanced-select', 'plupload-all', 'stupidtable', 'jquery-tiptip' ), $version ); + wp_register_script( 'zeroclipboard', WC()->plugin_url() . '/assets/js/zeroclipboard/jquery.zeroclipboard' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'qrcode', WC()->plugin_url() . '/assets/js/jquery-qrcode/jquery.qrcode' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'stupidtable', WC()->plugin_url() . '/assets/js/stupidtable/stupidtable' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'serializejson', WC()->plugin_url() . '/assets/js/jquery-serializejson/jquery.serializejson' . $suffix . '.js', array( 'jquery' ), '2.8.1' ); + wp_register_script( 'flot', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'flot-resize', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.resize' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); + wp_register_script( 'flot-time', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.time' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); + wp_register_script( 'flot-pie', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.pie' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); + wp_register_script( 'flot-stack', WC()->plugin_url() . '/assets/js/jquery-flot/jquery.flot.stack' . $suffix . '.js', array( 'jquery', 'flot' ), $version ); + wp_register_script( 'wc-settings-tax', WC()->plugin_url() . '/assets/js/admin/settings-views-html-settings-tax' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version ); + wp_register_script( 'wc-backbone-modal', WC()->plugin_url() . '/assets/js/admin/backbone-modal' . $suffix . '.js', array( 'underscore', 'backbone', 'wp-util' ), $version ); + wp_register_script( 'wc-shipping-zones', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zones' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-enhanced-select', 'wc-backbone-modal' ), $version ); + wp_register_script( 'wc-shipping-zone-methods', WC()->plugin_url() . '/assets/js/admin/wc-shipping-zone-methods' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-ui-sortable', 'wc-backbone-modal' ), $version ); + wp_register_script( 'wc-shipping-classes', WC()->plugin_url() . '/assets/js/admin/wc-shipping-classes' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone' ), $version ); + wp_register_script( 'wc-clipboard', WC()->plugin_url() . '/assets/js/admin/wc-clipboard' . $suffix . '.js', array( 'jquery' ), $version ); + wp_register_script( 'select2', WC()->plugin_url() . '/assets/js/select2/select2.full' . $suffix . '.js', array( 'jquery' ), '4.0.3' ); + wp_register_script( 'selectWoo', WC()->plugin_url() . '/assets/js/selectWoo/selectWoo.full' . $suffix . '.js', array( 'jquery' ), '1.0.6' ); + wp_register_script( 'wc-enhanced-select', WC()->plugin_url() . '/assets/js/admin/wc-enhanced-select' . $suffix . '.js', array( 'jquery', 'selectWoo' ), $version ); + wp_register_script( 'js-cookie', WC()->plugin_url() . '/assets/js/js-cookie/js.cookie' . $suffix . '.js', array(), '2.1.4', true ); + + wp_localize_script( + 'wc-enhanced-select', + 'wc_enhanced_select_params', + array( + 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), + 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), + 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), + 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'search_products_nonce' => wp_create_nonce( 'search-products' ), + 'search_customers_nonce' => wp_create_nonce( 'search-customers' ), + 'search_categories_nonce' => wp_create_nonce( 'search-categories' ), + 'search_pages_nonce' => wp_create_nonce( 'search-pages' ), + ) + ); + + wp_register_script( 'accounting', WC()->plugin_url() . '/assets/js/accounting/accounting' . $suffix . '.js', array( 'jquery' ), '0.4.2' ); + wp_localize_script( + 'accounting', + 'accounting_params', + array( + 'mon_decimal_point' => wc_get_price_decimal_separator(), + ) + ); + + wp_register_script( 'wc-orders', WC()->plugin_url() . '/assets/js/admin/wc-orders' . $suffix . '.js', array( 'jquery', 'wp-util', 'underscore', 'backbone', 'jquery-blockui' ), $version ); + wp_localize_script( + 'wc-orders', + 'wc_orders_params', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'preview_nonce' => wp_create_nonce( 'woocommerce-preview-order' ), + ) + ); + + // WooCommerce admin pages. + if ( in_array( $screen_id, wc_get_screen_ids() ) ) { + wp_enqueue_script( 'iris' ); + wp_enqueue_script( 'woocommerce_admin' ); + wp_enqueue_script( 'wc-enhanced-select' ); + wp_enqueue_script( 'jquery-ui-sortable' ); + wp_enqueue_script( 'jquery-ui-autocomplete' ); + + $locale = localeconv(); + $decimal = isset( $locale['decimal_point'] ) ? $locale['decimal_point'] : '.'; + + $params = array( + /* translators: %s: decimal */ + 'i18n_decimal_error' => sprintf( __( 'Please enter with one decimal point (%s) without thousand separators.', 'woocommerce' ), $decimal ), + /* translators: %s: price decimal separator */ + 'i18n_mon_decimal_error' => sprintf( __( 'Please enter with one monetary decimal point (%s) without thousand separators and currency symbols.', 'woocommerce' ), wc_get_price_decimal_separator() ), + 'i18n_country_iso_error' => __( 'Please enter in country code with two capital letters.', 'woocommerce' ), + 'i18n_sale_less_than_regular_error' => __( 'Please enter in a value less than the regular price.', 'woocommerce' ), + 'i18n_delete_product_notice' => __( 'This product has produced sales and may be linked to existing orders. Are you sure you want to delete it?', 'woocommerce' ), + 'i18n_remove_personal_data_notice' => __( 'This action cannot be reversed. Are you sure you wish to erase personal data from the selected orders?', 'woocommerce' ), + 'decimal_point' => $decimal, + 'mon_decimal_point' => wc_get_price_decimal_separator(), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'strings' => array( + 'import_products' => __( 'Import', 'woocommerce' ), + 'export_products' => __( 'Export', 'woocommerce' ), + ), + 'nonces' => array( + 'gateway_toggle' => wp_create_nonce( 'woocommerce-toggle-payment-gateway-enabled' ), + ), + 'urls' => array( + 'import_products' => current_user_can( 'import' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_importer' ) ) : null, + 'export_products' => current_user_can( 'export' ) ? esc_url_raw( admin_url( 'edit.php?post_type=product&page=product_exporter' ) ) : null, + ), + ); + + wp_localize_script( 'woocommerce_admin', 'woocommerce_admin', $params ); + } + + // Edit product category pages. + if ( in_array( $screen_id, array( 'edit-product_cat' ) ) ) { + wp_enqueue_media(); + } + + // Products. + if ( in_array( $screen_id, array( 'edit-product' ) ) ) { + wp_enqueue_script( 'woocommerce_quick-edit', WC()->plugin_url() . '/assets/js/admin/quick-edit' . $suffix . '.js', array( 'jquery', 'woocommerce_admin' ), $version ); + + $params = array( + 'strings' => array( + 'allow_reviews' => esc_js( __( 'Enable reviews', 'woocommerce' ) ), + ), + ); + + wp_localize_script( 'woocommerce_quick-edit', 'woocommerce_quick_edit', $params ); + } + + // Meta boxes. + if ( in_array( $screen_id, array( 'product', 'edit-product' ) ) ) { + wp_enqueue_media(); + wp_register_script( 'wc-admin-product-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'media-models' ), $version ); + wp_register_script( 'wc-admin-variation-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-product-variation' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'serializejson', 'media-models' ), $version ); + + wp_enqueue_script( 'wc-admin-product-meta-boxes' ); + wp_enqueue_script( 'wc-admin-variation-meta-boxes' ); + + $params = array( + 'post_id' => isset( $post->ID ) ? $post->ID : '', + 'plugin_url' => WC()->plugin_url(), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'woocommerce_placeholder_img_src' => wc_placeholder_img_src(), + 'add_variation_nonce' => wp_create_nonce( 'add-variation' ), + 'link_variation_nonce' => wp_create_nonce( 'link-variations' ), + 'delete_variations_nonce' => wp_create_nonce( 'delete-variations' ), + 'load_variations_nonce' => wp_create_nonce( 'load-variations' ), + 'save_variations_nonce' => wp_create_nonce( 'save-variations' ), + 'bulk_edit_variations_nonce' => wp_create_nonce( 'bulk-edit-variations' ), + /* translators: %d: Number of variations */ + 'i18n_link_all_variations' => esc_js( sprintf( __( 'Are you sure you want to link all variations? This will create a new variation for each and every possible combination of variation attributes (max %d per run).', 'woocommerce' ), Constants::is_defined( 'WC_MAX_LINKED_VARIATIONS' ) ? Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) : 50 ) ), + 'i18n_enter_a_value' => esc_js( __( 'Enter a value', 'woocommerce' ) ), + 'i18n_enter_menu_order' => esc_js( __( 'Variation menu order (determines position in the list of variations)', 'woocommerce' ) ), + 'i18n_enter_a_value_fixed_or_percent' => esc_js( __( 'Enter a value (fixed or %)', 'woocommerce' ) ), + 'i18n_delete_all_variations' => esc_js( __( 'Are you sure you want to delete all variations? This cannot be undone.', 'woocommerce' ) ), + 'i18n_last_warning' => esc_js( __( 'Last warning, are you sure?', 'woocommerce' ) ), + 'i18n_choose_image' => esc_js( __( 'Choose an image', 'woocommerce' ) ), + 'i18n_set_image' => esc_js( __( 'Set variation image', 'woocommerce' ) ), + 'i18n_variation_added' => esc_js( __( 'variation added', 'woocommerce' ) ), + 'i18n_variations_added' => esc_js( __( 'variations added', 'woocommerce' ) ), + 'i18n_no_variations_added' => esc_js( __( 'No variations added', 'woocommerce' ) ), + 'i18n_remove_variation' => esc_js( __( 'Are you sure you want to remove this variation?', 'woocommerce' ) ), + 'i18n_scheduled_sale_start' => esc_js( __( 'Sale start date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ), + 'i18n_scheduled_sale_end' => esc_js( __( 'Sale end date (YYYY-MM-DD format or leave blank)', 'woocommerce' ) ), + 'i18n_edited_variations' => esc_js( __( 'Save changes before changing page?', 'woocommerce' ) ), + 'i18n_variation_count_single' => esc_js( __( '%qty% variation', 'woocommerce' ) ), + 'i18n_variation_count_plural' => esc_js( __( '%qty% variations', 'woocommerce' ) ), + 'variations_per_page' => absint( apply_filters( 'woocommerce_admin_meta_boxes_variations_per_page', 15 ) ), + ); + + wp_localize_script( 'wc-admin-variation-meta-boxes', 'woocommerce_admin_meta_boxes_variations', $params ); + } + if ( in_array( str_replace( 'edit-', '', $screen_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) { + $default_location = wc_get_customer_default_location(); + + wp_enqueue_script( 'wc-admin-order-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-order' . $suffix . '.js', array( 'wc-admin-meta-boxes', 'wc-backbone-modal', 'selectWoo', 'wc-clipboard' ), $version ); + wp_localize_script( + 'wc-admin-order-meta-boxes', + 'woocommerce_admin_meta_boxes_order', + array( + 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), + 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), + 'default_country' => isset( $default_location['country'] ) ? $default_location['country'] : '', + 'default_state' => isset( $default_location['state'] ) ? $default_location['state'] : '', + 'placeholder_name' => esc_attr__( 'Name (required)', 'woocommerce' ), + 'placeholder_value' => esc_attr__( 'Value (required)', 'woocommerce' ), + ) + ); + } + if ( in_array( $screen_id, array( 'shop_coupon', 'edit-shop_coupon' ) ) ) { + wp_enqueue_script( 'wc-admin-coupon-meta-boxes', WC()->plugin_url() . '/assets/js/admin/meta-boxes-coupon' . $suffix . '.js', array( 'wc-admin-meta-boxes' ), $version ); + wp_localize_script( + 'wc-admin-coupon-meta-boxes', + 'woocommerce_admin_meta_boxes_coupon', + array( + 'generate_button_text' => esc_html__( 'Generate coupon code', 'woocommerce' ), + 'characters' => apply_filters( 'woocommerce_coupon_code_generator_characters', 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' ), + 'char_length' => apply_filters( 'woocommerce_coupon_code_generator_character_length', 8 ), + 'prefix' => apply_filters( 'woocommerce_coupon_code_generator_prefix', '' ), + 'suffix' => apply_filters( 'woocommerce_coupon_code_generator_suffix', '' ), + ) + ); + } + if ( in_array( str_replace( 'edit-', '', $screen_id ), array_merge( array( 'shop_coupon', 'product' ), wc_get_order_types( 'order-meta-boxes' ) ) ) ) { + $post_id = isset( $post->ID ) ? $post->ID : ''; + $currency = ''; + $remove_item_notice = __( 'Are you sure you want to remove the selected items?', 'woocommerce' ); + $remove_fee_notice = __( 'Are you sure you want to remove the selected fees?', 'woocommerce' ); + $remove_shipping_notice = __( 'Are you sure you want to remove the selected shipping?', 'woocommerce' ); + + if ( $post_id && in_array( get_post_type( $post_id ), wc_get_order_types( 'order-meta-boxes' ) ) ) { + $order = wc_get_order( $post_id ); + if ( $order ) { + $currency = $order->get_currency(); + + if ( ! $order->has_status( array( 'pending', 'failed', 'cancelled' ) ) ) { + $remove_item_notice = $remove_item_notice . ' ' . __( "You may need to manually restore the item's stock.", 'woocommerce' ); + } + } + } + + $params = array( + 'remove_item_notice' => $remove_item_notice, + 'remove_fee_notice' => $remove_fee_notice, + 'remove_shipping_notice' => $remove_shipping_notice, + 'i18n_select_items' => __( 'Please select some items.', 'woocommerce' ), + 'i18n_do_refund' => __( 'Are you sure you wish to process this refund? This action cannot be undone.', 'woocommerce' ), + 'i18n_delete_refund' => __( 'Are you sure you wish to delete this refund? This action cannot be undone.', 'woocommerce' ), + 'i18n_delete_tax' => __( 'Are you sure you wish to delete this tax column? This action cannot be undone.', 'woocommerce' ), + 'remove_item_meta' => __( 'Remove this item meta?', 'woocommerce' ), + 'remove_attribute' => __( 'Remove this attribute?', 'woocommerce' ), + 'name_label' => __( 'Name', 'woocommerce' ), + 'remove_label' => __( 'Remove', 'woocommerce' ), + 'click_to_toggle' => __( 'Click to toggle', 'woocommerce' ), + 'values_label' => __( 'Value(s)', 'woocommerce' ), + 'text_attribute_tip' => __( 'Enter some text, or some attributes by pipe (|) separating values.', 'woocommerce' ), + 'visible_label' => __( 'Visible on the product page', 'woocommerce' ), + 'used_for_variations_label' => __( 'Used for variations', 'woocommerce' ), + 'new_attribute_prompt' => __( 'Enter a name for the new attribute term:', 'woocommerce' ), + 'calc_totals' => __( 'Recalculate totals? This will calculate taxes based on the customers country (or the store base country) and update totals.', 'woocommerce' ), + 'copy_billing' => __( 'Copy billing information to shipping information? This will remove any currently entered shipping information.', 'woocommerce' ), + 'load_billing' => __( "Load the customer's billing information? This will remove any currently entered billing information.", 'woocommerce' ), + 'load_shipping' => __( "Load the customer's shipping information? This will remove any currently entered shipping information.", 'woocommerce' ), + 'featured_label' => __( 'Featured', 'woocommerce' ), + 'prices_include_tax' => esc_attr( get_option( 'woocommerce_prices_include_tax' ) ), + 'tax_based_on' => esc_attr( get_option( 'woocommerce_tax_based_on' ) ), + 'round_at_subtotal' => esc_attr( get_option( 'woocommerce_tax_round_at_subtotal' ) ), + 'no_customer_selected' => __( 'No customer selected', 'woocommerce' ), + 'plugin_url' => WC()->plugin_url(), + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'order_item_nonce' => wp_create_nonce( 'order-item' ), + 'add_attribute_nonce' => wp_create_nonce( 'add-attribute' ), + 'save_attributes_nonce' => wp_create_nonce( 'save-attributes' ), + 'calc_totals_nonce' => wp_create_nonce( 'calc-totals' ), + 'get_customer_details_nonce' => wp_create_nonce( 'get-customer-details' ), + 'search_products_nonce' => wp_create_nonce( 'search-products' ), + 'grant_access_nonce' => wp_create_nonce( 'grant-access' ), + 'revoke_access_nonce' => wp_create_nonce( 'revoke-access' ), + 'add_order_note_nonce' => wp_create_nonce( 'add-order-note' ), + 'delete_order_note_nonce' => wp_create_nonce( 'delete-order-note' ), + 'calendar_image' => WC()->plugin_url() . '/assets/images/calendar.png', + 'post_id' => isset( $post->ID ) ? $post->ID : '', + 'base_country' => WC()->countries->get_base_country(), + 'currency_format_num_decimals' => wc_get_price_decimals(), + 'currency_format_symbol' => get_woocommerce_currency_symbol( $currency ), + 'currency_format_decimal_sep' => esc_attr( wc_get_price_decimal_separator() ), + 'currency_format_thousand_sep' => esc_attr( wc_get_price_thousand_separator() ), + 'currency_format' => esc_attr( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ), // For accounting JS. + 'rounding_precision' => wc_get_rounding_precision(), + 'tax_rounding_mode' => wc_get_tax_rounding_mode(), + 'product_types' => array_unique( array_merge( array( 'simple', 'grouped', 'variable', 'external' ), array_keys( wc_get_product_types() ) ) ), + 'i18n_download_permission_fail' => __( 'Could not grant access - the user may already have permission for this file or billing email is not set. Ensure the billing email is set, and the order has been saved.', 'woocommerce' ), + 'i18n_permission_revoke' => __( 'Are you sure you want to revoke access to this download?', 'woocommerce' ), + 'i18n_tax_rate_already_exists' => __( 'You cannot add the same tax rate twice!', 'woocommerce' ), + 'i18n_delete_note' => __( 'Are you sure you wish to delete this note? This action cannot be undone.', 'woocommerce' ), + 'i18n_apply_coupon' => __( 'Enter a coupon code to apply. Discounts are applied to line totals, before taxes.', 'woocommerce' ), + 'i18n_add_fee' => __( 'Enter a fixed amount or percentage to apply as a fee.', 'woocommerce' ), + ); + + wp_localize_script( 'wc-admin-meta-boxes', 'woocommerce_admin_meta_boxes', $params ); + } + + // Term ordering - only when sorting by term_order. + if ( ( strstr( $screen_id, 'edit-pa_' ) || ( ! empty( $_GET['taxonomy'] ) && in_array( wp_unslash( $_GET['taxonomy'] ), apply_filters( 'woocommerce_sortable_taxonomies', array( 'product_cat' ) ) ) ) ) && ! isset( $_GET['orderby'] ) ) { + + wp_register_script( 'woocommerce_term_ordering', WC()->plugin_url() . '/assets/js/admin/term-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version ); + wp_enqueue_script( 'woocommerce_term_ordering' ); + + $taxonomy = isset( $_GET['taxonomy'] ) ? wc_clean( wp_unslash( $_GET['taxonomy'] ) ) : ''; + + $woocommerce_term_order_params = array( + 'taxonomy' => $taxonomy, + ); + + wp_localize_script( 'woocommerce_term_ordering', 'woocommerce_term_ordering_params', $woocommerce_term_order_params ); + } + + // Product sorting - only when sorting by menu order on the products page. + if ( current_user_can( 'edit_others_pages' ) && 'edit-product' === $screen_id && isset( $wp_query->query['orderby'] ) && 'menu_order title' === $wp_query->query['orderby'] ) { + wp_register_script( 'woocommerce_product_ordering', WC()->plugin_url() . '/assets/js/admin/product-ordering' . $suffix . '.js', array( 'jquery-ui-sortable' ), $version, true ); + wp_enqueue_script( 'woocommerce_product_ordering' ); + } + + // Reports Pages. + if ( in_array( $screen_id, apply_filters( 'woocommerce_reports_screen_ids', array( $wc_screen_id . '_page_wc-reports', 'toplevel_page_wc-reports', 'dashboard' ) ) ) ) { + wp_register_script( 'wc-reports', WC()->plugin_url() . '/assets/js/admin/reports' . $suffix . '.js', array( 'jquery', 'jquery-ui-datepicker' ), $version ); + + wp_enqueue_script( 'wc-reports' ); + wp_enqueue_script( 'flot' ); + wp_enqueue_script( 'flot-resize' ); + wp_enqueue_script( 'flot-time' ); + wp_enqueue_script( 'flot-pie' ); + wp_enqueue_script( 'flot-stack' ); + } + + // API settings. + if ( $wc_screen_id . '_page_wc-settings' === $screen_id && isset( $_GET['section'] ) && 'keys' == $_GET['section'] ) { + wp_register_script( 'wc-api-keys', WC()->plugin_url() . '/assets/js/admin/api-keys' . $suffix . '.js', array( 'jquery', 'woocommerce_admin', 'underscore', 'backbone', 'wp-util', 'qrcode', 'wc-clipboard' ), $version, true ); + wp_enqueue_script( 'wc-api-keys' ); + wp_localize_script( + 'wc-api-keys', + 'woocommerce_admin_api_keys', + array( + 'ajax_url' => admin_url( 'admin-ajax.php' ), + 'update_api_nonce' => wp_create_nonce( 'update-api-key' ), + 'clipboard_failed' => esc_html__( 'Copying to clipboard failed. Please press Ctrl/Cmd+C to copy.', 'woocommerce' ), + ) + ); + } + + // System status. + if ( $wc_screen_id . '_page_wc-status' === $screen_id ) { + wp_register_script( 'wc-admin-system-status', WC()->plugin_url() . '/assets/js/admin/system-status' . $suffix . '.js', array( 'wc-clipboard' ), $version ); + wp_enqueue_script( 'wc-admin-system-status' ); + wp_localize_script( + 'wc-admin-system-status', + 'woocommerce_admin_system_status', + array( + 'delete_log_confirmation' => esc_js( __( 'Are you sure you want to delete this log?', 'woocommerce' ) ), + 'run_tool_confirmation' => esc_js( __( 'Are you sure you want to run this tool?', 'woocommerce' ) ), + ) + ); + } + + if ( in_array( $screen_id, array( 'user-edit', 'profile' ) ) ) { + wp_register_script( 'wc-users', WC()->plugin_url() . '/assets/js/admin/users' . $suffix . '.js', array( 'jquery', 'wc-enhanced-select', 'selectWoo' ), $version, true ); + wp_enqueue_script( 'wc-users' ); + wp_localize_script( + 'wc-users', + 'wc_users_params', + array( + 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), + 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), + ) + ); + } + + if ( WC_Marketplace_Suggestions::show_suggestions_for_screen( $screen_id ) ) { + $active_plugin_slugs = array_map( 'dirname', get_option( 'active_plugins' ) ); + wp_register_script( + 'marketplace-suggestions', + WC()->plugin_url() . '/assets/js/admin/marketplace-suggestions' . $suffix . '.js', + array( 'jquery', 'underscore', 'js-cookie' ), + $version, + true + ); + wp_localize_script( + 'marketplace-suggestions', + 'marketplace_suggestions', + array( + 'dismiss_suggestion_nonce' => wp_create_nonce( 'add_dismissed_marketplace_suggestion' ), + 'active_plugins' => $active_plugin_slugs, + 'dismissed_suggestions' => WC_Marketplace_Suggestions::get_dismissed_suggestions(), + 'suggestions_data' => WC_Marketplace_Suggestions::get_suggestions_api_data(), + 'manage_suggestions_url' => admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=woocommerce_com' ), + 'in_app_purchase_params' => WC_Admin_Addons::get_in_app_purchase_url_params(), + 'i18n_marketplace_suggestions_default_cta' + => esc_html__( 'Learn More', 'woocommerce' ), + 'i18n_marketplace_suggestions_dismiss_tooltip' + => esc_attr__( 'Dismiss this suggestion', 'woocommerce' ), + 'i18n_marketplace_suggestions_manage_suggestions' + => esc_html__( 'Manage suggestions', 'woocommerce' ), + ) + ); + wp_enqueue_script( 'marketplace-suggestions' ); + } + + } + + } + +endif; + +return new WC_Admin_Assets(); diff --git a/includes/admin/class-wc-admin-attributes.php b/includes/admin/class-wc-admin-attributes.php new file mode 100644 index 0000000..bd772f1 --- /dev/null +++ b/includes/admin/class-wc-admin-attributes.php @@ -0,0 +1,477 @@ +

    ' . wp_kses_post( $result->get_error_message() ) . '

    '; + } + + // Show admin interface. + if ( ! empty( $_GET['edit'] ) ) { + self::edit_attribute(); + } else { + self::add_attribute(); + } + } + + /** + * Get and sanitize posted attribute data. + * + * @return array + */ + private static function get_posted_attribute() { + $attribute = array( + 'attribute_label' => isset( $_POST['attribute_label'] ) ? wc_clean( wp_unslash( $_POST['attribute_label'] ) ) : '', // WPCS: input var ok, CSRF ok. + 'attribute_name' => isset( $_POST['attribute_name'] ) ? wc_sanitize_taxonomy_name( wp_unslash( $_POST['attribute_name'] ) ) : '', // WPCS: input var ok, CSRF ok, sanitization ok. + 'attribute_type' => isset( $_POST['attribute_type'] ) ? wc_clean( wp_unslash( $_POST['attribute_type'] ) ) : 'select', // WPCS: input var ok, CSRF ok. + 'attribute_orderby' => isset( $_POST['attribute_orderby'] ) ? wc_clean( wp_unslash( $_POST['attribute_orderby'] ) ) : '', // WPCS: input var ok, CSRF ok. + 'attribute_public' => isset( $_POST['attribute_public'] ) ? 1 : 0, // WPCS: input var ok, CSRF ok. + ); + + if ( empty( $attribute['attribute_type'] ) ) { + $attribute['attribute_type'] = 'select'; + } + if ( empty( $attribute['attribute_label'] ) ) { + $attribute['attribute_label'] = ucfirst( $attribute['attribute_name'] ); + } + if ( empty( $attribute['attribute_name'] ) ) { + $attribute['attribute_name'] = wc_sanitize_taxonomy_name( $attribute['attribute_label'] ); + } + + return $attribute; + } + + /** + * Add an attribute. + * + * @return bool|WP_Error + */ + private static function process_add_attribute() { + check_admin_referer( 'woocommerce-add-new_attribute' ); + + $attribute = self::get_posted_attribute(); + $args = array( + 'name' => $attribute['attribute_label'], + 'slug' => $attribute['attribute_name'], + 'type' => $attribute['attribute_type'], + 'order_by' => $attribute['attribute_orderby'], + 'has_archives' => $attribute['attribute_public'], + ); + + $id = wc_create_attribute( $args ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + return true; + } + + /** + * Edit an attribute. + * + * @return bool|WP_Error + */ + private static function process_edit_attribute() { + $attribute_id = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0; + check_admin_referer( 'woocommerce-save-attribute_' . $attribute_id ); + + $attribute = self::get_posted_attribute(); + $args = array( + 'name' => $attribute['attribute_label'], + 'slug' => $attribute['attribute_name'], + 'type' => $attribute['attribute_type'], + 'order_by' => $attribute['attribute_orderby'], + 'has_archives' => $attribute['attribute_public'], + ); + + $id = wc_update_attribute( $attribute_id, $args ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + self::$edited_attribute_id = $id; + + return true; + } + + /** + * Delete an attribute. + * + * @return bool + */ + private static function process_delete_attribute() { + $attribute_id = isset( $_GET['delete'] ) ? absint( $_GET['delete'] ) : 0; + check_admin_referer( 'woocommerce-delete-attribute_' . $attribute_id ); + + return wc_delete_attribute( $attribute_id ); + } + + /** + * Edit Attribute admin panel. + * + * Shows the interface for changing an attributes type between select and text. + */ + public static function edit_attribute() { + global $wpdb; + + $edit = isset( $_GET['edit'] ) ? absint( $_GET['edit'] ) : 0; + + $attribute_to_edit = $wpdb->get_row( + $wpdb->prepare( + " + SELECT attribute_type, attribute_label, attribute_name, attribute_orderby, attribute_public + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d + ", + $edit + ) + ); + + ?> +
    +

    + +

    ' . esc_html__( 'Error: non-existing attribute ID.', 'woocommerce' ) . '

    '; + } else { + if ( self::$edited_attribute_id > 0 ) { + echo '

    ' . esc_html__( 'Attribute updated successfully', 'woocommerce' ) . '

    ' . esc_html__( 'Back to Attributes', 'woocommerce' ) . '

    '; + self::$edited_attribute_id = null; + } + $att_type = $attribute_to_edit->attribute_type; + $att_label = format_to_edit( $attribute_to_edit->attribute_label ); + $att_name = $attribute_to_edit->attribute_name; + $att_orderby = $attribute_to_edit->attribute_orderby; + $att_public = $attribute_to_edit->attribute_public; + ?> +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + +

    +
    + + + +

    +
    + + + /> +

    +
    + + + +

    +
    + + + +

    +
    +

    + +
    + + + +
    +

    + +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + attribute_label ); ?> + +
    |
    +
    attribute_name ); ?>attribute_type ) ); ?> attribute_public ? esc_html__( '(Public)', 'woocommerce' ) : ''; ?> + attribute_orderby ) { + case 'name': + esc_html_e( 'Name', 'woocommerce' ); + break; + case 'name_num': + esc_html_e( 'Name (numeric)', 'woocommerce' ); + break; + case 'id': + esc_html_e( 'Term ID', 'woocommerce' ); + break; + default: + esc_html_e( 'Custom ordering', 'woocommerce' ); + break; + } + ?> + + attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'hide_empty=0' ); + $terms_string = implode( ', ', wp_list_pluck( $terms, 'name' ) ); + if ( $terms_string ) { + echo esc_html( $terms_string ); + } else { + echo ''; + } + } else { + echo ''; + } + ?> +
    +
    +
    +
    +
    +
    +
    +

    +

    +
    + + +
    + + +

    +
    + +
    + + +

    +
    + +
    + + +

    +
    + + +
    + + +

    +
    + + +
    + + +

    +
    + + + +

    + +
    +
    +
    +
    +
    + +
    + __( '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-setup.php b/includes/admin/class-wc-admin-dashboard-setup.php new file mode 100644 index 0000000..4360241 --- /dev/null +++ b/includes/admin/class-wc-admin-dashboard-setup.php @@ -0,0 +1,213 @@ + array( + 'completed' => false, + 'button_link' => 'admin.php?page=wc-admin&path=%2Fsetup-wizard', + ), + 'products' => array( + 'completed' => false, + 'button_link' => 'admin.php?page=wc-admin&task=products', + ), + 'woocommerce-payments' => array( + 'completed' => false, + 'button_link' => 'admin.php?page=wc-admin&path=%2Fpayments%2Fconnect', + ), + 'payments' => array( + 'completed' => false, + 'button_link' => 'admin.php?page=wc-admin&task=payments', + ), + 'tax' => array( + 'completed' => false, + 'button_link' => 'admin.php?page=wc-admin&task=tax', + ), + 'shipping' => array( + 'completed' => false, + 'button_link' => 'admin.php?page=wc-admin&task=shipping', + ), + 'appearance' => array( + 'completed' => false, + 'button_link' => 'admin.php?page=wc-admin&task=appearance', + ), + ); + + /** + * # of completed tasks. + * + * @var int + */ + private $completed_tasks_count = 0; + + /** + * WC_Admin_Dashboard_Setup constructor. + */ + public function __construct() { + if ( $this->should_display_widget() ) { + $this->populate_general_tasks(); + $this->populate_payment_tasks(); + $this->completed_tasks_count = $this->get_completed_tasks_count(); + add_meta_box( + 'wc_admin_dashboard_setup', + __( 'WooCommerce Setup', 'woocommerce' ), + array( $this, 'render' ), + 'dashboard', + 'normal', + 'high' + ); + } + } + + /** + * Render meta box output. + */ + public function render() { + $version = Constants::get_constant( 'WC_VERSION' ); + wp_enqueue_style( 'wc-dashboard-setup', WC()->plugin_url() . '/assets/css/dashboard-setup.css', array(), $version ); + + $task = $this->get_next_task(); + if ( ! $task ) { + return; + } + + $button_link = $task['button_link']; + $completed_tasks_count = $this->completed_tasks_count; + $tasks_count = count( $this->tasks ); + + // Given 'r' (circle element's r attr), dashoffset = ((100-$desired_percentage)/100) * PI * (r*2). + $progress_percentage = ( $completed_tasks_count / $tasks_count ) * 100; + $circle_r = 6.5; + $circle_dashoffset = ( ( 100 - $progress_percentage ) / 100 ) * ( pi() * ( $circle_r * 2 ) ); + + include __DIR__ . '/views/html-admin-dashboard-setup.php'; + } + + /** + * Populate tasks from the database. + */ + private function populate_general_tasks() { + $tasks = get_option( 'woocommerce_task_list_tracked_completed_tasks', array() ); + foreach ( $tasks as $task ) { + if ( isset( $this->tasks[ $task ] ) ) { + $this->tasks[ $task ]['completed'] = true; + $this->tasks[ $task ]['button_link'] = wc_admin_url( $this->tasks[ $task ]['button_link'] ); + } + } + } + + /** + * Getter for $tasks + * + * @return array + */ + public function get_tasks() { + return $this->tasks; + } + + /** + * Return # of completed tasks + */ + public function get_completed_tasks_count() { + $completed_tasks = array_filter( + $this->tasks, + function( $task ) { + return $task['completed']; + } + ); + + return count( $completed_tasks ); + } + + /** + * Get the next task. + * + * @return array|null + */ + private function get_next_task() { + foreach ( $this->get_tasks() as $task ) { + if ( false === $task['completed'] ) { + return $task; + } + } + + return null; + } + + /** + * Check to see if we should display the widget + * + * @return bool + */ + private function should_display_widget() { + return WC()->is_wc_admin_active() && + 'yes' !== get_option( 'woocommerce_task_list_complete' ) && + 'yes' !== get_option( 'woocommerce_task_list_hidden' ); + } + + /** + * Populate payment tasks's visibility and completion + */ + private function populate_payment_tasks() { + $is_woo_payment_installed = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' ); + $country = explode( ':', get_option( 'woocommerce_default_country', 'US:CA' ) )[0]; + + // woocommerce-payments requires its plugin activated and country must be US. + if ( ! $is_woo_payment_installed || 'US' !== $country ) { + unset( $this->tasks['woocommerce-payments'] ); + } + + // payments can't be used when woocommerce-payments exists and country is US. + if ( $is_woo_payment_installed && 'US' === $country ) { + unset( $this->tasks['payments'] ); + } + + if ( isset( $this->tasks['payments'] ) ) { + $gateways = WC()->payment_gateways->get_available_payment_gateways(); + $enabled_gateways = array_filter( + $gateways, + function ( $gateway ) { + return 'yes' === $gateway->enabled; + } + ); + $this->tasks['payments']['completed'] = ! empty( $enabled_gateways ); + } + + if ( isset( $this->tasks['woocommerce-payments'] ) ) { + $wc_pay_is_connected = false; + if ( class_exists( '\WC_Payments' ) ) { + $wc_payments_gateway = \WC_Payments::get_gateway(); + $wc_pay_is_connected = method_exists( $wc_payments_gateway, 'is_connected' ) + ? $wc_payments_gateway->is_connected() + : false; + } + $this->tasks['woocommerce-payments']['completed'] = $wc_pay_is_connected; + } + } + } + +endif; + +return new WC_Admin_Dashboard_Setup(); diff --git a/includes/admin/class-wc-admin-dashboard.php b/includes/admin/class-wc-admin-dashboard.php new file mode 100644 index 0000000..6357b46 --- /dev/null +++ b/includes/admin/class-wc-admin-dashboard.php @@ -0,0 +1,562 @@ +should_display_widget() ) { + // If on network admin, only load the widget that works in that context and skip the rest. + if ( is_multisite() && is_network_admin() ) { + add_action( 'wp_network_dashboard_setup', array( $this, 'register_network_order_widget' ) ); + } else { + add_action( 'wp_dashboard_setup', array( $this, 'init' ) ); + } + } + } + + /** + * Init dashboard widgets. + */ + public function init() { + // Reviews Widget. + if ( current_user_can( 'publish_shop_orders' ) && post_type_supports( 'product', 'comments' ) ) { + wp_add_dashboard_widget( 'woocommerce_dashboard_recent_reviews', __( 'WooCommerce Recent Reviews', 'woocommerce' ), array( $this, 'recent_reviews' ) ); + } + wp_add_dashboard_widget( 'woocommerce_dashboard_status', __( 'WooCommerce Status', 'woocommerce' ), array( $this, 'status_widget' ) ); + + // Network Order Widget. + if ( is_multisite() && is_main_site() ) { + $this->register_network_order_widget(); + } + } + + /** + * Register the network order dashboard widget. + */ + public function register_network_order_widget() { + wp_add_dashboard_widget( 'woocommerce_network_orders', __( 'WooCommerce Network Orders', 'woocommerce' ), array( $this, 'network_orders' ) ); + } + + /** + * Check to see if we should display the widget. + * + * @return bool + */ + private function should_display_widget() { + if ( ! WC()->is_wc_admin_active() ) { + return false; + } + + $has_permission = current_user_can( 'view_woocommerce_reports' ) || current_user_can( 'manage_woocommerce' ) || current_user_can( 'publish_shop_orders' ); + $task_completed_or_hidden = 'yes' === get_option( 'woocommerce_task_list_complete' ) || 'yes' === get_option( 'woocommerce_task_list_hidden' ); + return $task_completed_or_hidden && $has_permission; + } + + /** + * Get top seller from DB. + * + * @return object + */ + private function get_top_seller() { + global $wpdb; + + $query = array(); + $query['fields'] = "SELECT SUM( order_item_meta.meta_value ) as qty, order_item_meta_2.meta_value as product_id + FROM {$wpdb->posts} as posts"; + $query['join'] = "INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON posts.ID = order_id "; + $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta ON order_items.order_item_id = order_item_meta.order_item_id "; + $query['join'] .= "INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS order_item_meta_2 ON order_items.order_item_id = order_item_meta_2.order_item_id "; + $query['where'] = "WHERE posts.post_type IN ( '" . implode( "','", wc_get_order_types( 'order-count' ) ) . "' ) "; + $query['where'] .= "AND posts.post_status IN ( 'wc-" . implode( "','wc-", apply_filters( 'woocommerce_reports_order_statuses', array( 'completed', 'processing', 'on-hold' ) ) ) . "' ) "; + $query['where'] .= "AND order_item_meta.meta_key = '_qty' "; + $query['where'] .= "AND order_item_meta_2.meta_key = '_product_id' "; + $query['where'] .= "AND posts.post_date >= '" . gmdate( 'Y-m-01', current_time( 'timestamp' ) ) . "' "; + $query['where'] .= "AND posts.post_date <= '" . gmdate( 'Y-m-d H:i:s', current_time( 'timestamp' ) ) . "' "; + $query['groupby'] = 'GROUP BY product_id'; + $query['orderby'] = 'ORDER BY qty DESC'; + $query['limits'] = 'LIMIT 1'; + + return $wpdb->get_row( implode( ' ', apply_filters( 'woocommerce_dashboard_status_widget_top_seller_query', $query ) ) ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Get sales report data. + * + * @return object + */ + private function get_sales_report_data() { + include_once dirname( __FILE__ ) . '/reports/class-wc-report-sales-by-date.php'; + + $sales_by_date = new WC_Report_Sales_By_Date(); + $sales_by_date->start_date = strtotime( gmdate( 'Y-m-01', current_time( 'timestamp' ) ) ); + $sales_by_date->end_date = strtotime( gmdate( 'Y-m-d', current_time( 'timestamp' ) ) ); + $sales_by_date->chart_groupby = 'day'; + $sales_by_date->group_by_query = 'YEAR(posts.post_date), MONTH(posts.post_date), DAY(posts.post_date)'; + + return $sales_by_date->get_report_data(); + } + + /** + * Show status widget. + */ + public function status_widget() { + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + wp_enqueue_script( 'wc-status-widget', WC()->plugin_url() . '/assets/js/admin/wc-status-widget' . $suffix . '.js', array( 'jquery' ), $version, true ); + + include_once dirname( __FILE__ ) . '/reports/class-wc-admin-report.php'; + + $is_wc_admin_disabled = apply_filters( 'woocommerce_admin_disabled', false ); + + $reports = new WC_Admin_Report(); + + $net_sales_link = 'admin.php?page=wc-reports&tab=orders&range=month'; + $top_seller_link = 'admin.php?page=wc-reports&tab=orders&report=sales_by_product&range=month&product_ids='; + $report_data = $is_wc_admin_disabled ? $this->get_sales_report_data() : $this->get_wc_admin_performance_data(); + if ( ! $is_wc_admin_disabled ) { + $net_sales_link = 'admin.php?page=wc-admin&path=%2Fanalytics%2Frevenue&chart=net_revenue&orderby=net_revenue&period=month&compare=previous_period'; + $top_seller_link = 'admin.php?page=wc-admin&filter=single_product&path=%2Fanalytics%2Fproducts&products='; + } + + echo ''; + } + + /** + * Show order data is status widget. + */ + private function status_widget_order_rows() { + if ( ! current_user_can( 'edit_shop_orders' ) ) { + return; + } + $on_hold_count = 0; + $processing_count = 0; + + foreach ( wc_get_order_types( 'order-count' ) as $type ) { + $counts = (array) wp_count_posts( $type ); + $on_hold_count += isset( $counts['wc-on-hold'] ) ? $counts['wc-on-hold'] : 0; + $processing_count += isset( $counts['wc-processing'] ) ? $counts['wc-processing'] : 0; + } + ?> +
  • + + %s order awaiting processing', '%s orders awaiting processing', $processing_count, 'woocommerce' ), + $processing_count + ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + ?> + +
  • +
  • + + %s order on-hold', '%s orders on-hold', $on_hold_count, 'woocommerce' ), + $on_hold_count + ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + ?> + +
  • + get_var( + $wpdb->prepare( + "SELECT COUNT( product_id ) + FROM {$wpdb->wc_product_meta_lookup} AS lookup + INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID + WHERE stock_quantity <= %d + AND stock_quantity > %d + AND posts.post_status = 'publish'", + $stock, + $nostock + ) + ); + } + + set_transient( $transient_name, (int) $lowinstock_count, DAY_IN_SECONDS * 30 ); + } + + $transient_name = 'wc_outofstock_count'; + $outofstock_count = get_transient( $transient_name ); + $lowstock_link = 'admin.php?page=wc-reports&tab=stock&report=low_in_stock'; + $outofstock_link = 'admin.php?page=wc-reports&tab=stock&report=out_of_stock'; + + if ( false === $is_wc_admin_disabled ) { + $lowstock_link = 'admin.php?page=wc-admin&type=lowstock&path=%2Fanalytics%2Fstock'; + $outofstock_link = 'admin.php?page=wc-admin&type=outofstock&path=%2Fanalytics%2Fstock'; + } + + if ( false === $outofstock_count ) { + /** + * Status widget out of stock count pre query. + * + * @since 4.3.0 + * @param null|string $outofstock_count Out of stock count, by default null. + * @param int $nostock No stock amount + */ + $outofstock_count = apply_filters( 'woocommerce_status_widget_out_of_stock_count_pre_query', null, $nostock ); + + if ( is_null( $outofstock_count ) ) { + $outofstock_count = (int) $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT( product_id ) + FROM {$wpdb->wc_product_meta_lookup} AS lookup + INNER JOIN {$wpdb->posts} as posts ON lookup.product_id = posts.ID + WHERE stock_quantity <= %d + AND posts.post_status = 'publish'", + $nostock + ) + ); + } + + set_transient( $transient_name, (int) $outofstock_count, DAY_IN_SECONDS * 30 ); + } + ?> +
  • + + %s product low in stock', '%s products low in stock', $lowinstock_count, 'woocommerce' ), + $lowinstock_count + ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + ?> + +
  • +
  • + + %s product out of stock', '%s products out of stock', $outofstock_count, 'woocommerce' ), + $outofstock_count + ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + ?> + +
  • + comments} comments + LEFT JOIN {$wpdb->posts} posts ON (comments.comment_post_ID = posts.ID) + WHERE comments.comment_approved = '1' + AND comments.comment_type = 'review' + AND posts.post_password = '' + AND posts.post_type = 'product' + AND comments.comment_parent = 0 + ORDER BY comments.comment_date_gmt DESC + LIMIT 5" + ); + + $comments = $wpdb->get_results( + "SELECT posts.ID, posts.post_title, comments.comment_author, comments.comment_author_email, comments.comment_ID, comments.comment_content {$query_from};" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ); + + if ( $comments ) { + echo '
      '; + foreach ( $comments as $comment ) { + + echo '
    • '; + + echo get_avatar( $comment->comment_author_email, '32' ); + + $rating = intval( get_comment_meta( $comment->comment_ID, 'rating', true ) ); + + /* translators: %s: rating */ + echo '
      ' . sprintf( esc_html__( '%s out of 5', 'woocommerce' ), esc_html( $rating ) ) . '
      '; + + /* translators: %s: review author */ + echo '

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

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

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

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

    + +

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

    + +

    +
    + + + +
    + set_query_params( + array( + 'before' => $end_date, + 'after' => $start_date, + 'stats' => 'revenue/total_sales,revenue/net_revenue,orders/orders_count,products/items_sold,variations/items_sold', + ) + ); + $response = rest_do_request( $request ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + if ( 200 !== $response->get_status() ) { + return new \WP_Error( 'woocommerce_analytics_performance_indicators_result_failed', __( 'Sorry, fetching performance indicators failed.', 'woocommerce' ) ); + } + $report_keys = array( + 'net_revenue' => 'net_sales', + ); + $performance_data = new stdClass(); + foreach ( $response->get_data() as $indicator ) { + if ( isset( $indicator['chart'] ) && isset( $indicator['value'] ) ) { + $key = isset( $report_keys[ $indicator['chart'] ] ) ? $report_keys[ $indicator['chart'] ] : $indicator['chart']; + $performance_data->$key = $indicator['value']; + } + } + return $performance_data; + } + + /** + * Overwrites the original sparkline to use the new reports data if WooAdmin is enabled. + * Prepares a sparkline to show sales in the last X days. + * + * @param WC_Admin_Report $reports old class for getting reports. + * @param bool $is_wc_admin_disabled If WC Admin is disabled or not. + * @param int $id ID of the product to show. Blank to get all orders. + * @param string $type Type of sparkline to get. Ignored if ID is not set. + * @return string + */ + private function sales_sparkline( $reports, $is_wc_admin_disabled = false, $id = '', $type = 'sales' ) { + $days = max( 7, gmdate( 'd', current_time( 'timestamp' ) ) ); + if ( $is_wc_admin_disabled ) { + return $reports->sales_sparkline( $id, $days, $type ); + } + $sales_endpoint = '/wc-analytics/reports/revenue/stats'; + $start_date = gmdate( 'Y-m-d 00:00:00', current_time( 'timestamp' ) - ( ( $days - 1 ) * DAY_IN_SECONDS ) ); + $end_date = gmdate( 'Y-m-d 23:59:59', current_time( 'timestamp' ) ); + $meta_key = 'net_revenue'; + $params = array( + 'order' => 'asc', + 'interval' => 'day', + 'per_page' => 100, + 'before' => $end_date, + 'after' => $start_date, + ); + if ( $id ) { + $sales_endpoint = '/wc-analytics/reports/products/stats'; + $meta_key = ( 'sales' === $type ) ? 'net_revenue' : 'items_sold'; + $params['products'] = $id; + } + $request = new \WP_REST_Request( 'GET', $sales_endpoint ); + $params['fields'] = array( $meta_key ); + $request->set_query_params( $params ); + + $response = rest_do_request( $request ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $resp_data = $response->get_data(); + $data = $resp_data['intervals']; + + $sparkline_data = array(); + $total = 0; + foreach ( $data as $d ) { + $total += $d['subtotals']->$meta_key; + array_push( $sparkline_data, array( strval( strtotime( $d['interval'] ) * 1000 ), $d['subtotals']->$meta_key ) ); + } + + if ( 'sales' === $type ) { + /* translators: 1: total income 2: days */ + $tooltip = sprintf( __( 'Sold %1$s worth in the last %2$d days', 'woocommerce' ), strip_tags( wc_price( $total ) ), $days ); + } else { + /* translators: 1: total items sold 2: days */ + $tooltip = sprintf( _n( 'Sold %1$d item in the last %2$d days', 'Sold %1$d items in the last %2$d days', $total, 'woocommerce' ), $total, $days ); + } + + return ''; + } + } + +endif; + +return new WC_Admin_Dashboard(); diff --git a/includes/admin/class-wc-admin-duplicate-product.php b/includes/admin/class-wc-admin-duplicate-product.php new file mode 100644 index 0000000..05c6702 --- /dev/null +++ b/includes/admin/class-wc-admin-duplicate-product.php @@ -0,0 +1,286 @@ +post_type ) { + return $actions; + } + + // Add Class to Delete Permanently link in row actions. + if ( empty( $the_product ) || $the_product->get_id() !== $post->ID ) { + $the_product = wc_get_product( $post ); + } + + if ( 'publish' === $post->post_status && $the_product && 0 < $the_product->get_total_sales() ) { + $actions['trash'] = sprintf( + '%s', + get_delete_post_link( $the_product->get_id(), '', false ), + /* translators: %s: post title */ + esc_attr( sprintf( __( 'Move “%s” to the Trash', 'woocommerce' ), $the_product->get_name() ) ), + esc_html__( 'Trash', 'woocommerce' ) + ); + } + + $actions['duplicate'] = '' . esc_html__( 'Duplicate', 'woocommerce' ) . ''; + + return $actions; + } + + /** + * 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' ) ) ) { + return; + } + + if ( ! is_object( $post ) ) { + return; + } + + if ( 'product' !== $post->post_type ) { + return; + } + + $notify_url = wp_nonce_url( admin_url( 'edit.php?post_type=product&action=duplicate_product&post=' . absint( $post->ID ) ), 'woocommerce-duplicate-product_' . $post->ID ); + ?> +
    + 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. + * + * @param WC_Product $product The product to duplicate. + * @return WC_Product The duplicate. + */ + public function product_duplicate( $product ) { + /** + * Filter to allow us to exclude meta keys from product duplication.. + * + * @param array $exclude_meta The keys to exclude from the duplicate. + * @param array $existing_meta_keys The meta keys that the product already has. + * @since 2.6 + */ + $meta_to_exclude = array_filter( + apply_filters( + 'woocommerce_duplicate_product_exclude_meta', + array(), + array_map( + function ( $datum ) { + return $datum->key; + }, + $product->get_meta_data() + ) + ) + ); + + $duplicate = clone $product; + $duplicate->set_id( 0 ); + /* translators: %s contains the name of the original product. */ + $duplicate->set_name( sprintf( esc_html__( '%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 ); + + foreach ( $meta_to_exclude as $meta_key ) { + $duplicate->delete_meta_data( $meta_key ); + } + + /** + * 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 ); + + // Save parent product. + $duplicate->save(); + + // 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 ); + $child_duplicate->set_date_created( null ); + + // If we wait and let the insertion generate the slug, we will see extreme performance degradation + // in the case where a product is used as a template. Every time the template is duplicated, each + // variation will query every consecutive slug until it finds an empty one. To avoid this, we can + // optimize the generation ourselves, avoiding the issue altogether. + $this->generate_unique_slug( $child_duplicate ); + + if ( '' !== $child->get_sku( 'edit' ) ) { + $child_duplicate->set_sku( wc_product_generate_unique_sku( 0, $child->get_sku( 'edit' ) ) ); + } + + foreach ( $meta_to_exclude as $meta_key ) { + $child_duplicate->delete_meta_data( $meta_key ); + } + + /** + * 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 ); + + $child_duplicate->save(); + } + + // Get new object to reflect new children. + $duplicate = wc_get_product( $duplicate->get_id() ); + } + + return $duplicate; + } + + /** + * Get a product from the database to duplicate. + * + * @deprecated 3.0.0 + * @param mixed $id The ID of the product to duplicate. + * @return object|bool + * @see duplicate_product + */ + private function get_product_to_duplicate( $id ) { + global $wpdb; + + $id = absint( $id ); + + if ( ! $id ) { + return false; + } + + $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) ); + + if ( isset( $post->post_type ) && 'revision' === $post->post_type ) { + $id = $post->post_parent; + $post = $wpdb->get_row( $wpdb->prepare( "SELECT {$wpdb->posts}.* FROM {$wpdb->posts} WHERE ID = %d", $id ) ); + } + + return $post; + } + + /** + * Generates a unique slug for a given product. We do this so that we can override the + * behavior of wp_unique_post_slug(). The normal slug generation will run single + * select queries on every non-unique slug, resulting in very bad performance. + * + * @param WC_Product $product The product to generate a slug for. + * @since 3.9.0 + */ + private function generate_unique_slug( $product ) { + global $wpdb; + + // We want to remove the suffix from the slug so that we can find the maximum suffix using this root slug. + // This will allow us to find the next-highest suffix that is unique. While this does not support gap + // filling, this shouldn't matter for our use-case. + $root_slug = preg_replace( '/-[0-9]+$/', '', $product->get_slug() ); + + $results = $wpdb->get_results( + $wpdb->prepare( "SELECT post_name FROM $wpdb->posts WHERE post_name LIKE %s AND post_type IN ( 'product', 'product_variation' )", $root_slug . '%' ) + ); + + // The slug is already unique! + if ( empty( $results ) ) { + return; + } + + // Find the maximum suffix so we can ensure uniqueness. + $max_suffix = 1; + foreach ( $results as $result ) { + // Pull a numerical suffix off the slug after the last hyphen. + $suffix = intval( substr( $result->post_name, strrpos( $result->post_name, '-' ) + 1 ) ); + if ( $suffix > $max_suffix ) { + $max_suffix = $suffix; + } + } + + $product->set_slug( $root_slug . '-' . ( $max_suffix + 1 ) ); + } +} + +return new WC_Admin_Duplicate_Product(); diff --git a/includes/admin/class-wc-admin-exporters.php b/includes/admin/class-wc-admin-exporters.php new file mode 100644 index 0000000..9a0758f --- /dev/null +++ b/includes/admin/class-wc-admin-exporters.php @@ -0,0 +1,220 @@ +export_allowed() ) { + return; + } + + add_action( 'admin_menu', array( $this, 'add_to_menus' ) ); + add_action( 'admin_head', array( $this, 'hide_from_menus' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); + add_action( 'admin_init', array( $this, 'download_export_file' ) ); + add_action( 'wp_ajax_woocommerce_do_ajax_product_export', array( $this, 'do_ajax_product_export' ) ); + + // Register WooCommerce exporters. + $this->exporters['product_exporter'] = array( + 'menu' => 'edit.php?post_type=product', + 'name' => __( 'Product Export', 'woocommerce' ), + 'capability' => 'export', + 'callback' => array( $this, 'product_exporter' ), + ); + } + + /** + * Return true if WooCommerce export is allowed for current user, false otherwise. + * + * @return bool Whether current user can perform export. + */ + protected function export_allowed() { + return current_user_can( 'edit_products' ) && current_user_can( 'export' ); + } + + /** + * 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 = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + wp_register_script( 'wc-product-export', WC()->plugin_url() . '/assets/js/admin/wc-product-export' . $suffix . '.js', array( 'jquery' ), $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( wp_unslash( $_GET['nonce'] ), 'product-csv' ) && 'download_product_csv' === wp_unslash( $_GET['action'] ) ) { // WPCS: input var ok, sanitization ok. + include_once WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php'; + $exporter = new WC_Product_CSV_Exporter(); + + if ( ! empty( $_GET['filename'] ) ) { // WPCS: input var ok. + $exporter->set_filename( wp_unslash( $_GET['filename'] ) ); // WPCS: input var ok, sanitization ok. + } + + $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 ( ! $this->export_allowed() ) { + wp_send_json_error( array( 'message' => __( 'Insufficient privileges to export products.', 'woocommerce' ) ) ); + } + + include_once WC_ABSPATH . 'includes/export/class-wc-product-csv-exporter.php'; + + $step = isset( $_POST['step'] ) ? absint( $_POST['step'] ) : 1; // WPCS: input var ok, sanitization ok. + $exporter = new WC_Product_CSV_Exporter(); + + if ( ! empty( $_POST['columns'] ) ) { // WPCS: input var ok. + $exporter->set_column_names( wp_unslash( $_POST['columns'] ) ); // WPCS: input var ok, sanitization ok. + } + + if ( ! empty( $_POST['selected_columns'] ) ) { // WPCS: input var ok. + $exporter->set_columns_to_export( wp_unslash( $_POST['selected_columns'] ) ); // WPCS: input var ok, sanitization ok. + } + + if ( ! empty( $_POST['export_meta'] ) ) { // WPCS: input var ok. + $exporter->enable_meta_export( true ); + } + + if ( ! empty( $_POST['export_types'] ) ) { // WPCS: input var ok. + $exporter->set_product_types_to_export( wp_unslash( $_POST['export_types'] ) ); // WPCS: input var ok, sanitization ok. + } + + if ( ! empty( $_POST['export_category'] ) && is_array( $_POST['export_category'] ) ) {// WPCS: input var ok. + $exporter->set_product_category_to_export( wp_unslash( array_values( $_POST['export_category'] ) ) ); // WPCS: input var ok, sanitization ok. + } + + if ( ! empty( $_POST['filename'] ) ) { // WPCS: input var ok. + $exporter->set_filename( wp_unslash( $_POST['filename'] ) ); // WPCS: input var ok, sanitization ok. + } + + $exporter->set_page( $step ); + $exporter->generate_file(); + + $query_args = apply_filters( + 'woocommerce_export_get_ajax_query_args', + array( + 'nonce' => wp_create_nonce( 'product-csv' ), + 'action' => 'download_product_csv', + 'filename' => $exporter->get_filename(), + ) + ); + + if ( 100 === $exporter->get_percent_complete() ) { + wp_send_json_success( + array( + 'step' => 'done', + 'percentage' => 100, + 'url' => add_query_arg( $query_args, 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(), + ) + ); + } + } + + /** + * Gets the product types that can be exported. + * + * @since 5.1.0 + * @return array The product types keys and labels. + */ + public static function get_product_types() { + $product_types = wc_get_product_types(); + $product_types['variation'] = __( 'Product variations', 'woocommerce' ); + + /** + * Allow third-parties to filter the exportable product types. + * + * @since 5.1.0 + * @param array $product_types { + * The product type key and label. + * + * @type string Product type key - eg 'variable', 'simple' etc. + * @type string A translated product label which appears in the export product type dropdown. + * } + */ + return apply_filters( 'woocommerce_exporter_product_types', $product_types ); + } +} + +new WC_Admin_Exporters(); diff --git a/includes/admin/class-wc-admin-help.php b/includes/admin/class-wc-admin-help.php new file mode 100644 index 0000000..c453191 --- /dev/null +++ b/includes/admin/class-wc-admin-help.php @@ -0,0 +1,85 @@ +id, wc_get_screen_ids() ) ) { + return; + } + + $screen->add_help_tab( + array( + 'id' => 'woocommerce_support_tab', + 'title' => __( 'Help & Support', 'woocommerce' ), + 'content' => + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

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

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

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

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

    ' . + '

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

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

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' + ); + } +} + +return new WC_Admin_Help(); diff --git a/includes/admin/class-wc-admin-importers.php b/includes/admin/class-wc-admin-importers.php new file mode 100644 index 0000000..95f9e3b --- /dev/null +++ b/includes/admin/class-wc-admin-importers.php @@ -0,0 +1,306 @@ +import_allowed() ) { + return; + } + + add_action( 'admin_menu', array( $this, 'add_to_menus' ) ); + add_action( 'admin_init', array( $this, 'register_importers' ) ); + add_action( 'admin_head', array( $this, 'hide_from_menus' ) ); + add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) ); + add_action( 'wp_ajax_woocommerce_do_ajax_product_import', array( $this, 'do_ajax_product_import' ) ); + + // Register WooCommerce importers. + $this->importers['product_importer'] = array( + 'menu' => 'edit.php?post_type=product', + 'name' => __( 'Product Import', 'woocommerce' ), + 'capability' => 'import', + 'callback' => array( $this, 'product_importer' ), + ); + } + + /** + * Return true if WooCommerce imports are allowed for current user, false otherwise. + * + * @return bool Whether current user can perform imports. + */ + protected function import_allowed() { + return current_user_can( 'edit_products' ) && current_user_can( 'import' ); + } + + /** + * 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 = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + wp_register_script( 'wc-product-import', WC()->plugin_url() . '/assets/js/admin/wc-product-import' . $suffix . '.js', array( 'jquery' ), $version, true ); + } + + /** + * 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 ( Constants::is_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() { + if ( Constants::is_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' ) ); + } + } + + /** + * The tax rate importer which extends WP_Importer. + */ + public function tax_rates_importer() { + require_once ABSPATH . 'wp-admin/includes/import.php'; + + if ( ! class_exists( 'WP_Importer' ) ) { + $class_wp_importer = ABSPATH . 'wp-admin/includes/class-wp-importer.php'; + + if ( file_exists( $class_wp_importer ) ) { + require $class_wp_importer; + } + } + + require dirname( __FILE__ ) . '/importers/class-wc-tax-rate-importer.php'; + + $importer = new WC_Tax_Rate_Importer(); + $importer->dispatch(); + } + + /** + * 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. + */ + public function post_importer_compatibility() { + global $wpdb; + + if ( empty( $_POST['import_id'] ) || ! class_exists( 'WXR_Parser' ) ) { // PHPCS: input var ok, CSRF ok. + return; + } + + $id = absint( $_POST['import_id'] ); // PHPCS: input var ok. + $file = get_attached_file( $id ); + $parser = new WXR_Parser(); + $import_data = $parser->parse( $file ); + + 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_attribute_taxonomy_slug( $term['domain'] ); + + // Create the taxonomy. + if ( ! in_array( $attribute_name, wc_get_attribute_taxonomies(), true ) ) { + wc_create_attribute( + array( + 'name' => $attribute_name, + 'slug' => $attribute_name, + 'type' => 'select', + 'order_by' => 'menu_order', + 'has_archives' => false, + ) + ); + } + + // 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, + ) + ) + ); + } + } + } + } + } + } + } + + /** + * 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 ( ! $this->import_allowed() || ! isset( $_POST['file'] ) ) { // PHPCS: input var ok. + wp_send_json_error( array( 'message' => __( 'Insufficient privileges to import products.', 'woocommerce' ) ) ); + } + + 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( wp_unslash( $_POST['file'] ) ); // PHPCS: input var ok. + $params = array( + 'delimiter' => ! empty( $_POST['delimiter'] ) ? wc_clean( wp_unslash( $_POST['delimiter'] ) ) : ',', // PHPCS: input var ok. + 'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0, // PHPCS: input var ok. + 'mapping' => isset( $_POST['mapping'] ) ? (array) wc_clean( wp_unslash( $_POST['mapping'] ) ) : array(), // PHPCS: input var ok. + 'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false, // PHPCS: input var ok. + '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 ) { + // @codingStandardsIgnoreStart. + $wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_original_id' ) ); + $wpdb->delete( $wpdb->posts, array( + 'post_type' => 'product', + 'post_status' => 'importing', + ) ); + $wpdb->delete( $wpdb->posts, array( + 'post_type' => 'product_variation', + 'post_status' => 'importing', + ) ); + // @codingStandardsIgnoreEnd. + + // Clean up orphaned data. + $wpdb->query( + " + DELETE {$wpdb->posts}.* FROM {$wpdb->posts} + LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->posts}.post_parent + WHERE wp.ID IS NULL AND {$wpdb->posts}.post_type = 'product_variation' + " + ); + $wpdb->query( + " + DELETE {$wpdb->postmeta}.* FROM {$wpdb->postmeta} + LEFT JOIN {$wpdb->posts} wp ON wp.ID = {$wpdb->postmeta}.post_id + WHERE wp.ID IS NULL + " + ); + // @codingStandardsIgnoreStart. + $wpdb->query( " + DELETE tr.* FROM {$wpdb->term_relationships} tr + LEFT JOIN {$wpdb->posts} wp ON wp.ID = tr.object_id + LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id + WHERE wp.ID IS NULL + AND tt.taxonomy IN ( '" . implode( "','", array_map( 'esc_sql', get_object_taxonomies( 'product' ) ) ) . "' ) + " ); + // @codingStandardsIgnoreEnd. + + // Send success. + wp_send_json_success( + array( + 'position' => 'done', + 'percentage' => 100, + 'url' => add_query_arg( array( '_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ) ), 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'] ), + ) + ); + } + } +} + +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 0000000..2d9b922 --- /dev/null +++ b/includes/admin/class-wc-admin-log-table-list.php @@ -0,0 +1,395 @@ + '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( + 'Y-m-d H:i:s', + $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 ] ) ) { + return ''; + } + + $level = $levels[ $level_key ]; + $level_class = sanitize_html_class( 'log-level--' . $level_key ); + return '' . esc_html( $level ) . ''; + } + + /** + * 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( $_REQUEST['s'] ) ) { + $where_conditions[] = 'message like %s'; + $where_values[] = '%' . $wpdb->esc_like( wc_clean( wp_unslash( $_REQUEST['s'] ) ) ) . '%'; + } + + if ( empty( $where_conditions ) ) { + return ''; + } + + return $wpdb->prepare( 'WHERE 1 = 1 AND ' . implode( ' AND ', $where_conditions ), $where_values ); + } + + /** + * 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 new file mode 100644 index 0000000..6461e70 --- /dev/null +++ b/includes/admin/class-wc-admin-menus.php @@ -0,0 +1,428 @@ + Menus > Pages. + add_action( 'admin_head-nav-menus.php', array( $this, 'add_nav_menu_meta_boxes' ) ); + + // Admin bar menus. + if ( apply_filters( 'woocommerce_show_admin_bar_visit_store', true ) ) { + add_action( 'admin_bar_menu', array( $this, 'admin_bar_menus' ), 31 ); + } + + // Handle saving settings earlier than load-{page} hook to avoid race conditions in conditional menus. + add_action( 'wp_loaded', array( $this, 'save_settings' ) ); + } + + /** + * Add menu items. + */ + public function admin_menu() { + global $menu; + + $woocommerce_icon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPjxwYXRoIGZpbGw9IiNhMmFhYjIiIGQ9Ik02MTIuMTkyIDQyNi4zMzZjMC02Ljg5Ni0zLjEzNi01MS42LTI4LTUxLjYtMzcuMzYgMC00Ni43MDQgNzIuMjU2LTQ2LjcwNCA4Mi42MjQgMCAzLjQwOCAzLjE1MiA1OC40OTYgMjguMDMyIDU4LjQ5NiAzNC4xOTItLjAzMiA0Ni42NzItNzIuMjg4IDQ2LjY3Mi04OS41MnptMjAyLjE5MiAwYzAtNi44OTYtMy4xNTItNTEuNi0yOC4wMzItNTEuNi0zNy4yOCAwLTQ2LjYwOCA3Mi4yNTYtNDYuNjA4IDgyLjYyNCAwIDMuNDA4IDMuMDcyIDU4LjQ5NiAyNy45NTIgNTguNDk2IDM0LjE5Mi0uMDMyIDQ2LjY4OC03Mi4yODggNDYuNjg4LTg5LjUyek0xNDEuMjk2Ljc2OGMtNjguMjI0IDAtMTIzLjUwNCA1NS40ODgtMTIzLjUwNCAxMjMuOTJ2NjUwLjcyYzAgNjguNDMyIDU1LjI5NiAxMjMuOTIgMTIzLjUwNCAxMjMuOTJoMzM5LjgwOGwxMjMuNTA0IDEyMy45MzZWODk5LjMyOGgyNzguMDQ4YzY4LjIyNCAwIDEyMy41Mi01NS40NzIgMTIzLjUyLTEyMy45MnYtNjUwLjcyYzAtNjguNDMyLTU1LjI5Ni0xMjMuOTItMTIzLjUyLTEyMy45MmgtNzQxLjM2em01MjYuODY0IDQyMi4xNmMwIDU1LjA4OC0zMS4wODggMTU0Ljg4LTEwMi42NCAxNTQuODgtNi4yMDggMC0xOC40OTYtMy42MTYtMjUuNDI0LTYuMDE2LTMyLjUxMi0xMS4xNjgtNTAuMTkyLTQ5LjY5Ni01Mi4zNTItNjYuMjU2IDAgMC0zLjA3Mi0xNy43OTItMy4wNzItNDAuNzUyIDAtMjIuOTkyIDMuMDcyLTQ1LjMyOCAzLjA3Mi00NS4zMjggMTUuNTUyLTc1LjcyOCA0My41NTItMTA2LjczNiA5Ni40NDgtMTA2LjczNiA1OS4wNzItLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4ek00ODYuNDk2IDMwMi40YzAgMy4zOTItNDMuNTUyIDE0MS4xNjgtNDMuNTUyIDIxMy40MjR2NzUuNzEyYy0yLjU5MiAxMi4wOC00LjE2IDI0LjE0NC0yMS44MjQgMjQuMTQ0LTQ2LjYwOCAwLTg4Ljg4LTE1MS40NzItOTIuMDE2LTE2MS44NC02LjIwOCA2Ljg5Ni02Mi4yNCAxNjEuODQtOTYuNDQ4IDE2MS44NC0yNC44NjQgMC00My41NTItMTEzLjY0OC00Ni42MDgtMTIzLjkzNkMxNzYuNzA0IDQzNi42NzIgMTYwIDMzNC4yMjQgMTYwIDMyNy4zMjhjMC0yMC42NzIgMS4xNTItMzguNzM2IDI2LjA0OC0zOC43MzYgNi4yMDggMCAyMS42IDYuMDY0IDIzLjcxMiAxNy4xNjggMTEuNjQ4IDYyLjAzMiAxNi42ODggMTIwLjUxMiAyOS4xNjggMTg1Ljk2OCAxLjg1NiAyLjkyOCAxLjUwNCA3LjAwOCA0LjU2IDEwLjQzMiAzLjE1Mi0xMC4yODggNjYuOTI4LTE2OC43ODQgOTQuOTYtMTY4Ljc4NCAyMi41NDQgMCAzMC40IDQ0LjU5MiAzMy41MzYgNjEuODI0IDYuMjA4IDIwLjY1NiAxMy4wODggNTUuMjE2IDIyLjQxNiA4Mi43NTIgMC0xMy43NzYgMTIuNDgtMjAzLjEyIDY1LjM5Mi0yMDMuMTIgMTguNTkyLjAzMiAyNi43MDQgNi45MjggMjYuNzA0IDI3LjU2OHpNODcwLjMyIDQyMi45MjhjMCA1NS4wODgtMzEuMDg4IDE1NC44OC0xMDIuNjQgMTU0Ljg4LTYuMTkyIDAtMTguNDQ4LTMuNjE2LTI1LjQyNC02LjAxNi0zMi40MzItMTEuMTY4LTUwLjE3Ni00OS42OTYtNTIuMjg4LTY2LjI1NiAwIDAtMy44ODgtMTcuOTItMy44ODgtNDAuODk2czMuODg4LTQ1LjE4NCAzLjg4OC00NS4xODRjMTUuNTUyLTc1LjcyOCA0My40ODgtMTA2LjczNiA5Ni4zODQtMTA2LjczNiA1OS4xMDQtLjAzMiA4My45NjggNTguNTI4IDgzLjk2OCAxMTAuMjA4eiIvPjwvc3ZnPg=='; + + if ( current_user_can( 'edit_others_shop_orders' ) ) { + $menu[] = array( '', 'read', 'separator-woocommerce', '', 'wp-menu-separator woocommerce' ); // WPCS: override ok. + } + + add_menu_page( __( 'WooCommerce', 'woocommerce' ), __( 'WooCommerce', 'woocommerce' ), 'edit_others_shop_orders', 'woocommerce', null, $woocommerce_icon, '55.5' ); + + add_submenu_page( 'edit.php?post_type=product', __( 'Attributes', 'woocommerce' ), __( 'Attributes', 'woocommerce' ), 'manage_product_terms', 'product_attributes', array( $this, 'attributes_page' ) ); + } + + /** + * Add menu item. + */ + public function reports_menu() { + if ( current_user_can( 'edit_others_shop_orders' ) ) { + add_submenu_page( 'woocommerce', __( 'Reports', 'woocommerce' ), __( 'Reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ) ); + } else { + add_menu_page( __( 'Sales reports', 'woocommerce' ), __( 'Sales reports', 'woocommerce' ), 'view_woocommerce_reports', 'wc-reports', array( $this, 'reports_page' ), 'dashicons-chart-bar', '55.6' ); + } + } + + /** + * Add menu item. + */ + public function settings_menu() { + $settings_page = add_submenu_page( 'woocommerce', __( 'WooCommerce settings', 'woocommerce' ), __( 'Settings', 'woocommerce' ), 'manage_woocommerce', 'wc-settings', array( $this, 'settings_page' ) ); + + add_action( 'load-' . $settings_page, array( $this, 'settings_page_init' ) ); + } + + /** + * Loads gateways and shipping methods into memory for use within settings. + */ + public function settings_page_init() { + WC()->payment_gateways(); + WC()->shipping(); + + // Include settings pages. + WC_Admin_Settings::get_settings_pages(); + + // Add any posted messages. + if ( ! empty( $_GET['wc_error'] ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::add_error( wp_kses_post( wp_unslash( $_GET['wc_error'] ) ) ); // WPCS: input var okay, CSRF ok. + } + + if ( ! empty( $_GET['wc_message'] ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::add_message( wp_kses_post( wp_unslash( $_GET['wc_message'] ) ) ); // WPCS: input var okay, CSRF ok. + } + + do_action( 'woocommerce_settings_page_init' ); + } + + /** + * Handle saving of settings. + * + * @return void + */ + public function save_settings() { + global $current_tab, $current_section; + + // We should only save on the settings page. + if ( ! is_admin() || ! isset( $_GET['page'] ) || 'wc-settings' !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + // Include settings pages. + WC_Admin_Settings::get_settings_pages(); + + // Get current tab/section. + $current_tab = empty( $_GET['tab'] ) ? 'general' : sanitize_title( wp_unslash( $_GET['tab'] ) ); // WPCS: input var okay, CSRF ok. + $current_section = empty( $_REQUEST['section'] ) ? '' : sanitize_title( wp_unslash( $_REQUEST['section'] ) ); // WPCS: input var okay, CSRF ok. + + // Save settings if data has been posted. + if ( '' !== $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}_{$current_section}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::save(); + } elseif ( '' === $current_section && apply_filters( "woocommerce_save_settings_{$current_tab}", ! empty( $_POST['save'] ) ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::save(); + } + } + + /** + * Add menu item. + */ + public function status_menu() { + add_submenu_page( 'woocommerce', __( 'WooCommerce status', 'woocommerce' ), __( 'Status', 'woocommerce' ), 'manage_woocommerce', 'wc-status', array( $this, 'status_page' ) ); + } + + /** + * Addons menu item. + */ + public function addons_menu() { + $count_html = WC_Helper_Updater::get_updates_count_html(); + /* translators: %s: extensions count */ + $menu_title = sprintf( __( 'My Subscriptions %s', 'woocommerce' ), $count_html ); + add_submenu_page( 'woocommerce', __( 'WooCommerce Marketplace', 'woocommerce' ), __( 'Marketplace', 'woocommerce' ), 'manage_woocommerce', 'wc-addons', array( $this, 'addons_page' ) ); + add_submenu_page( 'woocommerce', __( 'My WooCommerce.com Subscriptions', 'woocommerce' ), $menu_title, 'manage_woocommerce', 'wc-addons§ion=helper', array( $this, 'addons_page' ) ); + } + + /** + * Highlights the correct top level admin menu item for post type add screens. + */ + public function menu_highlight() { + global $parent_file, $submenu_file, $post_type; + + switch ( $post_type ) { + case 'shop_order': + case 'shop_coupon': + $parent_file = 'woocommerce'; // WPCS: override ok. + break; + case 'product': + $screen = get_current_screen(); + if ( $screen && taxonomy_is_product_attribute( $screen->taxonomy ) ) { + $submenu_file = 'product_attributes'; // WPCS: override ok. + $parent_file = 'edit.php?post_type=product'; // WPCS: override ok. + } + break; + } + } + + /** + * Adds the order processing count to the menu. + */ + public function menu_order_count() { + global $submenu; + + if ( isset( $submenu['woocommerce'] ) ) { + // Remove 'WooCommerce' sub menu item. + unset( $submenu['woocommerce'][0] ); + + // Add count if user has access. + if ( apply_filters( 'woocommerce_include_processing_order_count_in_menu', true ) && current_user_can( 'edit_others_shop_orders' ) ) { + $order_count = apply_filters( 'woocommerce_menu_order_count', wc_processing_order_count() ); + + if ( $order_count ) { + foreach ( $submenu['woocommerce'] as $key => $menu_item ) { + if ( 0 === strpos( $menu_item[0], _x( 'Orders', 'Admin menu name', 'woocommerce' ) ) ) { + $submenu['woocommerce'][ $key ][0] .= ' ' . number_format_i18n( $order_count ) . ''; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + break; + } + } + } + } + } + } + + /** + * Reorder the WC menu items in admin. + * + * @param int $menu_order Menu order. + * @return array + */ + public function menu_order( $menu_order ) { + // Initialize our custom order array. + $woocommerce_menu_order = array(); + + // Get the index of our custom separator. + $woocommerce_separator = array_search( 'separator-woocommerce', $menu_order, true ); + + // Get index of product menu. + $woocommerce_product = array_search( 'edit.php?post_type=product', $menu_order, true ); + + // Loop through menu order and do some rearranging. + foreach ( $menu_order as $index => $item ) { + + if ( 'woocommerce' === $item ) { + $woocommerce_menu_order[] = 'separator-woocommerce'; + $woocommerce_menu_order[] = $item; + $woocommerce_menu_order[] = 'edit.php?post_type=product'; + unset( $menu_order[ $woocommerce_separator ] ); + unset( $menu_order[ $woocommerce_product ] ); + } elseif ( ! in_array( $item, array( 'separator-woocommerce' ), true ) ) { + $woocommerce_menu_order[] = $item; + } + } + + // Return order. + return $woocommerce_menu_order; + } + + /** + * Custom menu order. + * + * @param bool $enabled Whether custom menu ordering is already enabled. + * @return bool + */ + public function custom_menu_order( $enabled ) { + return $enabled || current_user_can( 'edit_others_shop_orders' ); + } + + /** + * Validate screen options on update. + * + * @param bool|int $status Screen option value. Default false to skip. + * @param string $option The option name. + * @param int $value The number of rows to use. + */ + public function set_screen_option( $status, $option, $value ) { + if ( in_array( $option, array( 'woocommerce_keys_per_page', 'woocommerce_webhooks_per_page' ), true ) ) { + return $value; + } + + return $status; + } + + /** + * Init the reports page. + */ + public function reports_page() { + WC_Admin_Reports::output(); + } + + /** + * Init the settings page. + */ + public function settings_page() { + WC_Admin_Settings::output(); + } + + /** + * Init the attributes page. + */ + public function attributes_page() { + WC_Admin_Attributes::output(); + } + + /** + * Init the status page. + */ + public function status_page() { + WC_Admin_Status::output(); + } + + /** + * Init the addons page. + */ + public function addons_page() { + WC_Admin_Addons::output(); + } + + /** + * Add custom nav meta box. + * + * Adapted from http://www.johnmorrisonline.com/how-to-add-a-fully-functional-custom-meta-box-to-wordpress-navigation-menus/. + */ + public function add_nav_menu_meta_boxes() { + add_meta_box( 'woocommerce_endpoints_nav_link', __( 'WooCommerce endpoints', 'woocommerce' ), array( $this, 'nav_menu_links' ), 'nav-menus', 'side', 'low' ); + } + + /** + * Output menu links. + */ + public function nav_menu_links() { + // Get items from account menu. + $endpoints = wc_get_account_menu_items(); + + // Remove dashboard item. + if ( isset( $endpoints['dashboard'] ) ) { + unset( $endpoints['dashboard'] ); + } + + // Include missing lost password. + $endpoints['lost-password'] = __( 'Lost password', 'woocommerce' ); + + $endpoints = apply_filters( 'woocommerce_custom_nav_menu_items', $endpoints ); + + ?> +
    +
    +
      + $value ) : + ?> +
    • + + + + + +
    • + +
    +
    +

    + + + + + + + +

    +
    + add_node( + array( + 'parent' => 'site-name', + 'id' => 'view-store', + 'title' => __( 'Visit Store', 'woocommerce' ), + 'href' => wc_get_page_permalink( 'shop' ), + ) + ); + } + + /** + * Highlight the My Subscriptions menu item when on that page + * + * @param string $submenu_file The submenu file. + * @param string $parent_file currently opened page. + * + * @return string + */ + public function update_menu_highlight( $submenu_file, $parent_file ) { + if ( 'woocommerce' === $parent_file && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { + $submenu_file = 'wc-addons§ion=helper'; + } + return $submenu_file; + } + + /** + * Update the My Subscriptions document title when on that page. + * We want to maintain existing page URL but add it as a separate page, + * which requires updating it manually. + * + * @param string $admin_title existing page title. + * @return string + */ + public function update_my_subscriptions_title( $admin_title ) { + if ( + isset( $_GET['page'] ) && 'wc-addons' === $_GET['page'] && + isset( $_GET['section'] ) && 'helper' === $_GET['section'] + ) { + $admin_title = 'My WooCommerce.com Subscriptions'; + } + return $admin_title; + } +} + +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 new file mode 100644 index 0000000..b983947 --- /dev/null +++ b/includes/admin/class-wc-admin-meta-boxes.php @@ -0,0 +1,255 @@ +'; + + foreach ( $errors as $error ) { + echo '

    ' . wp_kses_post( $error ) . '

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    ' . + '

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

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

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

    '; + ?> +

    + + +

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

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

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

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

    +
    + get_customer_meta_fields(); + + foreach ( $save_fields as $fieldset ) { + + foreach ( $fieldset['fields'] as $key => $field ) { + + if ( isset( $field['type'] ) && 'checkbox' === $field['type'] ) { + update_user_meta( $user_id, $key, isset( $_POST[ $key ] ) ); + } elseif ( isset( $_POST[ $key ] ) ) { + update_user_meta( $user_id, $key, wc_clean( $_POST[ $key ] ) ); + } + } + } + } + + /** + * Get user meta for a given key, with fallbacks to core user info for pre-existing fields. + * + * @since 3.1.0 + * @param int $user_id User ID of the user being edited + * @param string $key Key for user meta field + * @return string + */ + protected function get_user_meta( $user_id, $key ) { + $value = get_user_meta( $user_id, $key, true ); + $existing_fields = array( 'billing_first_name', 'billing_last_name' ); + if ( ! $value && in_array( $key, $existing_fields ) ) { + $value = get_user_meta( $user_id, str_replace( 'billing_', '', $key ), true ); + } elseif ( ! $value && ( 'billing_email' === $key ) ) { + $user = get_userdata( $user_id ); + $value = $user->user_email; + } + + return $value; + } + } + +endif; + +return new WC_Admin_Profile(); diff --git a/includes/admin/class-wc-admin-reports.php b/includes/admin/class-wc-admin-reports.php new file mode 100644 index 0000000..0d5dc43 --- /dev/null +++ b/includes/admin/class-wc-admin-reports.php @@ -0,0 +1,179 @@ + array( + 'title' => __( 'Orders', 'woocommerce' ), + 'reports' => array( + 'sales_by_date' => array( + 'title' => __( 'Sales by date', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + 'sales_by_product' => array( + 'title' => __( 'Sales by product', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + 'sales_by_category' => array( + 'title' => __( 'Sales by category', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + 'coupon_usage' => array( + 'title' => __( 'Coupons by date', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + 'downloads' => array( + 'title' => __( 'Customer downloads', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + ), + ), + 'customers' => array( + 'title' => __( 'Customers', 'woocommerce' ), + 'reports' => array( + 'customers' => array( + 'title' => __( 'Customers vs. guests', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + 'customer_list' => array( + 'title' => __( 'Customer list', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + ), + ), + 'stock' => array( + 'title' => __( 'Stock', 'woocommerce' ), + 'reports' => array( + 'low_in_stock' => array( + 'title' => __( 'Low in stock', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + 'out_of_stock' => array( + 'title' => __( 'Out of stock', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + 'most_stocked' => array( + 'title' => __( 'Most stocked', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + ), + ), + ); + + if ( wc_tax_enabled() ) { + $reports['taxes'] = array( + 'title' => __( 'Taxes', 'woocommerce' ), + 'reports' => array( + 'taxes_by_code' => array( + 'title' => __( 'Taxes by code', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + 'taxes_by_date' => array( + 'title' => __( 'Taxes by date', 'woocommerce' ), + 'description' => '', + 'hide_title' => true, + 'callback' => array( __CLASS__, 'get_report' ), + ), + ), + ); + } + + $reports = apply_filters( 'woocommerce_admin_reports', $reports ); + $reports = apply_filters( 'woocommerce_reports_charts', $reports ); // Backwards compatibility. + + foreach ( $reports as $key => $report_group ) { + if ( isset( $reports[ $key ]['charts'] ) ) { + $reports[ $key ]['reports'] = $reports[ $key ]['charts']; + } + + foreach ( $reports[ $key ]['reports'] as $report_key => $report ) { + if ( isset( $reports[ $key ]['reports'][ $report_key ]['function'] ) ) { + $reports[ $key ]['reports'][ $report_key ]['callback'] = $reports[ $key ]['reports'][ $report_key ]['function']; + } + } + } + + return $reports; + } + + /** + * Get a report from our reports subfolder. + * + * @param string $name + */ + public static function get_report( $name ) { + $name = sanitize_title( str_replace( '_', '-', $name ) ); + $class = 'WC_Report_' . str_replace( '-', '_', $name ); + + include_once apply_filters( 'wc_admin_reports_path', 'reports/class-wc-report-' . $name . '.php', $name, $class ); + + if ( ! class_exists( $class ) ) { + return; + } + + $report = new $class(); + $report->output_report(); + } +} diff --git a/includes/admin/class-wc-admin-settings.php b/includes/admin/class-wc-admin-settings.php new file mode 100644 index 0000000..6950606 --- /dev/null +++ b/includes/admin/class-wc-admin-settings.php @@ -0,0 +1,941 @@ +query->init_query_vars(); + WC()->query->add_endpoints(); + + do_action( 'woocommerce_settings_saved' ); + } + + /** + * Add a message. + * + * @param string $text Message. + */ + public static function add_message( $text ) { + self::$messages[] = $text; + } + + /** + * Add an error. + * + * @param string $text Message. + */ + public static function add_error( $text ) { + self::$errors[] = $text; + } + + /** + * Output messages + errors. + */ + public static function show_messages() { + if ( count( self::$errors ) > 0 ) { + foreach ( self::$errors as $error ) { + echo '

    ' . esc_html( $error ) . '

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

    ' . esc_html( $message ) . '

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

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

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

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

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

    ' . wp_kses_post( $description ) . '

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

    ' . $tooltip_html . '

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

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

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

    +

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

    +

    + +
    + + +

    + +

    +
    + is_wc_admin_active() ) : ?> +

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

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

    + +

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

    + +

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

    + +

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

    + +

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

      +
      +
      +

      +
      +
    • +
    • +
      +

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

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

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

      +
    • +
    + + +
    +

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

    +
    + +

    + plugin_install_info(); ?> + + +

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

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

    '; + $paypal_checkout_description = '

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

    '; + $klarna_checkout_description = '

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

    '; + $klarna_payments_description = '

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

    '; + $square_description = '

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

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

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

    +
    +

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

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

    + plugin_install_info(); ?> + + +

    +
    + + + +

    +

    + +

    +
    + +

    + plugin_install_info(); ?> + + +

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

    +

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

    + + + +

    + +

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

    +
    +

    + +

    + + +
    + +

    + +

    +
      +
    • +

      + +

      +

      + +

      +
    • +
    • +

      + +

      +

      + +

      +
    • +
    • +

      + +

      +

      + +

      +
    • +
    • +

      + +

      +

      + +

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

    + +
    +

    +
    + +
    +
    + + +

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

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

    '; + } else { + echo '

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

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

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

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

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

    +
    +
    Note: Deleting a term will remove it from all products and variations to which it has been assigned. Recreating a term will not automatically assign it back to products.', 'woocommerce' ) ), + array( 'p' => array() ) + ); + } + + /** + * Thumbnail column added to category admin. + * + * @param mixed $columns Columns array. + * @return array + */ + public function product_cat_columns( $columns ) { + $new_columns = array(); + + if ( isset( $columns['cb'] ) ) { + $new_columns['cb'] = $columns['cb']; + unset( $columns['cb'] ); + } + + $new_columns['thumb'] = __( 'Image', 'woocommerce' ); + + $columns = array_merge( $new_columns, $columns ); + $columns['handle'] = ''; + + return $columns; + } + + /** + * Adjust row actions. + * + * @param array $actions Array of actions. + * @param object $term Term object. + * @return array + */ + public function product_cat_row_actions( $actions, $term ) { + $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); + + if ( $default_category_id !== $term->term_id && current_user_can( 'edit_term', $term->term_id ) ) { + $actions['make_default'] = sprintf( + '%s', + wp_nonce_url( 'edit-tags.php?action=make_default&taxonomy=product_cat&post_type=product&tag_ID=' . absint( $term->term_id ), 'make_default_' . absint( $term->term_id ) ), + /* translators: %s: taxonomy term name */ + esc_attr( sprintf( __( 'Make “%s” the default category', 'woocommerce' ), $term->name ) ), + __( 'Make default', 'woocommerce' ) + ); + } + + return $actions; + } + + /** + * Handle custom row actions. + */ + public function handle_product_cat_row_actions() { + if ( isset( $_GET['action'], $_GET['tag_ID'], $_GET['_wpnonce'] ) && 'make_default' === $_GET['action'] ) { // WPCS: CSRF ok, input var ok. + $make_default_id = absint( $_GET['tag_ID'] ); // WPCS: Input var ok. + + if ( wp_verify_nonce( $_GET['_wpnonce'], 'make_default_' . $make_default_id ) && current_user_can( 'edit_term', $make_default_id ) ) { // WPCS: Sanitization ok, input var ok, CSRF ok. + update_option( 'default_product_cat', $make_default_id ); + } + } + } + + /** + * Thumbnail column value added to category admin. + * + * @param string $columns Column HTML output. + * @param string $column Column name. + * @param int $id Product ID. + * + * @return string + */ + public function product_cat_column( $columns, $column, $id ) { + if ( 'thumb' === $column ) { + // Prepend tooltip for default category. + $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); + + if ( $default_category_id === $id ) { + $columns .= wc_help_tip( __( 'This is the default category and it cannot be deleted. It will be automatically assigned to products with no category.', 'woocommerce' ) ); + } + + $thumbnail_id = get_term_meta( $id, 'thumbnail_id', true ); + + if ( $thumbnail_id ) { + $image = wp_get_attachment_thumb_url( $thumbnail_id ); + } else { + $image = wc_placeholder_img_src(); + } + + // Prevent esc_url from breaking spaces in urls for image embeds. Ref: https://core.trac.wordpress.org/ticket/23605 . + $image = str_replace( ' ', '%20', $image ); + $columns .= '' . esc_attr__( 'Thumbnail', 'woocommerce' ) . ''; + } + if ( 'handle' === $column ) { + $columns .= ''; + } + return $columns; + } + + /** + * Maintain term hierarchy when editing a product. + * + * @param array $args Term checklist args. + * @return array + */ + public function disable_checked_ontop( $args ) { + if ( ! empty( $args['taxonomy'] ) && 'product_cat' === $args['taxonomy'] ) { + $args['checked_ontop'] = false; + } + return $args; + } + + /** + * Admin footer scripts for the product categories admin screen + * + * @return void + */ + public function scripts_at_product_cat_screen_footer() { + if ( ! isset( $_GET['taxonomy'] ) || 'product_cat' !== $_GET['taxonomy'] ) { // WPCS: CSRF ok, input var ok. + return; + } + // Ensure the tooltip is displayed when the image column is disabled on product categories. + wc_enqueue_js( + "(function( $ ) { + 'use strict'; + var product_cat = $( 'tr#tag-" . absint( $this->default_cat_id ) . "' ); + product_cat.find( 'th' ).empty(); + product_cat.find( 'td.thumb span' ).detach( 'span' ).appendTo( product_cat.find( 'th' ) ); + })( jQuery );" + ); + } +} + +$wc_admin_taxonomies = WC_Admin_Taxonomies::get_instance(); diff --git a/includes/admin/class-wc-admin-webhooks-table-list.php b/includes/admin/class-wc-admin-webhooks-table-list.php new file mode 100644 index 0000000..d2c6ea5 --- /dev/null +++ b/includes/admin/class-wc-admin-webhooks-table-list.php @@ -0,0 +1,316 @@ + 'webhook', + 'plural' => 'webhooks', + 'ajax' => false, + ) + ); + } + + /** + * No items found text. + */ + public function no_items() { + esc_html_e( 'No webhooks found.', 'woocommerce' ); + } + + /** + * Get list columns. + * + * @return array + */ + public function get_columns() { + return array( + 'cb' => '', + 'title' => __( 'Name', 'woocommerce' ), + 'status' => __( 'Status', 'woocommerce' ), + 'topic' => __( 'Topic', 'woocommerce' ), + 'delivery_url' => __( 'Delivery URL', 'woocommerce' ), + ); + } + + /** + * Column cb. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_cb( $webhook ) { + return sprintf( '', $this->_args['singular'], $webhook->get_id() ); + } + + /** + * Return title column. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_title( $webhook ) { + $edit_link = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=webhooks&edit-webhook=' . $webhook->get_id() ); + $output = ''; + + // Title. + $output .= '' . esc_html( $webhook->get_name() ) . ''; + + // Get actions. + $actions = array( + /* translators: %s: webhook ID. */ + 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $webhook->get_id() ), + 'edit' => '' . esc_html__( 'Edit', 'woocommerce' ) . '', + /* translators: %s: webhook name */ + 'delete' => 'get_name() ) ) . '" href="' . esc_url( + wp_nonce_url( + add_query_arg( + array( + 'delete' => $webhook->get_id(), + ), + admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' ) + ), + 'delete-webhook' + ) + ) . '">' . esc_html__( 'Delete permanently', 'woocommerce' ) . '', + ); + + $actions = apply_filters( 'webhook_row_actions', $actions, $webhook ); + $row_actions = array(); + + foreach ( $actions as $action => $link ) { + $row_actions[] = '' . $link . ''; + } + + $output .= '
    ' . implode( ' | ', $row_actions ) . '
    '; + + return $output; + } + + /** + * Return status column. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_status( $webhook ) { + return $webhook->get_i18n_status(); + } + + /** + * Return topic column. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_topic( $webhook ) { + return $webhook->get_topic(); + } + + /** + * Return delivery URL column. + * + * @param WC_Webhook $webhook Webhook instance. + * @return string + */ + public function column_delivery_url( $webhook ) { + return $webhook->get_delivery_url(); + } + + /** + * Get the status label for webhooks. + * + * @param string $status_name Status name. + * @param int $amount Amount of webhooks. + * @return array + */ + private function get_status_label( $status_name, $amount ) { + $statuses = wc_get_webhook_statuses(); + + if ( isset( $statuses[ $status_name ] ) ) { + return array( + 'singular' => sprintf( '%s (%s)', esc_html( $statuses[ $status_name ] ), $amount ), + 'plural' => sprintf( '%s (%s)', esc_html( $statuses[ $status_name ] ), $amount ), + 'context' => '', + 'domain' => 'woocommerce', + ); + } + + return array( + 'singular' => sprintf( '%s (%s)', esc_html( $status_name ), $amount ), + 'plural' => sprintf( '%s (%s)', esc_html( $status_name ), $amount ), + 'context' => '', + 'domain' => 'woocommerce', + ); + } + + /** + * Table list views. + * + * @return array + */ + protected function get_views() { + $status_links = array(); + $data_store = WC_Data_Store::load( 'webhook' ); + $num_webhooks = $data_store->get_count_webhooks_by_status(); + $total_webhooks = array_sum( (array) $num_webhooks ); + $statuses = array_keys( wc_get_webhook_statuses() ); + $class = empty( $_REQUEST['status'] ) ? ' class="current"' : ''; // WPCS: input var okay. CSRF ok. + + /* translators: %s: count */ + $status_links['all'] = "" . sprintf( _nx( 'All (%s)', 'All (%s)', $total_webhooks, 'posts', 'woocommerce' ), number_format_i18n( $total_webhooks ) ) . ''; + + foreach ( $statuses as $status_name ) { + $class = ''; + + if ( empty( $num_webhooks[ $status_name ] ) ) { + continue; + } + + if ( isset( $_REQUEST['status'] ) && sanitize_key( wp_unslash( $_REQUEST['status'] ) ) === $status_name ) { // WPCS: input var okay, CSRF ok. + $class = ' class="current"'; + } + + $label = $this->get_status_label( $status_name, $num_webhooks[ $status_name ] ); + + $status_links[ $status_name ] = "" . sprintf( translate_nooped_plural( $label, $num_webhooks[ $status_name ] ), number_format_i18n( $num_webhooks[ $status_name ] ) ) . ''; + } + + return $status_links; + } + + /** + * Get bulk actions. + * + * @return array + */ + protected function get_bulk_actions() { + return array( + 'delete' => __( 'Delete permanently', 'woocommerce' ), + ); + } + + /** + * Process bulk actions. + */ + public function process_bulk_action() { + $action = $this->current_action(); + $webhooks = isset( $_REQUEST['webhook'] ) ? array_map( 'absint', (array) $_REQUEST['webhook'] ) : array(); // WPCS: input var okay, CSRF ok. + + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_die( esc_html__( 'You do not have permission to edit Webhooks', 'woocommerce' ) ); + } + + if ( 'delete' === $action ) { + WC_Admin_Webhooks::bulk_delete( $webhooks ); + } + } + + /** + * Generate the table navigation above or below the table. + * Included to remove extra nonce input. + * + * @param string $which The location of the extra table nav markup: 'top' or 'bottom'. + */ + protected function display_tablenav( $which ) { + echo '
    '; + + if ( $this->has_items() ) { + echo '
    '; + $this->bulk_actions( $which ); + echo '
    '; + } + + $this->extra_tablenav( $which ); + $this->pagination( $which ); + echo '
    '; + echo '
    '; + } + + /** + * Search box. + * + * @param string $text Button text. + * @param string $input_id Input ID. + */ + public function search_box( $text, $input_id ) { + if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) { // WPCS: input var okay, CSRF ok. + return; + } + + $input_id = $input_id . '-search-input'; + $search_query = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; // WPCS: input var okay, CSRF ok. + + echo ''; + } + + /** + * Prepare table list items. + */ + public function prepare_items() { + $per_page = $this->get_items_per_page( 'woocommerce_webhooks_per_page' ); + $current_page = $this->get_pagenum(); + + // Query args. + $args = array( + 'limit' => $per_page, + 'offset' => $per_page * ( $current_page - 1 ), + ); + + // Handle the status query. + if ( ! empty( $_REQUEST['status'] ) ) { // WPCS: input var okay, CSRF ok. + $args['status'] = sanitize_key( wp_unslash( $_REQUEST['status'] ) ); // WPCS: input var okay, CSRF ok. + } + + if ( ! empty( $_REQUEST['s'] ) ) { // WPCS: input var okay, CSRF ok. + $args['search'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ); // WPCS: input var okay, CSRF ok. + } + + $args['paginate'] = true; + + // Get the webhooks. + $data_store = WC_Data_Store::load( 'webhook' ); + $webhooks = $data_store->search_webhooks( $args ); + $this->items = array_map( 'wc_get_webhook', $webhooks->webhooks ); + + // Set the pagination. + $this->set_pagination_args( + array( + 'total_items' => $webhooks->total, + 'per_page' => $per_page, + 'total_pages' => $webhooks->max_num_pages, + ) + ); + } +} diff --git a/includes/admin/class-wc-admin-webhooks.php b/includes/admin/class-wc-admin-webhooks.php new file mode 100644 index 0000000..9614c9d --- /dev/null +++ b/includes/admin/class-wc-admin-webhooks.php @@ -0,0 +1,349 @@ +set_name( $name ); + + if ( ! $webhook->get_user_id() ) { + $webhook->set_user_id( get_current_user_id() ); + } + + // Status. + $webhook->set_status( ! empty( $_POST['webhook_status'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_status'] ) ) : 'disabled' ); // WPCS: input var okay, CSRF ok. + + // Delivery URL. + $delivery_url = ! empty( $_POST['webhook_delivery_url'] ) ? esc_url_raw( wp_unslash( $_POST['webhook_delivery_url'] ) ) : ''; // WPCS: input var okay, CSRF ok. + + if ( wc_is_valid_url( $delivery_url ) ) { + $webhook->set_delivery_url( $delivery_url ); + } + + // Secret. + $secret = ! empty( $_POST['webhook_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_secret'] ) ) : wp_generate_password( 50, true, true ); // WPCS: input var okay, CSRF ok. + $webhook->set_secret( $secret ); + + // Topic. + if ( ! empty( $_POST['webhook_topic'] ) ) { // WPCS: input var okay, CSRF ok. + $resource = ''; + $event = ''; + + switch ( $_POST['webhook_topic'] ) { // WPCS: input var okay, CSRF ok. + case 'action': + $resource = 'action'; + $event = ! empty( $_POST['webhook_action_event'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_action_event'] ) ) : ''; // WPCS: input var okay, CSRF ok. + break; + + default: + list( $resource, $event ) = explode( '.', sanitize_text_field( wp_unslash( $_POST['webhook_topic'] ) ) ); // WPCS: input var okay, CSRF ok. + break; + } + + $topic = $resource . '.' . $event; + + if ( wc_is_webhook_valid_topic( $topic ) ) { + $webhook->set_topic( $topic ); + } else { + $errors[] = __( 'Webhook topic unknown. Please select a valid topic.', 'woocommerce' ); + } + } + + // API version. + $rest_api_versions = wc_get_webhook_rest_api_versions(); + $webhook->set_api_version( ! empty( $_POST['webhook_api_version'] ) ? sanitize_text_field( wp_unslash( $_POST['webhook_api_version'] ) ) : end( $rest_api_versions ) ); // WPCS: input var okay, CSRF ok. + + $webhook->save(); + + // Run actions. + do_action( 'woocommerce_webhook_options_save', $webhook->get_id() ); + if ( $errors ) { + // Redirect to webhook edit page to avoid settings save actions. + wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=' . $webhook->get_id() . '&error=' . rawurlencode( implode( '|', $errors ) ) ) ); + exit(); + } elseif ( isset( $_POST['webhook_status'] ) && 'active' === $_POST['webhook_status'] && $webhook->get_pending_delivery() ) { // WPCS: input var okay, CSRF ok. + // Ping the webhook at the first time that is activated. + $result = $webhook->deliver_ping(); + + if ( is_wp_error( $result ) ) { + // Redirect to webhook edit page to avoid settings save actions. + wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=' . $webhook->get_id() . '&error=' . rawurlencode( $result->get_error_message() ) ) ); + exit(); + } + } + + // Redirect to webhook edit page to avoid settings save actions. + wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks&edit-webhook=' . $webhook->get_id() . '&updated=1' ) ); + exit(); + } + + /** + * Bulk delete. + * + * @param array $webhooks List of webhooks IDs. + */ + public static function bulk_delete( $webhooks ) { + foreach ( $webhooks as $webhook_id ) { + $webhook = new WC_Webhook( (int) $webhook_id ); + $webhook->delete( true ); + } + + $qty = count( $webhooks ); + $status = isset( $_GET['status'] ) ? '&status=' . sanitize_text_field( wp_unslash( $_GET['status'] ) ) : ''; // WPCS: input var okay, CSRF ok. + + // Redirect to webhooks page. + wp_safe_redirect( admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=webhooks' . $status . '&deleted=' . $qty ) ); + exit(); + } + + /** + * Delete webhook. + */ + private function delete() { + check_admin_referer( 'delete-webhook' ); + + if ( isset( $_GET['delete'] ) ) { // WPCS: input var okay, CSRF ok. + $webhook_id = absint( $_GET['delete'] ); // WPCS: input var okay, CSRF ok. + + if ( $webhook_id ) { + $this->bulk_delete( array( $webhook_id ) ); + } + } + } + + /** + * Webhooks admin actions. + */ + public function actions() { + if ( $this->is_webhook_settings_page() ) { + // Save. + if ( isset( $_POST['save'] ) && isset( $_POST['webhook_id'] ) ) { // WPCS: input var okay, CSRF ok. + $this->save(); + } + + // Delete webhook. + if ( isset( $_GET['delete'] ) ) { // WPCS: input var okay, CSRF ok. + $this->delete(); + } + } + } + + /** + * Page output. + */ + public static function page_output() { + // Hide the save button. + $GLOBALS['hide_save_button'] = true; + + if ( isset( $_GET['edit-webhook'] ) ) { // WPCS: input var okay, CSRF ok. + $webhook_id = absint( $_GET['edit-webhook'] ); // WPCS: input var okay, CSRF ok. + $webhook = new WC_Webhook( $webhook_id ); + + include __DIR__ . '/settings/views/html-webhooks-edit.php'; + return; + } + + self::table_list_output(); + } + + /** + * Notices. + */ + public static function notices() { + if ( isset( $_GET['deleted'] ) ) { // WPCS: input var okay, CSRF ok. + $deleted = absint( $_GET['deleted'] ); // WPCS: input var okay, CSRF ok. + + /* translators: %d: count */ + WC_Admin_Settings::add_message( sprintf( _n( '%d webhook permanently deleted.', '%d webhooks permanently deleted.', $deleted, 'woocommerce' ), $deleted ) ); + } + + if ( isset( $_GET['updated'] ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::add_message( __( 'Webhook updated successfully.', 'woocommerce' ) ); + } + + if ( isset( $_GET['created'] ) ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::add_message( __( 'Webhook created successfully.', 'woocommerce' ) ); + } + + if ( isset( $_GET['error'] ) ) { // WPCS: input var okay, CSRF ok. + foreach ( explode( '|', sanitize_text_field( wp_unslash( $_GET['error'] ) ) ) as $message ) { // WPCS: input var okay, CSRF ok. + WC_Admin_Settings::add_error( trim( $message ) ); + } + } + } + + /** + * Add screen option. + */ + public function screen_option() { + global $webhooks_table_list; + + if ( ! isset( $_GET['edit-webhook'] ) && $this->is_webhook_settings_page() ) { // WPCS: input var okay, CSRF ok. + $webhooks_table_list = new WC_Admin_Webhooks_Table_List(); + + // Add screen option. + add_screen_option( + 'per_page', + array( + 'default' => 10, + 'option' => 'woocommerce_webhooks_per_page', + ) + ); + } + } + + /** + * Table list output. + */ + private static function table_list_output() { + global $webhooks_table_list; + + echo '

    ' . esc_html__( 'Webhooks', 'woocommerce' ) . ' ' . esc_html__( 'Add webhook', 'woocommerce' ) . '

    '; + + // Get the webhooks count. + $data_store = WC_Data_Store::load( 'webhook' ); + $num_webhooks = $data_store->get_count_webhooks_by_status(); + $count = array_sum( $num_webhooks ); + + if ( 0 < $count ) { + $webhooks_table_list->process_bulk_action(); + $webhooks_table_list->prepare_items(); + + echo ''; + echo ''; + echo ''; + + $webhooks_table_list->views(); + $webhooks_table_list->search_box( __( 'Search webhooks', 'woocommerce' ), 'webhook' ); + $webhooks_table_list->display(); + } else { + echo '
    '; + ?> +

    + + + get_topic(); + $event = ''; + $resource = ''; + + if ( $topic ) { + list( $resource, $event ) = explode( '.', $topic ); + + if ( 'action' === $resource ) { + $topic = 'action'; + } elseif ( ! in_array( $resource, array( 'coupon', 'customer', 'order', 'product' ), true ) ) { + $topic = 'custom'; + } + } + + return array( + 'topic' => $topic, + 'event' => $event, + 'resource' => $resource, + ); + } + + /** + * Get the logs navigation. + * + * @deprecated 3.3.0 + * @param int $total Deprecated. + * @param WC_Webhook $webhook Deprecated. + */ + public static function get_logs_navigation( $total, $webhook ) { + wc_deprecated_function( 'WC_Admin_Webhooks::get_logs_navigation', '3.3' ); + } +} + +new WC_Admin_Webhooks(); diff --git a/includes/admin/class-wc-admin.php b/includes/admin/class-wc-admin.php new file mode 100644 index 0000000..215f061 --- /dev/null +++ b/includes/admin/class-wc-admin.php @@ -0,0 +1,317 @@ +id ) { + case 'dashboard': + case 'dashboard-network': + include __DIR__ . '/class-wc-admin-dashboard-setup.php'; + include __DIR__ . '/class-wc-admin-dashboard.php'; + break; + case 'options-permalink': + include __DIR__ . '/class-wc-admin-permalink-settings.php'; + break; + case 'plugins': + include __DIR__ . '/plugin-updates/class-wc-plugins-screen-updates.php'; + break; + case 'update-core': + include __DIR__ . '/plugin-updates/class-wc-updates-screen-updates.php'; + break; + case 'users': + case 'user': + case 'profile': + case 'user-edit': + include __DIR__ . '/class-wc-admin-profile.php'; + break; + } + } + + /** + * Handle redirects to setup/welcome page after install and updates. + * + * The user must have access rights, and we must ignore the network/bulk plugin updaters. + */ + public function admin_redirects() { + // Don't run this fn from Action Scheduler requests, as it would clear _wc_activation_redirect transient. + // That means OBW would never be shown. + if ( wc_is_running_from_async_action_scheduler() ) { + return; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + // Nonced plugin install redirects. + if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) { + $plugin_slug = wc_clean( wp_unslash( $_GET['wc-install-plugin-redirect'] ) ); + + if ( current_user_can( 'install_plugins' ) && in_array( $plugin_slug, array( 'woocommerce-gateway-stripe' ), true ) ) { + $nonce = wp_create_nonce( 'install-plugin_' . $plugin_slug ); + $url = self_admin_url( 'update.php?action=install-plugin&plugin=' . $plugin_slug . '&_wpnonce=' . $nonce ); + } else { + $url = admin_url( 'plugin-install.php?tab=search&type=term&s=' . $plugin_slug ); + } + + wp_safe_redirect( $url ); + exit; + } + + // phpcs:enable WordPress.Security.NonceVerification.Recommended + } + + /** + * Prevent any user who cannot 'edit_posts' (subscribers, customers etc) from accessing admin. + */ + public function prevent_admin_access() { + $prevent_access = false; + + if ( apply_filters( 'woocommerce_disable_admin_bar', true ) && ! is_ajax() && isset( $_SERVER['SCRIPT_FILENAME'] ) && basename( sanitize_text_field( wp_unslash( $_SERVER['SCRIPT_FILENAME'] ) ) ) !== 'admin-post.php' ) { + $has_cap = false; + $access_caps = array( 'edit_posts', 'manage_woocommerce', 'view_admin_dashboard' ); + + foreach ( $access_caps as $access_cap ) { + if ( current_user_can( $access_cap ) ) { + $has_cap = true; + break; + } + } + + if ( ! $has_cap ) { + $prevent_access = true; + } + } + + if ( apply_filters( 'woocommerce_prevent_admin_access', $prevent_access ) ) { + wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); + exit; + } + } + + /** + * Preview email template. + */ + public function preview_emails() { + + if ( isset( $_GET['preview_woocommerce_mail'] ) ) { + if ( ! ( isset( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'preview-mail' ) ) ) { + die( 'Security check' ); + } + + // load the mailer class. + $mailer = WC()->mailer(); + + // get the preview email subject. + $email_heading = __( 'HTML email template', 'woocommerce' ); + + // get the preview email content. + ob_start(); + include __DIR__ . '/views/html-email-template-preview.php'; + $message = ob_get_clean(); + + // create a new email. + $email = new WC_Email(); + + // wrap the content with the email template and then add styles. + $message = apply_filters( 'woocommerce_mail_content', $email->style_inline( $mailer->wrap_message( $email_heading, $message ) ) ); + + // print the preview email. + // phpcs:ignore WordPress.Security.EscapeOutput + echo $message; + // phpcs:enable + exit; + } + } + + /** + * Change the admin footer text on WooCommerce admin pages. + * + * @since 2.3 + * @param string $footer_text text to be rendered in the footer. + * @return string + */ + public function admin_footer_text( $footer_text ) { + if ( ! current_user_can( 'manage_woocommerce' ) || ! function_exists( 'wc_get_screen_ids' ) ) { + return $footer_text; + } + $current_screen = get_current_screen(); + $wc_pages = wc_get_screen_ids(); + + // Set only WC pages. + $wc_pages = array_diff( $wc_pages, array( 'profile', 'user-edit' ) ); + + // Check to make sure we're on a WooCommerce admin page. + if ( isset( $current_screen->id ) && apply_filters( 'woocommerce_display_admin_footer_text', in_array( $current_screen->id, $wc_pages, true ) ) ) { + // Change the footer text. + if ( ! get_option( 'woocommerce_admin_footer_text_rated' ) ) { + $footer_text = sprintf( + /* translators: 1: WooCommerce 2:: five stars */ + __( 'If you like %1$s please leave us a %2$s rating. A huge thanks in advance!', 'woocommerce' ), + sprintf( '%s', esc_html__( 'WooCommerce', 'woocommerce' ) ), + '★★★★★' + ); + wc_enqueue_js( + "jQuery( 'a.wc-rating-link' ).on( 'click', function() { + jQuery.post( '" . WC()->ajax_url() . "', { action: 'woocommerce_rated' } ); + jQuery( this ).parent().text( jQuery( this ).data( 'rated' ) ); + });" + ); + } else { + $footer_text = __( 'Thank you for selling with WooCommerce.', 'woocommerce' ); + } + } + + return $footer_text; + } + + /** + * Check on a Jetpack install queued by the Setup Wizard. + * + * See: WC_Admin_Setup_Wizard::install_jetpack() + */ + public function setup_wizard_check_jetpack() { + $jetpack_active = class_exists( 'Jetpack' ); + + wp_send_json_success( + array( + 'is_active' => $jetpack_active ? 'yes' : 'no', + ) + ); + } + + /** + * Disable WXR export of scheduled action posts. + * + * @since 3.6.2 + * + * @param array $args Scehduled action post type registration args. + * + * @return array + */ + public function disable_webhook_post_export( $args ) { + $args['can_export'] = false; + return $args; + } + + /** + * Include admin classes. + * + * @since 4.2.0 + * @param string $classes Body classes string. + * @return string + */ + public function include_admin_body_class( $classes ) { + if ( in_array( array( 'wc-wp-version-gte-53', 'wc-wp-version-gte-55' ), explode( ' ', $classes ), true ) ) { + return $classes; + } + + $raw_version = get_bloginfo( 'version' ); + $version_parts = explode( '-', $raw_version ); + $version = count( $version_parts ) > 1 ? $version_parts[0] : $raw_version; + + // Add WP 5.3+ compatibility class. + if ( $raw_version && version_compare( $version, '5.3', '>=' ) ) { + $classes .= ' wc-wp-version-gte-53'; + } + + // Add WP 5.5+ compatibility class. + if ( $raw_version && version_compare( $version, '5.5', '>=' ) ) { + $classes .= ' wc-wp-version-gte-55'; + } + + return $classes; + } +} + +return new WC_Admin(); diff --git a/includes/admin/helper/class-wc-helper-api.php b/includes/admin/helper/class-wc-helper-api.php new file mode 100644 index 0000000..50ce432 --- /dev/null +++ b/includes/admin/helper/class-wc-helper-api.php @@ -0,0 +1,170 @@ + 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(); + } + + $headers = array( + 'Authorization' => 'Bearer ' . $auth['access_token'], + 'X-Woo-Signature' => $signature, + ); + $args['headers'] = wp_parse_args( $headers, $args['headers'] ); + + $url = add_query_arg( + array( + 'token' => $auth['access_token'], + 'signature' => $signature, + ), + $url + ); + + return true; + } + + /** + * 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 ); + } + + /** + * 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 put( $endpoint, $args = array() ) { + $args['method'] = 'PUT'; + 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 0000000..50d02ff --- /dev/null +++ b/includes/admin/helper/class-wc-helper-compat.php @@ -0,0 +1,204 @@ +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' ); + + // Notify the user when the plugin is deactivated. + add_action( 'pre_current_active_plugins', array( __CLASS__, 'plugin_deactivation_notice' ) ); + } + } + + /** + * Display admin notice directing the user where to go. + */ + public static function plugin_deactivation_notice() { + ?> +
    +

    Manage subscriptions from the extensions tab instead.', 'woocommerce' ), esc_url( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ) ); ?>

    +
    + '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 0000000..4f1611c --- /dev/null +++ b/includes/admin/helper/class-wc-helper-options.php @@ -0,0 +1,60 @@ +slug ) ) { + return $response; + } + + // Only for slugs that start with woo- + if ( 0 !== strpos( $args->slug, 'woocommerce-com-' ) ) { + return $response; + } + + $clean_slug = str_replace( 'woocommerce-com-', '', $args->slug ); + + // Look through update data by slug. + $update_data = WC_Helper_Updater::get_update_data(); + $products = wp_list_filter( $update_data, array( 'slug' => $clean_slug ) ); + + if ( empty( $products ) ) { + return $response; + } + + $product_id = array_keys( $products ); + $product_id = array_shift( $product_id ); + + // Fetch the product information from the Helper API. + $request = WC_Helper_API::get( + add_query_arg( + array( + 'product_id' => absint( $product_id ), + ), + '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 0000000..25ec134 --- /dev/null +++ b/includes/admin/helper/class-wc-helper-updater.php @@ -0,0 +1,492 @@ + 'woocommerce-com-' . $plugin['_product_id'], + 'slug' => 'woocommerce-com-' . $data['slug'], + 'plugin' => $filename, + 'new_version' => $data['version'], + 'url' => $data['url'], + 'package' => $data['package'], + 'upgrade_notice' => $data['upgrade_notice'], + ); + + if ( isset( $data['requires_php'] ) ) { + $item['requires_php'] = $data['requires_php']; + } + + // We don't want to deliver a valid upgrade package when their subscription has expired. + // To avoid the generic "no_package" error that empty strings give, we will store an + // indication of expiration for the `upgrader_pre_download` filter to error on. + if ( ! self::_has_active_subscription( $plugin['_product_id'] ) ) { + $item['package'] = 'woocommerce-com-expired-' . $plugin['_product_id']; + } + + 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 ] ); + } + } + + $translations = self::get_translations_update_data(); + $transient->translations = array_merge( isset( $transient->translations ) ? $transient->translations : array(), $translations ); + + 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 ); + } + + /** + * Get translations updates informations. + * + * 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_translations_update_data() { + $payload = array(); + + $installed_translations = wp_get_installed_translations( 'plugins' ); + + $locales = array_values( get_available_languages() ); + /** + * Filters the locales requested for plugin translations. + * + * @since 3.7.0 + * @since 4.5.0 The default value of the `$locales` parameter changed to include all locales. + * + * @param array $locales Plugin locales. Default is all available locales of the site. + */ + $locales = apply_filters( 'plugins_update_check_locales', $locales ); + $locales = array_unique( $locales ); + + // No locales, the respone will be empty, we can return now. + if ( empty( $locales ) ) { + return array(); + } + + // Scan local plugins which may or may not have a subscription. + $plugins = WC_Helper::get_local_woo_plugins(); + $active_woo_plugins = array_intersect( array_keys( $plugins ), get_option( 'active_plugins', array() ) ); + + /* + * Use only plugins that are subscribed to the automatic translations updates. + */ + $active_for_translations = array_filter( + $active_woo_plugins, + function( $plugin ) use ( $plugins ) { + return apply_filters( 'woocommerce_translations_updates_for_' . $plugins[ $plugin ]['slug'], false ); + } + ); + + // Nothing to check for, exit. + if ( empty( $active_for_translations ) ) { + return array(); + } + + if ( wp_doing_cron() ) { + $timeout = 30; + } else { + // Three seconds, plus one extra second for every 10 plugins. + $timeout = 3 + (int) ( count( $active_for_translations ) / 10 ); + } + + $request_body = array( + 'locales' => $locales, + 'plugins' => array(), + ); + + foreach ( $active_for_translations as $active_plugin ) { + $plugin = $plugins[ $active_plugin ]; + $request_body['plugins'][ $plugin['slug'] ] = array( 'version' => $plugin['Version'] ); + } + + $raw_response = wp_remote_post( + 'https://translate.wordpress.com/api/translations-updates/woocommerce', + array( + 'body' => json_encode( $request_body ), + 'headers' => array( 'Content-Type: application/json' ), + 'timeout' => $timeout, + ) + ); + + // Something wrong happened on the translate server side. + $response_code = wp_remote_retrieve_response_code( $raw_response ); + if ( 200 !== $response_code ) { + return array(); + } + + $response = json_decode( wp_remote_retrieve_body( $raw_response ), true ); + + // API error, api returned but something was wrong. + if ( array_key_exists( 'success', $response ) && false === $response['success'] ) { + return array(); + } + + $translations = array(); + + foreach ( $response['data'] as $plugin_name => $language_packs ) { + foreach ( $language_packs as $language_pack ) { + // Maybe we have this language pack already installed so lets check revision date. + if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) { + $installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] ); + $new_translation_revision_time = new DateTime( $language_pack['last_modified'] ); + // Skip if translation language pack is not newer than what is installed already. + if ( $new_translation_revision_time <= $installed_translation_revision_time ) { + continue; + } + } + $translations[] = array( + 'type' => 'plugin', + 'slug' => $plugin_name, + 'language' => $language_pack['wp_locale'], + 'version' => $language_pack['version'], + 'updated' => $language_pack['last_modified'], + 'package' => $language_pack['package'], + 'autoupdate' => true, + ); + } + } + + return $translations; + } + + /** + * 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. + * + * @param array $payload Information about the plugin to update. + * @return array Update data for each requested product. + */ + private static function _update_check( $payload ) { + ksort( $payload ); + $hash = md5( wp_json_encode( $payload ) ); + + $cache_key = '_woocommerce_helper_updates'; + $data = get_transient( $cache_key ); + if ( false !== $data ) { + 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' => wp_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; + } + + /** + * Get the number of products that have updates. + * + * @return int The number of products with updates. + */ + public static function get_updates_count() { + $cache_key = '_woocommerce_helper_updates_count'; + $count = get_transient( $cache_key ); + if ( false !== $count ) { + return $count; + } + + // Don't fetch any new data since this function in high-frequency. + if ( ! get_transient( '_woocommerce_helper_subscriptions' ) ) { + return 0; + } + + if ( ! get_transient( '_woocommerce_helper_updates' ) ) { + return 0; + } + + $count = 0; + $update_data = self::get_update_data(); + + if ( empty( $update_data ) ) { + set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS ); + return $count; + } + + // Scan local plugins. + foreach ( WC_Helper::get_local_woo_plugins() as $plugin ) { + if ( empty( $update_data[ $plugin['_product_id'] ] ) ) { + continue; + } + + if ( version_compare( $plugin['Version'], $update_data[ $plugin['_product_id'] ]['version'], '<' ) ) { + $count++; + } + } + + // Scan local themes. + foreach ( WC_Helper::get_local_woo_themes() as $theme ) { + if ( empty( $update_data[ $theme['_product_id'] ] ) ) { + continue; + } + + if ( version_compare( $theme['Version'], $update_data[ $theme['_product_id'] ]['version'], '<' ) ) { + $count++; + } + } + + set_transient( $cache_key, $count, 12 * HOUR_IN_SECONDS ); + return $count; + } + + /** + * Return the updates count markup. + * + * @return string Updates count markup, empty string if no updates avairable. + */ + public static function get_updates_count_html() { + $count = self::get_updates_count(); + if ( ! $count ) { + return ''; + } + + $count_html = sprintf( '%d', $count, number_format_i18n( $count ) ); + return $count_html; + } + + /** + * Flushes cached update data. + */ + public static function flush_updates_cache() { + delete_transient( '_woocommerce_helper_updates' ); + delete_transient( '_woocommerce_helper_updates_count' ); + delete_site_transient( 'update_plugins' ); + delete_site_transient( 'update_themes' ); + } + + /** + * Fires when a user successfully updated a theme or a plugin. + */ + public static function upgrader_process_complete() { + delete_transient( '_woocommerce_helper_updates_count' ); + } + + /** + * Hooked into the upgrader_pre_download filter in order to better handle error messaging around expired + * plugin updates. Initially we were using an empty string, but the error message that no_package + * results in does not fit the cause. + * + * @since 4.1.0 + * @param bool $reply Holds the current filtered response. + * @param string $package The path to the package file for the update. + * @return false|WP_Error False to proceed with the update as normal, anything else to be returned instead of updating. + */ + public static function block_expired_updates( $reply, $package ) { + // Don't override a reply that was set already. + if ( false !== $reply ) { + return $reply; + } + + // Only for packages with expired subscriptions. + if ( 0 !== strpos( $package, 'woocommerce-com-expired-' ) ) { + return false; + } + + return new WP_Error( + 'woocommerce_subscription_expired', + sprintf( + // translators: %s: URL of WooCommerce.com subscriptions tab. + __( 'Please visit the subscriptions page and renew to continue receiving updates.', 'woocommerce' ), + esc_url( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ) + ) + ); + } +} + +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 0000000..6fb43c2 --- /dev/null +++ b/includes/admin/helper/class-wc-helper.php @@ -0,0 +1,1641 @@ + 'wc-addons', + 'section' => 'helper', + 'wc-helper-connect' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'connect' ), + ), + admin_url( 'admin.php' ) + ); + + include self::get_view_filename( 'html-oauth-start.php' ); + return; + } + $disconnect_url = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-disconnect' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'disconnect' ), + ), + admin_url( 'admin.php' ) + ); + + $current_filter = self::get_current_filter(); + $refresh_url = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => $current_filter, + 'wc-helper-refresh' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'refresh' ), + ), + admin_url( 'admin.php' ) + ); + + // Installed plugins and themes, with or without an active subscription. + $woo_plugins = self::get_local_woo_plugins(); + $woo_themes = self::get_local_woo_themes(); + + $site_id = absint( $auth['site_id'] ); + $subscriptions = self::get_subscriptions(); + $updates = WC_Helper_Updater::get_update_data(); + $subscriptions_product_ids = wp_list_pluck( $subscriptions, 'product_id' ); + + foreach ( $subscriptions as &$subscription ) { + $subscription['active'] = in_array( $site_id, $subscription['connections'] ); + + $subscription['activate_url'] = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => $current_filter, + 'wc-helper-activate' => 1, + 'wc-helper-product-key' => $subscription['product_key'], + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'activate:' . $subscription['product_key'] ), + ), + admin_url( 'admin.php' ) + ); + + $subscription['deactivate_url'] = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => $current_filter, + 'wc-helper-deactivate' => 1, + 'wc-helper-product-key' => $subscription['product_key'], + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'deactivate:' . $subscription['product_key'] ), + ), + admin_url( 'admin.php' ) + ); + + $subscription['local'] = array( + 'installed' => false, + 'active' => false, + 'version' => null, + ); + + $subscription['update_url'] = admin_url( 'update-core.php' ); + + $local = wp_list_filter( array_merge( $woo_plugins, $woo_themes ), array( '_product_id' => $subscription['product_id'] ) ); + + if ( ! empty( $local ) ) { + $local = array_shift( $local ); + $subscription['local']['installed'] = true; + $subscription['local']['version'] = $local['Version']; + + if ( 'plugin' == $local['_type'] ) { + if ( is_plugin_active( $local['_filename'] ) ) { + $subscription['local']['active'] = true; + } elseif ( is_multisite() && is_plugin_active_for_network( $local['_filename'] ) ) { + $subscription['local']['active'] = true; + } + + // A magic update_url. + $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $local['_filename'], 'upgrade-plugin_' . $local['_filename'] ); + + } elseif ( 'theme' == $local['_type'] ) { + if ( in_array( $local['_stylesheet'], array( get_stylesheet(), get_template() ) ) ) { + $subscription['local']['active'] = true; + } + + // Another magic update_url. + $subscription['update_url'] = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-theme&theme=' . $local['_stylesheet'] ), 'upgrade-theme_' . $local['_stylesheet'] ); + } + } + + $subscription['has_update'] = false; + if ( $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { + $subscription['has_update'] = version_compare( $updates[ $subscription['product_id'] ]['version'], $subscription['local']['version'], '>' ); + } + + $subscription['download_primary'] = true; + $subscription['download_url'] = 'https://woocommerce.com/my-account/downloads/'; + if ( ! $subscription['local']['installed'] && ! empty( $updates[ $subscription['product_id'] ] ) ) { + $subscription['download_url'] = $updates[ $subscription['product_id'] ]['package']; + } + + $subscription['actions'] = array(); + + if ( $subscription['has_update'] && ! $subscription['expired'] ) { + $action = array( + /* translators: %s: version number */ + 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), + 'button_label' => __( 'Update', 'woocommerce' ), + 'button_url' => $subscription['update_url'], + 'status' => 'update-available', + 'icon' => 'dashicons-update', + ); + + // Subscription is not active on this site. + if ( ! $subscription['active'] ) { + $action['message'] .= ' ' . __( 'To enable this update you need to activate this subscription.', 'woocommerce' ); + $action['button_label'] = null; + $action['button_url'] = null; + } + + $subscription['actions'][] = $action; + } + + if ( $subscription['has_update'] && $subscription['expired'] ) { + $action = array( + /* translators: %s: version number */ + 'message' => sprintf( __( 'Version %s is available.', 'woocommerce' ), esc_html( $updates[ $subscription['product_id'] ]['version'] ) ), + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $action['message'] .= ' ' . __( 'To enable this update you need to purchase a new subscription.', 'woocommerce' ); + $action['button_label'] = __( 'Purchase', 'woocommerce' ); + $action['button_url'] = $subscription['product_url']; + + $subscription['actions'][] = $action; + } elseif ( $subscription['expired'] && ! empty( $subscription['master_user_email'] ) ) { + $action = array( + 'message' => sprintf( __( 'This subscription has expired. Contact the owner to renew the subscription to receive updates and support.', 'woocommerce' ) ), + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['actions'][] = $action; + } elseif ( $subscription['expired'] ) { + $action = array( + 'message' => sprintf( __( 'This subscription has expired. Please renew to receive updates and support.', 'woocommerce' ) ), + 'button_label' => __( 'Renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['actions'][] = $action; + } + + if ( $subscription['expiring'] && ! $subscription['autorenew'] ) { + $action = array( + 'message' => __( 'Subscription is expiring soon.', 'woocommerce' ), + 'button_label' => __( 'Enable auto-renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['download_primary'] = false; + $subscription['actions'][] = $action; + } elseif ( $subscription['expiring'] ) { + $action = array( + 'message' => sprintf( __( 'This subscription is expiring soon. Please renew to continue receiving updates and support.', 'woocommerce' ) ), + 'button_label' => __( 'Renew', 'woocommerce' ), + 'button_url' => 'https://woocommerce.com/my-account/my-subscriptions/', + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $subscription['download_primary'] = false; + $subscription['actions'][] = $action; + } + + // Mark the first action primary. + foreach ( $subscription['actions'] as $key => $action ) { + if ( ! empty( $action['button_label'] ) ) { + $subscription['actions'][ $key ]['primary'] = true; + break; + } + } + } + + // Break the by-ref. + unset( $subscription ); + + // Installed products without a subscription. + $no_subscriptions = array(); + foreach ( array_merge( $woo_plugins, $woo_themes ) as $filename => $data ) { + if ( in_array( $data['_product_id'], $subscriptions_product_ids ) ) { + continue; + } + + $data['_product_url'] = '#'; + $data['_has_update'] = false; + + if ( ! empty( $updates[ $data['_product_id'] ] ) ) { + $data['_has_update'] = version_compare( $updates[ $data['_product_id'] ]['version'], $data['Version'], '>' ); + + if ( ! empty( $updates[ $data['_product_id'] ]['url'] ) ) { + $data['_product_url'] = $updates[ $data['_product_id'] ]['url']; + } elseif ( ! empty( $data['PluginURI'] ) ) { + $data['_product_url'] = $data['PluginURI']; + } + } + + $data['_actions'] = array(); + + if ( $data['_has_update'] ) { + $action = array( + /* translators: %s: version number */ + 'message' => sprintf( __( 'Version %s is available. To enable this update you need to purchase a new subscription.', 'woocommerce' ), esc_html( $updates[ $data['_product_id'] ]['version'] ) ), + 'button_label' => __( 'Purchase', 'woocommerce' ), + 'button_url' => $data['_product_url'], + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $data['_actions'][] = $action; + } else { + $action = array( + /* translators: 1: subscriptions docs 2: subscriptions docs */ + 'message' => sprintf( __( 'To receive updates and support for this extension, you need to purchase a new subscription or consolidate your extensions to one connected account by sharing or transferring this extension to this connected account.', 'woocommerce' ), 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-10', 'https://docs.woocommerce.com/document/managing-woocommerce-com-subscriptions/#section-5' ), + 'button_label' => __( 'Purchase', 'woocommerce' ), + 'button_url' => $data['_product_url'], + 'status' => 'expired', + 'icon' => 'dashicons-info', + ); + + $data['_actions'][] = $action; + } + + $no_subscriptions[ $filename ] = $data; + } + + // Update the user id if it came from a migrated connection. + if ( empty( $auth['user_id'] ) ) { + $auth['user_id'] = get_current_user_id(); + WC_Helper_Options::update( 'auth', $auth ); + } + + // Sort alphabetically. + uasort( $subscriptions, array( __CLASS__, '_sort_by_product_name' ) ); + uasort( $no_subscriptions, array( __CLASS__, '_sort_by_name' ) ); + + // Filters. + self::get_filters_counts( $subscriptions ); // Warm it up. + self::_filter( $subscriptions, self::get_current_filter() ); + + // We have an active connection. + include self::get_view_filename( 'html-main.php' ); + return; + } + + /** + * Get available subscriptions filters. + * + * @return array An array of filter keys and labels. + */ + public static function get_filters() { + $filters = array( + 'all' => __( 'All', 'woocommerce' ), + 'active' => __( 'Active', 'woocommerce' ), + 'inactive' => __( 'Inactive', 'woocommerce' ), + 'installed' => __( 'Installed', 'woocommerce' ), + 'update-available' => __( 'Update Available', 'woocommerce' ), + 'expiring' => __( 'Expiring Soon', 'woocommerce' ), + 'expired' => __( 'Expired', 'woocommerce' ), + 'download' => __( 'Download', 'woocommerce' ), + ); + + return $filters; + } + + /** + * Get counts data for the filters array. + * + * @param array $subscriptions The array of all available subscriptions. + * + * @return array Filter counts (filter => count). + */ + public static function get_filters_counts( $subscriptions = null ) { + static $filters; + + if ( isset( $filters ) ) { + return $filters; + } + + $filters = array_fill_keys( array_keys( self::get_filters() ), 0 ); + if ( empty( $subscriptions ) ) { + return array(); + } + + foreach ( $filters as $key => $count ) { + $_subs = $subscriptions; + self::_filter( $_subs, $key ); + $filters[ $key ] = count( $_subs ); + } + + return $filters; + } + + /** + * Get current filter. + * + * @return string The current filter. + */ + public static function get_current_filter() { + $current_filter = 'all'; + $valid_filters = array_keys( self::get_filters() ); + + if ( ! empty( $_GET['filter'] ) && in_array( wp_unslash( $_GET['filter'] ), $valid_filters ) ) { + $current_filter = wc_clean( wp_unslash( $_GET['filter'] ) ); + } + + return $current_filter; + } + + /** + * Filter an array of subscriptions by $filter. + * + * @param array $subscriptions The subscriptions array, passed by ref. + * @param string $filter The filter. + */ + private static function _filter( &$subscriptions, $filter ) { + switch ( $filter ) { + case 'active': + $subscriptions = wp_list_filter( $subscriptions, array( 'active' => true ) ); + break; + + case 'inactive': + $subscriptions = wp_list_filter( $subscriptions, array( 'active' => false ) ); + break; + + case 'installed': + foreach ( $subscriptions as $key => $subscription ) { + if ( empty( $subscription['local']['installed'] ) ) { + unset( $subscriptions[ $key ] ); + } + } + break; + + case 'update-available': + $subscriptions = wp_list_filter( $subscriptions, array( 'has_update' => true ) ); + break; + + case 'expiring': + $subscriptions = wp_list_filter( $subscriptions, array( 'expiring' => true ) ); + break; + + case 'expired': + $subscriptions = wp_list_filter( $subscriptions, array( 'expired' => true ) ); + break; + + case 'download': + foreach ( $subscriptions as $key => $subscription ) { + if ( $subscription['local']['installed'] || $subscription['expired'] ) { + unset( $subscriptions[ $key ] ); + } + } + break; + } + } + + /** + * Enqueue admin scripts and styles. + */ + public static function admin_enqueue_scripts() { + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + + if ( $wc_screen_id . '_page_wc-addons' === $screen_id && isset( $_GET['section'] ) && 'helper' === $_GET['section'] ) { + wp_enqueue_style( 'woocommerce-helper', WC()->plugin_url() . '/assets/css/helper.css', array(), Constants::get_constant( 'WC_VERSION' ) ); + wp_style_add_data( 'woocommerce-helper', 'rtl', 'replace' ); + } + } + + /** + * Various success/error notices. + * + * Runs during admin page render, so no headers/redirects here. + * + * @return array Array pairs of message/type strings with notices. + */ + private static function _get_return_notices() { + $return_status = isset( $_GET['wc-helper-status'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-status'] ) ) : null; + $notices = array(); + + switch ( $return_status ) { + case 'activate-success': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'updated', + 'message' => sprintf( + /* translators: %s: product name */ + __( '%s activated successfully. You will now receive updates for this product.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ), + ); + break; + + case 'activate-error': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'error', + 'message' => sprintf( + /* translators: %s: product name */ + __( 'An error has occurred when activating %s. Please try again later.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ), + ); + break; + + case 'deactivate-success': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $local = self::_get_local_from_product_id( $product_id ); + + $message = sprintf( + /* translators: %s: product name */ + __( 'Subscription for %s deactivated successfully. You will no longer receive updates for this product.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ); + + if ( $local && is_plugin_active( $local['_filename'] ) && current_user_can( 'activate_plugins' ) ) { + $deactivate_plugin_url = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-deactivate-plugin' => 1, + 'wc-helper-product-id' => $subscription['product_id'], + 'wc-helper-nonce' => wp_create_nonce( 'deactivate-plugin:' . $subscription['product_id'] ), + ), + admin_url( 'admin.php' ) + ); + + $message = sprintf( + /* translators: %1$s: product name, %2$s: deactivate url */ + __( 'Subscription for %1$s deactivated successfully. You will no longer receive updates for this product. Click here if you wish to deactivate the plugin as well.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '', + esc_url( $deactivate_plugin_url ) + ); + } + + $notices[] = array( + 'message' => $message, + 'type' => 'updated', + ); + break; + + case 'deactivate-error': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'error', + 'message' => sprintf( + /* translators: %s: product name */ + __( 'An error has occurred when deactivating the subscription for %s. Please try again later.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ), + ); + break; + + case 'deactivate-plugin-success': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'updated', + 'message' => sprintf( + /* translators: %s: product name */ + __( 'The extension %s has been deactivated successfully.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '' + ), + ); + break; + + case 'deactivate-plugin-error': + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $subscription = self::_get_subscriptions_from_product_id( $product_id ); + $notices[] = array( + 'type' => 'error', + 'message' => sprintf( + /* translators: %1$s: product name, %2$s: plugins screen url */ + __( 'An error has occurred when deactivating the extension %1$s. Please proceed to the Plugins screen to deactivate it manually.', 'woocommerce' ), + '' . esc_html( $subscription['product_name'] ) . '', + admin_url( 'plugins.php' ) + ), + ); + break; + + case 'helper-connected': + $notices[] = array( + 'message' => __( 'You have successfully connected your store to WooCommerce.com', 'woocommerce' ), + 'type' => 'updated', + ); + break; + + case 'helper-disconnected': + $notices[] = array( + 'message' => __( 'You have successfully disconnected your store from WooCommerce.com', 'woocommerce' ), + 'type' => 'updated', + ); + break; + + case 'helper-refreshed': + $notices[] = array( + 'message' => __( 'Authentication and subscription caches refreshed successfully.', 'woocommerce' ), + 'type' => 'updated', + ); + break; + } + + return $notices; + } + + /** + * Various early-phase actions with possible redirects. + * + * @param object $screen WP screen object. + */ + public static function current_screen( $screen ) { + $wc_screen_id = sanitize_title( __( 'WooCommerce', 'woocommerce' ) ); + + if ( $wc_screen_id . '_page_wc-addons' !== $screen->id ) { + return; + } + + if ( empty( $_GET['section'] ) || 'helper' !== $_GET['section'] ) { + return; + } + + if ( ! empty( $_GET['wc-helper-connect'] ) ) { + return self::_helper_auth_connect(); + } + + if ( ! empty( $_GET['wc-helper-return'] ) ) { + return self::_helper_auth_return(); + } + + if ( ! empty( $_GET['wc-helper-disconnect'] ) ) { + return self::_helper_auth_disconnect(); + } + + if ( ! empty( $_GET['wc-helper-refresh'] ) ) { + return self::_helper_auth_refresh(); + } + + if ( ! empty( $_GET['wc-helper-activate'] ) ) { + return self::_helper_subscription_activate(); + } + + if ( ! empty( $_GET['wc-helper-deactivate'] ) ) { + return self::_helper_subscription_deactivate(); + } + + if ( ! empty( $_GET['wc-helper-deactivate-plugin'] ) ) { + return self::_helper_plugin_deactivate(); + } + } + + /** + * Initiate a new OAuth connection. + */ + private static function _helper_auth_connect() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_auth_connect' ); + wp_die( 'Could not verify nonce' ); + } + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-return' => 1, + 'wc-helper-nonce' => wp_create_nonce( 'connect' ), + ), + admin_url( 'admin.php' ) + ); + + $request = WC_Helper_API::post( + 'oauth/request_token', + array( + 'body' => array( + 'home_url' => home_url(), + 'redirect_uri' => $redirect_uri, + ), + ) + ); + + $code = wp_remote_retrieve_response_code( $request ); + + if ( 200 !== $code ) { + self::log( sprintf( 'Call to oauth/request_token returned a non-200 response code (%d)', $code ) ); + wp_die( 'Something went wrong' ); + } + + $secret = json_decode( wp_remote_retrieve_body( $request ) ); + if ( empty( $secret ) ) { + self::log( sprintf( 'Call to oauth/request_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); + wp_die( 'Something went wrong' ); + } + + /** + * Fires when the Helper connection process is initiated. + */ + do_action( 'woocommerce_helper_connect_start' ); + + $connect_url = add_query_arg( + array( + 'home_url' => rawurlencode( home_url() ), + 'redirect_uri' => rawurlencode( $redirect_uri ), + 'secret' => rawurlencode( $secret ), + ), + WC_Helper_API::url( 'oauth/authorize' ) + ); + + wp_redirect( esc_url_raw( $connect_url ) ); + die(); + } + + /** + * Return from WooCommerce.com OAuth flow. + */ + private static function _helper_auth_return() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'connect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_auth_return' ); + wp_die( 'Something went wrong' ); + } + + // Bail if the user clicked deny. + if ( ! empty( $_GET['deny'] ) ) { + /** + * Fires when the Helper connection process is denied/cancelled. + */ + do_action( 'woocommerce_helper_denied' ); + wp_safe_redirect( admin_url( 'admin.php?page=wc-addons§ion=helper' ) ); + die(); + } + + // We do need a request token... + if ( empty( $_GET['request_token'] ) ) { + self::log( 'Request token not found in _helper_auth_return' ); + wp_die( 'Something went wrong' ); + } + + // Obtain an access token. + $request = WC_Helper_API::post( + 'oauth/access_token', + array( + 'body' => array( + 'request_token' => wp_unslash( $_GET['request_token'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + 'home_url' => home_url(), + ), + ) + ); + + $code = wp_remote_retrieve_response_code( $request ); + + if ( 200 !== $code ) { + self::log( sprintf( 'Call to oauth/access_token returned a non-200 response code (%d)', $code ) ); + wp_die( 'Something went wrong' ); + } + + $access_token = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( ! $access_token ) { + self::log( sprintf( 'Call to oauth/access_token returned an invalid body: %s', wp_remote_retrieve_body( $request ) ) ); + wp_die( 'Something went wrong' ); + } + + WC_Helper_Options::update( + 'auth', + array( + 'access_token' => $access_token['access_token'], + 'access_token_secret' => $access_token['access_token_secret'], + 'site_id' => $access_token['site_id'], + 'user_id' => get_current_user_id(), + 'updated' => time(), + ) + ); + + // Obtain the connected user info. + if ( ! self::_flush_authentication_cache() ) { + self::log( 'Could not obtain connected user info in _helper_auth_return' ); + WC_Helper_Options::update( 'auth', array() ); + wp_die( 'Something went wrong.' ); + } + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + /** + * Fires when the Helper connection process has completed successfully. + */ + do_action( 'woocommerce_helper_connected' ); + + // Enable tracking when connected. + if ( class_exists( 'WC_Tracker' ) ) { + update_option( 'woocommerce_allow_tracking', 'yes' ); + WC_Tracker::send_tracking_data( true ); + } + + // If connecting through in-app purchase, redirects back to WooCommerce.com + // for product installation. + if ( ! empty( $_GET['wccom-install-url'] ) ) { + wp_redirect( wp_unslash( $_GET['wccom-install-url'] ) ); + exit; + } + + wp_safe_redirect( + add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => 'helper-connected', + ), + admin_url( 'admin.php' ) + ) + ); + die(); + } + + /** + * Disconnect from WooCommerce.com, clear OAuth tokens. + */ + private static function _helper_auth_disconnect() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'disconnect' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_auth_disconnect' ); + wp_die( 'Could not verify nonce' ); + } + + /** + * Fires when the Helper has been disconnected. + */ + do_action( 'woocommerce_helper_disconnected' ); + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'wc-helper-status' => 'helper-disconnected', + ), + admin_url( 'admin.php' ) + ); + + WC_Helper_API::post( + 'oauth/invalidate_token', + array( + 'authenticated' => true, + ) + ); + + WC_Helper_Options::update( 'auth', array() ); + WC_Helper_Options::update( 'auth_user_data', array() ); + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * User hit the Refresh button, clear all caches. + */ + private static function _helper_auth_refresh() { + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'refresh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_auth_refresh' ); + wp_die( 'Could not verify nonce' ); + } + + /** + * Fires when Helper subscriptions are refreshed. + */ + do_action( 'woocommerce_helper_subscriptions_refresh' ); + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-status' => 'helper-refreshed', + ), + admin_url( 'admin.php' ) + ); + + self::_flush_authentication_cache(); + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Active a product subscription. + */ + private static function _helper_subscription_activate() { + $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'activate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_subscription_activate' ); + wp_die( 'Could not verify nonce' ); + } + + // Activate subscription. + $activation_response = WC_Helper_API::post( + 'activate', + array( + 'authenticated' => true, + 'body' => wp_json_encode( + array( + 'product_key' => $product_key, + ) + ), + ) + ); + + $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; + $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); + + if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { + $activated = true; + } + + if ( $activated ) { + /** + * Fires when the Helper activates a product successfully. + * + * @param int $product_id Product ID being activated. + * @param string $product_key Subscription product key. + * @param array $activation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); + } else { + /** + * Fires when the Helper fails to activate a product. + * + * @param int $product_id Product ID being activated. + * @param string $product_key Subscription product key. + * @param array $activation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); + } + + // Attempt to activate this plugin. + $local = self::_get_local_from_product_id( $product_id ); + if ( $local && 'plugin' == $local['_type'] && current_user_can( 'activate_plugins' ) && ! is_plugin_active( $local['_filename'] ) ) { + activate_plugin( $local['_filename'] ); + } + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-status' => $activated ? 'activate-success' : 'activate-error', + 'wc-helper-product-id' => $product_id, + ), + admin_url( 'admin.php' ) + ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Deactivate a product subscription. + */ + private static function _helper_subscription_deactivate() { + $product_key = isset( $_GET['wc-helper-product-key'] ) ? wc_clean( wp_unslash( $_GET['wc-helper-product-key'] ) ) : ''; + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate:' . $product_key ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_subscription_deactivate' ); + wp_die( 'Could not verify nonce' ); + } + + $deactivation_response = WC_Helper_API::post( + 'deactivate', + array( + 'authenticated' => true, + 'body' => wp_json_encode( + array( + 'product_key' => $product_key, + ) + ), + ) + ); + + $code = wp_remote_retrieve_response_code( $deactivation_response ); + $deactivated = 200 === $code; + + if ( $deactivated ) { + /** + * Fires when the Helper activates a product successfully. + * + * @param int $product_id Product ID being deactivated. + * @param string $product_key Subscription product key. + * @param array $deactivation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); + } else { + self::log( sprintf( 'Deactivate API call returned a non-200 response code (%d)', $code ) ); + + /** + * Fires when the Helper fails to activate a product. + * + * @param int $product_id Product ID being deactivated. + * @param string $product_key Subscription product key. + * @param array $deactivation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); + } + + self::_flush_subscriptions_cache(); + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-status' => $deactivated ? 'deactivate-success' : 'deactivate-error', + 'wc-helper-product-id' => $product_id, + ), + admin_url( 'admin.php' ) + ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Deactivate a plugin. + */ + private static function _helper_plugin_deactivate() { + $product_id = isset( $_GET['wc-helper-product-id'] ) ? absint( $_GET['wc-helper-product-id'] ) : 0; + $deactivated = false; + + if ( empty( $_GET['wc-helper-nonce'] ) || ! wp_verify_nonce( wp_unslash( $_GET['wc-helper-nonce'] ), 'deactivate-plugin:' . $product_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + self::log( 'Could not verify nonce in _helper_plugin_deactivate' ); + wp_die( 'Could not verify nonce' ); + } + + if ( ! current_user_can( 'activate_plugins' ) ) { + wp_die( 'You are not allowed to manage plugins on this site.' ); + } + + $local = wp_list_filter( + array_merge( + self::get_local_woo_plugins(), + self::get_local_woo_themes() + ), + array( '_product_id' => $product_id ) + ); + + // Attempt to deactivate this plugin or theme. + if ( ! empty( $local ) ) { + $local = array_shift( $local ); + if ( is_plugin_active( $local['_filename'] ) ) { + deactivate_plugins( $local['_filename'] ); + } + + $deactivated = ! is_plugin_active( $local['_filename'] ); + } + + $redirect_uri = add_query_arg( + array( + 'page' => 'wc-addons', + 'section' => 'helper', + 'filter' => self::get_current_filter(), + 'wc-helper-status' => $deactivated ? 'deactivate-plugin-success' : 'deactivate-plugin-error', + 'wc-helper-product-id' => $product_id, + ), + admin_url( 'admin.php' ) + ); + + wp_safe_redirect( $redirect_uri ); + die(); + } + + /** + * Get a local plugin/theme entry from product_id. + * + * @param int $product_id The product id. + * + * @return array|bool The array containing the local plugin/theme data or false. + */ + private static function _get_local_from_product_id( $product_id ) { + $local = wp_list_filter( + array_merge( + self::get_local_woo_plugins(), + self::get_local_woo_themes() + ), + array( '_product_id' => $product_id ) + ); + + if ( ! empty( $local ) ) { + return array_shift( $local ); + } + + return false; + } + + /** + * Checks whether current site has product subscription of a given ID. + * + * @since 3.7.0 + * + * @param int $product_id The product id. + * + * @return bool Returns true if product subscription exists, false otherwise. + */ + public static function has_product_subscription( $product_id ) { + $subscription = self::_get_subscriptions_from_product_id( $product_id, true ); + return ! empty( $subscription ); + } + + /** + * Get a subscription entry from product_id. If multiple subscriptions are + * found with the same product id and $single is set to true, will return the + * first one in the list, so you can use this method to get things like extension + * name, version, etc. + * + * @param int $product_id The product id. + * @param bool $single Whether to return a single subscription or all matching a product id. + * + * @return array|bool The array containing sub data or false. + */ + private static function _get_subscriptions_from_product_id( $product_id, $single = true ) { + $subscriptions = wp_list_filter( self::get_subscriptions(), array( 'product_id' => $product_id ) ); + if ( ! empty( $subscriptions ) ) { + return $single ? array_shift( $subscriptions ) : $subscriptions; + } + + return false; + } + + /** + * Obtain a list of data about locally installed Woo extensions. + */ + public static function get_local_woo_plugins() { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + + /** + * Check if plugins have WC headers, if not then clear cache and fetch again. + * WC Headers will not be present if `wc_enable_wc_plugin_headers` hook was added after a `get_plugins` call -- for example when WC is activated/updated. + * Also, get_plugins call is expensive so we should clear this cache very conservatively. + */ + if ( ! empty( $plugins ) && ! array_key_exists( 'Woo', current( $plugins ) ) ) { + wp_clean_plugins_cache( false ); + $plugins = get_plugins(); + } + + $woo_plugins = array(); + + // Backwards compatibility for woothemes_queue_update(). + $_compat = array(); + if ( ! empty( $GLOBALS['woothemes_queued_updates'] ) ) { + foreach ( $GLOBALS['woothemes_queued_updates'] as $_compat_plugin ) { + $_compat[ $_compat_plugin->file ] = array( + 'product_id' => $_compat_plugin->product_id, + 'file_id' => $_compat_plugin->file_id, + ); + } + } + + foreach ( $plugins as $filename => $data ) { + if ( empty( $data['Woo'] ) && ! empty( $_compat[ $filename ] ) ) { + $data['Woo'] = sprintf( '%d:%s', $_compat[ $filename ]['product_id'], $_compat[ $filename ]['file_id'] ); + } + + if ( empty( $data['Woo'] ) ) { + continue; + } + + list( $product_id, $file_id ) = explode( ':', $data['Woo'] ); + if ( empty( $product_id ) || empty( $file_id ) ) { + continue; + } + + $data['_filename'] = $filename; + $data['_product_id'] = absint( $product_id ); + $data['_file_id'] = $file_id; + $data['_type'] = 'plugin'; + $data['slug'] = dirname( $filename ); + $woo_plugins[ $filename ] = $data; + } + + return $woo_plugins; + } + + /** + * Get locally installed Woo themes. + */ + public static function get_local_woo_themes() { + $themes = wp_get_themes(); + $woo_themes = array(); + + foreach ( $themes as $theme ) { + $header = $theme->get( 'Woo' ); + + // Backwards compatibility for theme_info.txt. + if ( ! $header ) { + $txt = $theme->get_stylesheet_directory() . '/theme_info.txt'; + if ( is_readable( $txt ) ) { + $txt = file_get_contents( $txt ); + $txt = preg_split( '#\s#', $txt ); + if ( count( $txt ) >= 2 ) { + $header = sprintf( '%d:%s', $txt[0], $txt[1] ); + } + } + } + + if ( empty( $header ) ) { + continue; + } + + list( $product_id, $file_id ) = explode( ':', $header ); + if ( empty( $product_id ) || empty( $file_id ) ) { + continue; + } + + $data = array( + 'Name' => $theme->get( 'Name' ), + 'Version' => $theme->get( 'Version' ), + 'Woo' => $header, + + '_filename' => $theme->get_stylesheet() . '/style.css', + '_stylesheet' => $theme->get_stylesheet(), + '_product_id' => absint( $product_id ), + '_file_id' => $file_id, + '_type' => 'theme', + ); + + $woo_themes[ $data['_filename'] ] = $data; + } + + return $woo_themes; + } + + /** + * Get the connected user's subscriptions. + * + * @return array + */ + public static function get_subscriptions() { + $cache_key = '_woocommerce_helper_subscriptions'; + $data = get_transient( $cache_key ); + if ( false !== $data ) { + return $data; + } + + // Obtain the connected user info. + $request = WC_Helper_API::get( + 'subscriptions', + array( + 'authenticated' => true, + ) + ); + + if ( wp_remote_retrieve_response_code( $request ) !== 200 ) { + set_transient( $cache_key, array(), 15 * MINUTE_IN_SECONDS ); + return array(); + } + + $data = json_decode( wp_remote_retrieve_body( $request ), true ); + if ( empty( $data ) || ! is_array( $data ) ) { + $data = array(); + } + + set_transient( $cache_key, $data, 1 * HOUR_IN_SECONDS ); + return $data; + } + + /** + * Runs when any plugin is activated. + * + * Depending on the activated plugin attempts to look through available + * subscriptions and auto-activate one if possible, so the user does not + * need to visit the Helper UI at all after installing a new extension. + * + * @param string $filename The filename of the activated plugin. + */ + public static function activated_plugin( $filename ) { + $plugins = self::get_local_woo_plugins(); + + // Not a local woo plugin. + if ( empty( $plugins[ $filename ] ) ) { + return; + } + + // Make sure we have a connection. + $auth = WC_Helper_Options::get( 'auth' ); + if ( empty( $auth ) ) { + return; + } + + $plugin = $plugins[ $filename ]; + $product_id = $plugin['_product_id']; + $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); + + // No valid subscriptions for this product. + if ( empty( $subscriptions ) ) { + return; + } + + $subscription = null; + foreach ( $subscriptions as $_sub ) { + + // Don't attempt to activate expired subscriptions. + if ( $_sub['expired'] ) { + continue; + } + + // No more sites available in this subscription. + if ( $_sub['sites_max'] && $_sub['sites_active'] >= $_sub['sites_max'] ) { + continue; + } + + // Looks good. + $subscription = $_sub; + break; + } + + // No valid subscription found. + if ( ! $subscription ) { + return; + } + + $product_key = $subscription['product_key']; + $activation_response = WC_Helper_API::post( + 'activate', + array( + 'authenticated' => true, + 'body' => wp_json_encode( + array( + 'product_key' => $product_key, + ) + ), + ) + ); + + $activated = wp_remote_retrieve_response_code( $activation_response ) === 200; + $body = json_decode( wp_remote_retrieve_body( $activation_response ), true ); + + if ( ! $activated && ! empty( $body['code'] ) && 'already_connected' === $body['code'] ) { + $activated = true; + } + + if ( $activated ) { + self::log( 'Auto-activated a subscription for ' . $filename ); + /** + * Fires when the Helper activates a product successfully. + * + * @param int $product_id Product ID being activated. + * @param string $product_key Subscription product key. + * @param array $activation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_activate_success', $product_id, $product_key, $activation_response ); + } else { + self::log( 'Could not activate a subscription upon plugin activation: ' . $filename ); + + /** + * Fires when the Helper fails to activate a product. + * + * @param int $product_id Product ID being activated. + * @param string $product_key Subscription product key. + * @param array $activation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_activate_error', $product_id, $product_key, $activation_response ); + } + + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + } + + /** + * Runs when any plugin is deactivated. + * + * When a user deactivates a plugin, attempt to deactivate any subscriptions + * associated with the extension. + * + * @param string $filename The filename of the deactivated plugin. + */ + public static function deactivated_plugin( $filename ) { + $plugins = self::get_local_woo_plugins(); + + // Not a local woo plugin. + if ( empty( $plugins[ $filename ] ) ) { + return; + } + + // Make sure we have a connection. + $auth = WC_Helper_Options::get( 'auth' ); + if ( empty( $auth ) ) { + return; + } + + $plugin = $plugins[ $filename ]; + $product_id = $plugin['_product_id']; + $subscriptions = self::_get_subscriptions_from_product_id( $product_id, false ); + $site_id = absint( $auth['site_id'] ); + + // No valid subscriptions for this product. + if ( empty( $subscriptions ) ) { + return; + } + + $deactivated = 0; + + foreach ( $subscriptions as $subscription ) { + // Don't touch subscriptions that aren't activated on this site. + if ( ! in_array( $site_id, $subscription['connections'], true ) ) { + continue; + } + + $product_key = $subscription['product_key']; + $deactivation_response = WC_Helper_API::post( + 'deactivate', + array( + 'authenticated' => true, + 'body' => wp_json_encode( + array( + 'product_key' => $product_key, + ) + ), + ) + ); + + if ( wp_remote_retrieve_response_code( $deactivation_response ) === 200 ) { + $deactivated++; + + /** + * Fires when the Helper activates a product successfully. + * + * @param int $product_id Product ID being deactivated. + * @param string $product_key Subscription product key. + * @param array $deactivation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_deactivate_success', $product_id, $product_key, $deactivation_response ); + } else { + /** + * Fires when the Helper fails to activate a product. + * + * @param int $product_id Product ID being deactivated. + * @param string $product_key Subscription product key. + * @param array $deactivation_response The response object from wp_safe_remote_request(). + */ + do_action( 'woocommerce_helper_subscription_deactivate_error', $product_id, $product_key, $deactivation_response ); + } + } + + if ( $deactivated ) { + self::log( sprintf( 'Auto-deactivated %d subscription(s) for %s', $deactivated, $filename ) ); + self::_flush_subscriptions_cache(); + self::_flush_updates_cache(); + } + } + + /** + * Various Helper-related admin notices. + */ + public static function admin_notices() { + if ( apply_filters( 'woocommerce_helper_suppress_admin_notices', false ) ) { + return; + } + + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + if ( 'update-core' !== $screen_id ) { + return; + } + + // Don't nag if Woo doesn't have an update available. + if ( ! self::_woo_core_update_available() ) { + return; + } + + // Add a note about available extension updates if Woo core has an update available. + $notice = self::_get_extensions_update_notice(); + if ( ! empty( $notice ) ) { + echo '

    ' . $notice . '

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

    + + + +
    +

    + +

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

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

    + +

    +
    + + + +
    + + +

    +

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

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

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

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

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

    + + +

    +

    +

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    +

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

    +

    + +

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

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

    '; + $this->footer(); + die(); + } + + /** + * Added to http_request_timeout filter to force timeout at 60 seconds during import. + * + * @param int $val Value. + * @return int 60 + */ + public function bump_request_timeout( $val ) { + return 60; + } +} diff --git a/includes/admin/importers/mappings/default.php b/includes/admin/importers/mappings/default.php new file mode 100644 index 0000000..0d9599d --- /dev/null +++ b/includes/admin/importers/mappings/default.php @@ -0,0 +1,113 @@ + 'id', + 'Type' => 'type', + 'SKU' => 'sku', + 'Name' => 'name', + 'Published' => 'published', + 'Is featured?' => 'featured', + 'Visibility in catalog' => 'catalog_visibility', + 'Short description' => 'short_description', + 'Description' => 'description', + 'Date sale price starts' => 'date_on_sale_from', + 'Date sale price ends' => 'date_on_sale_to', + 'Tax status' => 'tax_status', + 'Tax class' => 'tax_class', + 'In stock?' => 'stock_status', + 'Stock' => 'stock_quantity', + 'Backorders allowed?' => 'backorders', + 'Low stock amount' => 'low_stock_amount', + 'Sold individually?' => 'sold_individually', + sprintf( 'Weight (%s)', $weight_unit ) => 'weight', + sprintf( 'Length (%s)', $dimension_unit ) => 'length', + sprintf( 'Width (%s)', $dimension_unit ) => 'width', + sprintf( 'Height (%s)', $dimension_unit ) => 'height', + 'Allow customer reviews?' => 'reviews_allowed', + 'Purchase note' => 'purchase_note', + 'Sale price' => 'sale_price', + 'Regular price' => 'regular_price', + 'Categories' => 'category_ids', + 'Tags' => 'tag_ids', + 'Shipping class' => 'shipping_class_id', + 'Images' => 'images', + 'Download limit' => 'download_limit', + 'Download expiry days' => 'download_expiry', + 'Parent' => 'parent_id', + 'Upsells' => 'upsell_ids', + 'Cross-sells' => 'cross_sell_ids', + 'Grouped products' => 'grouped_products', + 'External URL' => 'product_url', + 'Button text' => 'button_text', + 'Position' => 'menu_order', + ); + + return array_merge( $mappings, $new_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_default_english_mappings', 100 ); + +/** + * Add English special mapping placeholders when not using English as current language. + * + * @since 3.1.0 + * @param array $mappings Importer columns mappings. + * @return array + */ +function wc_importer_default_special_english_mappings( $mappings ) { + if ( 'en_US' === wc_importer_current_locale() ) { + return $mappings; + } + + $new_mappings = array( + 'Attribute %d name' => 'attributes:name', + 'Attribute %d value(s)' => 'attributes:value', + 'Attribute %d visible' => 'attributes:visible', + 'Attribute %d global' => 'attributes:taxonomy', + 'Attribute %d default' => 'attributes:default', + 'Download %d ID' => 'downloads:id', + 'Download %d name' => 'downloads:name', + 'Download %d URL' => 'downloads:url', + 'Meta: %s' => 'meta:', + ); + + return array_merge( $mappings, $new_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_default_special_english_mappings', 100 ); diff --git a/includes/admin/importers/mappings/generic.php b/includes/admin/importers/mappings/generic.php new file mode 100644 index 0000000..d5b14ab --- /dev/null +++ b/includes/admin/importers/mappings/generic.php @@ -0,0 +1,31 @@ + 'name', + __( 'Product Title', 'woocommerce' ) => 'name', + __( 'Price', 'woocommerce' ) => 'regular_price', + __( 'Parent SKU', 'woocommerce' ) => 'parent_id', + __( 'Quantity', 'woocommerce' ) => 'stock_quantity', + __( 'Menu order', 'woocommerce' ) => 'menu_order', + ); + + 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 0000000..15587ae --- /dev/null +++ b/includes/admin/importers/mappings/mappings.php @@ -0,0 +1,15 @@ + 'sku', + 'Title' => 'name', + 'Body (HTML)' => 'description', + 'Quantity' => 'stock_quantity', + 'Variant Inventory Qty' => 'stock_quantity', + 'Image Src' => 'images', + 'Variant Image' => 'images', + 'Variant SKU' => 'sku', + 'Variant Price' => 'sale_price', + 'Variant Compare At Price' => 'regular_price', + 'Type' => 'category_ids', + 'Tags' => 'tag_ids_spaces', + 'Variant Grams' => 'weight', + 'Variant Requires Shipping' => 'meta:shopify_requires_shipping', + 'Variant Taxable' => 'tax_status', + ); + return array_merge( $mappings, $shopify_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_default_columns', 'wc_importer_shopify_mappings', 10, 2 ); + +/** + * Add special wildcard Shopify mappings. + * + * @since 3.7.0 + * @param array $mappings Importer columns mappings. + * @param array $raw_headers Raw headers from CSV being imported. + * @return array + */ +function wc_importer_shopify_special_mappings( $mappings, $raw_headers ) { + // Only map if this is looks like a Shopify export. + if ( 0 !== count( array_diff( array( 'Title', 'Body (HTML)', 'Type', 'Variant SKU' ), $raw_headers ) ) ) { + return $mappings; + } + $shopify_mappings = array( + 'Option%d Name' => 'attributes:name', + 'Option%d Value' => 'attributes:value', + ); + return array_merge( $mappings, $shopify_mappings ); +} +add_filter( 'woocommerce_csv_product_import_mapping_special_columns', 'wc_importer_shopify_special_mappings', 10, 2 ); + +/** + * Expand special Shopify columns to WC format. + * + * @since 3.7.0 + * @param array $data Array of data. + * @return array Expanded data. + */ +function wc_importer_shopify_expand_data( $data ) { + if ( isset( $data['meta:shopify_requires_shipping'] ) ) { + $requires_shipping = wc_string_to_bool( $data['meta:shopify_requires_shipping'] ); + + if ( ! $requires_shipping ) { + if ( isset( $data['type'] ) ) { + $data['type'][] = 'virtual'; + } else { + $data['type'] = array( 'virtual' ); + } + } + + unset( $data['meta:shopify_requires_shipping'] ); + } + return $data; +} +add_filter( 'woocommerce_product_importer_pre_expand_data', 'wc_importer_shopify_expand_data' ); diff --git a/includes/admin/importers/mappings/wordpress.php b/includes/admin/importers/mappings/wordpress.php new file mode 100644 index 0000000..755c507 --- /dev/null +++ b/includes/admin/importers/mappings/wordpress.php @@ -0,0 +1,31 @@ + '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 0000000..1774001 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-done.php @@ -0,0 +1,104 @@ + +
    +
    + ' . number_format_i18n( $imported ) . '' + ); + } + + if ( 0 < $updated ) { + $results[] = sprintf( + /* translators: %d: products count */ + _n( '%s product updated', '%s products updated', $updated, 'woocommerce' ), + '' . number_format_i18n( $updated ) . '' + ); + } + + if ( 0 < $skipped ) { + $results[] = sprintf( + /* translators: %d: products count */ + _n( '%s product was skipped', '%s products were skipped', $skipped, 'woocommerce' ), + '' . number_format_i18n( $skipped ) . '' + ); + } + + if ( 0 < $failed ) { + $results [] = sprintf( + /* translators: %d: products count */ + _n( 'Failed to import %s product', 'Failed to import %s products', $failed, 'woocommerce' ), + '' . number_format_i18n( $failed ) . '' + ); + } + + if ( 0 < $failed || 0 < $skipped ) { + $results[] = '' . __( 'View import log', 'woocommerce' ) . ''; + } + + if ( ! empty( $file_name ) ) { + $results[] = sprintf( + /* translators: %s: File name */ + __( 'File uploaded: %s', 'woocommerce' ), + '' . $file_name . '' + ); + } + + /* translators: %d: import results */ + echo wp_kses_post( __( 'Import complete!', 'woocommerce' ) . ' ' . implode( '. ', $results ) ); + ?> +
    + + +
    + +
    +
    diff --git a/includes/admin/importers/views/html-csv-import-footer.php b/includes/admin/importers/views/html-csv-import-footer.php new file mode 100644 index 0000000..cf35e17 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-footer.php @@ -0,0 +1,13 @@ + +
    +
    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 0000000..78c42e1 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-header.php @@ -0,0 +1,15 @@ + +
    +

    + +
    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 0000000..20cdc42 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-mapping.php @@ -0,0 +1,65 @@ + +
    +
    +

    +

    +
    +
    + + + + + + + + + $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 0000000..ea1ed33 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-progress.php @@ -0,0 +1,21 @@ + +
    +
    + +

    +

    +
    +
    + +
    +
    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 0000000..2f8c8f8 --- /dev/null +++ b/includes/admin/importers/views/html-csv-import-steps.php @@ -0,0 +1,26 @@ + +
      + steps as $step_key => $step ) : ?> + step ) { + $step_class = 'active'; + } elseif ( array_search( $this->step, array_keys( $this->steps ), true ) > array_search( $step_key, array_keys( $this->steps ), true ) ) { + $step_class = 'done'; + } + ?> +
    1. + +
    2. + +
    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 0000000..3b96ec6 --- /dev/null +++ b/includes/admin/importers/views/html-product-csv-import-form.php @@ -0,0 +1,104 @@ + +
    +
    +

    +

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

    +

    +
    + + + + +
    + + + + +

    + + + +
    +
    + +
    + + + +
    +
    diff --git a/includes/admin/list-tables/abstract-class-wc-admin-list-table.php b/includes/admin/list-tables/abstract-class-wc-admin-list-table.php new file mode 100644 index 0000000..4a0c93d --- /dev/null +++ b/includes/admin/list-tables/abstract-class-wc-admin-list-table.php @@ -0,0 +1,276 @@ +list_table_type ) { + add_action( 'manage_posts_extra_tablenav', array( $this, 'maybe_render_blank_state' ) ); + add_filter( 'view_mode_post_types', array( $this, 'disable_view_mode' ) ); + add_action( 'restrict_manage_posts', array( $this, 'restrict_manage_posts' ) ); + add_filter( 'request', array( $this, 'request_query' ) ); + add_filter( 'post_row_actions', array( $this, 'row_actions' ), 100, 2 ); + add_filter( 'default_hidden_columns', array( $this, 'default_hidden_columns' ), 10, 2 ); + add_filter( 'list_table_primary_column', array( $this, 'list_table_primary_column' ), 10, 2 ); + add_filter( 'manage_edit-' . $this->list_table_type . '_sortable_columns', array( $this, 'define_sortable_columns' ) ); + add_filter( 'manage_' . $this->list_table_type . '_posts_columns', array( $this, 'define_columns' ) ); + add_filter( 'bulk_actions-edit-' . $this->list_table_type, array( $this, 'define_bulk_actions' ) ); + add_action( 'manage_' . $this->list_table_type . '_posts_custom_column', array( $this, 'render_columns' ), 10, 2 ); + add_filter( 'handle_bulk_actions-edit-' . $this->list_table_type, array( $this, 'handle_bulk_actions' ), 10, 3 ); + } + } + + /** + * Show blank slate. + * + * @param string $which String which tablenav is being shown. + */ + public function maybe_render_blank_state( $which ) { + global $post_type; + + if ( $post_type === $this->list_table_type && 'bottom' === $which ) { + $counts = (array) wp_count_posts( $post_type ); + unset( $counts['auto-draft'] ); + $count = array_sum( $counts ); + + if ( 0 < $count ) { + return; + } + + $this->render_blank_state(); + + echo ''; + } + } + + /** + * Render blank state. Extend to add content. + */ + protected function render_blank_state() {} + + /** + * Removes this type from list of post types that support "View Mode" switching. + * View mode is seen on posts where you can switch between list or excerpt. Our post types don't support + * it, so we want to hide the useless UI from the screen options tab. + * + * @param array $post_types Array of post types supporting view mode. + * @return array Array of post types supporting view mode, without this type. + */ + public function disable_view_mode( $post_types ) { + unset( $post_types[ $this->list_table_type ] ); + return $post_types; + } + + /** + * See if we should render search filters or not. + */ + public function restrict_manage_posts() { + global $typenow; + + if ( $this->list_table_type === $typenow ) { + $this->render_filters(); + } + } + + /** + * Handle any filters. + * + * @param array $query_vars Query vars. + * @return array + */ + public function request_query( $query_vars ) { + global $typenow; + + if ( $this->list_table_type === $typenow ) { + return $this->query_filters( $query_vars ); + } + + return $query_vars; + } + + /** + * Render any custom filters and search inputs for the list table. + */ + protected function render_filters() {} + + /** + * Handle any custom filters. + * + * @param array $query_vars Query vars. + * @return array + */ + protected function query_filters( $query_vars ) { + return $query_vars; + } + + /** + * Set row actions. + * + * @param array $actions Array of actions. + * @param WP_Post $post Current post object. + * @return array + */ + public function row_actions( $actions, $post ) { + if ( $this->list_table_type === $post->post_type ) { + return $this->get_row_actions( $actions, $post ); + } + return $actions; + } + + /** + * Get row actions to show in the list table. + * + * @param array $actions Array of actions. + * @param WP_Post $post Current post object. + * @return array + */ + protected function get_row_actions( $actions, $post ) { + return $actions; + } + + /** + * Adjust which columns are displayed by default. + * + * @param array $hidden Current hidden columns. + * @param object $screen Current screen. + * @return array + */ + public function default_hidden_columns( $hidden, $screen ) { + if ( isset( $screen->id ) && 'edit-' . $this->list_table_type === $screen->id ) { + $hidden = array_merge( $hidden, $this->define_hidden_columns() ); + } + return $hidden; + } + + /** + * Set list table primary column. + * + * @param string $default Default value. + * @param string $screen_id Current screen ID. + * @return string + */ + public function list_table_primary_column( $default, $screen_id ) { + if ( 'edit-' . $this->list_table_type === $screen_id && $this->get_primary_column() ) { + return $this->get_primary_column(); + } + return $default; + } + + /** + * Define primary column. + * + * @return array + */ + protected function get_primary_column() { + return ''; + } + + /** + * Define hidden columns. + * + * @return array + */ + protected function define_hidden_columns() { + return array(); + } + + /** + * Define which columns are sortable. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_sortable_columns( $columns ) { + return $columns; + } + + /** + * Define which columns to show on this screen. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_columns( $columns ) { + return $columns; + } + + /** + * Define bulk actions. + * + * @param array $actions Existing actions. + * @return array + */ + public function define_bulk_actions( $actions ) { + return $actions; + } + + /** + * Pre-fetch any data for the row each column has access to it. + * + * @param int $post_id Post ID being shown. + */ + protected function prepare_row_data( $post_id ) {} + + /** + * Render individual columns. + * + * @param string $column Column ID to render. + * @param int $post_id Post ID being shown. + */ + public function render_columns( $column, $post_id ) { + $this->prepare_row_data( $post_id ); + + if ( ! $this->object ) { + return; + } + + if ( is_callable( array( $this, 'render_' . $column . '_column' ) ) ) { + $this->{"render_{$column}_column"}(); + } + } + + /** + * Handle bulk actions. + * + * @param string $redirect_to URL to redirect to. + * @param string $action Action name. + * @param array $ids List of ids. + * @return string + */ + public function handle_bulk_actions( $redirect_to, $action, $ids ) { + return esc_url_raw( $redirect_to ); + } +} diff --git a/includes/admin/list-tables/class-wc-admin-list-table-coupons.php b/includes/admin/list-tables/class-wc-admin-list-table-coupons.php new file mode 100644 index 0000000..91f499a --- /dev/null +++ b/includes/admin/list-tables/class-wc-admin-list-table-coupons.php @@ -0,0 +1,232 @@ +'; + echo '

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

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

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

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

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

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

    ' . esc_html( $message ) . '

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

    ' . esc_html( $message ) . '

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

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

    '; + + echo ''; + + do_action( 'wc_marketplace_suggestions_products_empty_state' ); + + echo ''; + } + + /** + * Define primary column. + * + * @return string + */ + protected function get_primary_column() { + return 'name'; + } + + /** + * Get row actions to show in the list table. + * + * @param array $actions Array of actions. + * @param WP_Post $post Current post object. + * @return array + */ + protected function get_row_actions( $actions, $post ) { + /* translators: %d: product ID. */ + return array_merge( array( 'id' => sprintf( __( 'ID: %d', 'woocommerce' ), $post->ID ) ), $actions ); + } + + /** + * Define which columns are sortable. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_sortable_columns( $columns ) { + $custom = array( + 'price' => 'price', + 'sku' => 'sku', + 'name' => 'title', + ); + return wp_parse_args( $custom, $columns ); + } + + /** + * Define which columns to show on this screen. + * + * @param array $columns Existing columns. + * @return array + */ + public function define_columns( $columns ) { + if ( empty( $columns ) && ! is_array( $columns ) ) { + $columns = array(); + } + + unset( $columns['title'], $columns['comments'], $columns['date'] ); + + $show_columns = array(); + $show_columns['cb'] = ''; + $show_columns['thumb'] = '' . __( 'Image', 'woocommerce' ) . ''; + $show_columns['name'] = __( 'Name', 'woocommerce' ); + + if ( wc_product_sku_enabled() ) { + $show_columns['sku'] = __( 'SKU', 'woocommerce' ); + } + + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + $show_columns['is_in_stock'] = __( 'Stock', 'woocommerce' ); + } + + $show_columns['price'] = __( 'Price', 'woocommerce' ); + $show_columns['product_cat'] = __( 'Categories', 'woocommerce' ); + $show_columns['product_tag'] = __( 'Tags', 'woocommerce' ); + $show_columns['featured'] = '' . __( 'Featured', 'woocommerce' ) . ''; + $show_columns['date'] = __( 'Date', 'woocommerce' ); + + return array_merge( $show_columns, $columns ); + } + + /** + * Pre-fetch any data for the row each column has access to it. the_product global is there for bw compat. + * + * @param int $post_id Post ID being shown. + */ + protected function prepare_row_data( $post_id ) { + global $the_product; + + if ( empty( $this->object ) || $this->object->get_id() !== $post_id ) { + $the_product = wc_get_product( $post_id ); + $this->object = $the_product; + } + } + + /** + * Render column: thumb. + */ + protected function render_thumb_column() { + echo '' . $this->object->get_image( 'thumbnail' ) . ''; // WPCS: XSS ok. + } + + /** + * Render column: name. + */ + protected function render_name_column() { + global $post; + + $edit_link = get_edit_post_link( $this->object->get_id() ); + $title = _draft_or_post_title(); + + echo '' . esc_html( $title ) . ''; + + _post_states( $post ); + + echo ''; + + if ( $this->object->get_parent_id() > 0 ) { + echo '  ← ' . get_the_title( $this->object->get_parent_id() ) . ''; // @codingStandardsIgnoreLine. + } + + get_inline_data( $post ); + + /* Custom inline data for woocommerce. */ + echo ' + + '; + } + + /** + * Render column: sku. + */ + protected function render_sku_column() { + echo $this->object->get_sku() ? esc_html( $this->object->get_sku() ) : ''; + } + + /** + * Render column: price. + */ + protected function render_price_column() { + echo $this->object->get_price_html() ? wp_kses_post( $this->object->get_price_html() ) : ''; + } + + /** + * Render column: product_cat. + */ + protected function render_product_cat_column() { + $terms = get_the_terms( $this->object->get_id(), 'product_cat' ); + if ( ! $terms ) { + echo ''; + } else { + $termlist = array(); + foreach ( $terms as $term ) { + $termlist[] = '' . esc_html( $term->name ) . ''; + } + + echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_cat', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok. + } + } + + /** + * Render column: product_tag. + */ + protected function render_product_tag_column() { + $terms = get_the_terms( $this->object->get_id(), 'product_tag' ); + if ( ! $terms ) { + echo ''; + } else { + $termlist = array(); + foreach ( $terms as $term ) { + $termlist[] = '' . esc_html( $term->name ) . ''; + } + + echo apply_filters( 'woocommerce_admin_product_term_list', implode( ', ', $termlist ), 'product_tag', $this->object->get_id(), $termlist, $terms ); // WPCS: XSS ok. + } + } + + /** + * Render column: featured. + */ + protected function render_featured_column() { + $url = wp_nonce_url( admin_url( 'admin-ajax.php?action=woocommerce_feature_product&product_id=' . $this->object->get_id() ), 'woocommerce-feature-product' ); + echo ''; + if ( $this->object->is_featured() ) { + echo '' . esc_html__( 'Yes', 'woocommerce' ) . ''; + } else { + echo '' . esc_html__( 'No', 'woocommerce' ) . ''; + } + echo ''; + } + + /** + * Render column: is_in_stock. + */ + protected function render_is_in_stock_column() { + if ( $this->object->is_on_backorder() ) { + $stock_html = '' . __( 'On backorder', 'woocommerce' ) . ''; + } elseif ( $this->object->is_in_stock() ) { + $stock_html = '' . __( 'In stock', 'woocommerce' ) . ''; + } else { + $stock_html = '' . __( 'Out of stock', 'woocommerce' ) . ''; + } + + if ( $this->object->managing_stock() ) { + $stock_html .= ' (' . wc_stock_amount( $this->object->get_stock_quantity() ) . ')'; + } + + echo wp_kses_post( apply_filters( 'woocommerce_admin_stock_html', $stock_html, $this->object ) ); + } + + /** + * Query vars for custom searches. + * + * @param mixed $public_query_vars Array of query vars. + * @return array + */ + public function add_custom_query_var( $public_query_vars ) { + $public_query_vars[] = 'sku'; + return $public_query_vars; + } + + /** + * Render any custom filters and search inputs for the list table. + */ + protected function render_filters() { + $filters = apply_filters( + 'woocommerce_products_admin_list_table_filters', + array( + 'product_category' => array( $this, 'render_products_category_filter' ), + 'product_type' => array( $this, 'render_products_type_filter' ), + 'stock_status' => array( $this, 'render_products_stock_status_filter' ), + ) + ); + + ob_start(); + foreach ( $filters as $filter_callback ) { + call_user_func( $filter_callback ); + } + $output = ob_get_clean(); + + echo apply_filters( 'woocommerce_product_filters', $output ); // WPCS: XSS ok. + } + + /** + * Render the product category filter for the list table. + * + * @since 3.5.0 + */ + protected function render_products_category_filter() { + $categories_count = (int) wp_count_terms( 'product_cat' ); + + if ( $categories_count <= apply_filters( 'woocommerce_product_category_filter_threshold', 100 ) ) { + wc_product_dropdown_categories( + array( + 'option_select_text' => __( 'Filter by category', 'woocommerce' ), + 'hide_empty' => 0, + ) + ); + } else { + $current_category_slug = isset( $_GET['product_cat'] ) ? wc_clean( wp_unslash( $_GET['product_cat'] ) ) : false; // WPCS: input var ok, CSRF ok. + $current_category = $current_category_slug ? get_term_by( 'slug', $current_category_slug, 'product_cat' ) : false; + ?> + + '; + + foreach ( wc_get_product_types() as $value => $label ) { + $output .= ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + } + } + + /** + * Get country address formats. + * + * These define how addresses are formatted for display in various countries. + * + * @return array + */ + public function get_address_formats() { + if ( empty( $this->address_formats ) ) { + $this->address_formats = apply_filters( + 'woocommerce_localisation_address_formats', + array( + 'default' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode}\n{country}", + 'AT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'AU' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {state} {postcode}\n{country}", + 'BE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'CA' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {state_code} {postcode}\n{country}", + 'CH' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'CL' => "{company}\n{name}\n{address_1}\n{address_2}\n{state}\n{postcode} {city}\n{country}", + 'CN' => "{country} {postcode}\n{state}, {city}, {address_2}, {address_1}\n{company}\n{name}", + 'CZ' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'DE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'DK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'EE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'ES' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{state}\n{country}", + 'FI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'FR' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city_upper}\n{country}", + 'HK' => "{company}\n{first_name} {last_name_upper}\n{address_1}\n{address_2}\n{city_upper}\n{state_upper}\n{country}", + 'HU' => "{last_name} {first_name}\n{company}\n{city}\n{address_1}\n{address_2}\n{postcode}\n{country}", + 'IN' => "{company}\n{name}\n{address_1}\n{address_2}\n{city} {postcode}\n{state}, {country}", + 'IS' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'IT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode}\n{city}\n{state_upper}\n{country}", + 'JM' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}\n{postcode_upper}\n{country}", + 'JP' => "{postcode}\n{state} {city} {address_1}\n{address_2}\n{company}\n{last_name} {first_name}\n{country}", + 'LI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'NL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'NO' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'NZ' => "{name}\n{company}\n{address_1}\n{address_2}\n{city} {postcode}\n{country}", + 'PL' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'PR' => "{company}\n{name}\n{address_1} {address_2}\n{city} \n{country} {postcode}", + 'PT' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'RS' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SE' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SI' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'SK' => "{company}\n{name}\n{address_1}\n{address_2}\n{postcode} {city}\n{country}", + 'TR' => "{name}\n{company}\n{address_1}\n{address_2}\n{postcode} {city} {state}\n{country}", + 'TW' => "{company}\n{last_name} {first_name}\n{address_1}\n{address_2}\n{state}, {city} {postcode}\n{country}", + 'UG' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}\n{state}, {country}", + 'US' => "{name}\n{company}\n{address_1}\n{address_2}\n{city}, {state_code} {postcode}\n{country}", + 'VN' => "{name}\n{company}\n{address_1}\n{city}\n{country}", + ) + ); + } + return $this->address_formats; + } + + /** + * Get country address format. + * + * @param array $args Arguments. + * @param string $separator How to separate address lines. @since 3.5.0. + * @return string + */ + public function get_formatted_address( $args = array(), $separator = '
    ' ) { + $default_args = array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + ); + + $args = array_map( 'trim', wp_parse_args( $args, $default_args ) ); + $state = $args['state']; + $country = $args['country']; + + // Get all formats. + $formats = $this->get_address_formats(); + + // Get format for the address' country. + $format = ( $country && isset( $formats[ $country ] ) ) ? $formats[ $country ] : $formats['default']; + + // Handle full country name. + $full_country = ( isset( $this->countries[ $country ] ) ) ? $this->countries[ $country ] : $country; + + // Country is not needed if the same as base. + if ( $country === $this->get_base_country() && ! apply_filters( 'woocommerce_formatted_address_force_country_display', false ) ) { + $format = str_replace( '{country}', '', $format ); + } + + // Handle full state name. + $full_state = ( $country && $state && isset( $this->states[ $country ][ $state ] ) ) ? $this->states[ $country ][ $state ] : $state; + + // Substitute address parts into the string. + $replace = array_map( + 'esc_html', + apply_filters( + 'woocommerce_formatted_address_replacements', + array( + '{first_name}' => $args['first_name'], + '{last_name}' => $args['last_name'], + '{name}' => sprintf( + /* translators: 1: first name 2: last name */ + _x( '%1$s %2$s', 'full name', 'woocommerce' ), + $args['first_name'], + $args['last_name'] + ), + '{company}' => $args['company'], + '{address_1}' => $args['address_1'], + '{address_2}' => $args['address_2'], + '{city}' => $args['city'], + '{state}' => $full_state, + '{postcode}' => $args['postcode'], + '{country}' => $full_country, + '{first_name_upper}' => wc_strtoupper( $args['first_name'] ), + '{last_name_upper}' => wc_strtoupper( $args['last_name'] ), + '{name_upper}' => wc_strtoupper( + sprintf( + /* translators: 1: first name 2: last name */ + _x( '%1$s %2$s', 'full name', 'woocommerce' ), + $args['first_name'], + $args['last_name'] + ) + ), + '{company_upper}' => wc_strtoupper( $args['company'] ), + '{address_1_upper}' => wc_strtoupper( $args['address_1'] ), + '{address_2_upper}' => wc_strtoupper( $args['address_2'] ), + '{city_upper}' => wc_strtoupper( $args['city'] ), + '{state_upper}' => wc_strtoupper( $full_state ), + '{state_code}' => wc_strtoupper( $state ), + '{postcode_upper}' => wc_strtoupper( $args['postcode'] ), + '{country_upper}' => wc_strtoupper( $full_country ), + ), + $args + ) + ); + + $formatted_address = str_replace( array_keys( $replace ), $replace, $format ); + + // Clean up white space. + $formatted_address = preg_replace( '/ +/', ' ', trim( $formatted_address ) ); + $formatted_address = preg_replace( '/\n\n+/', "\n", $formatted_address ); + + // Break newlines apart and remove empty lines/trim commas and white space. + $formatted_address = array_filter( array_map( array( $this, 'trim_formatted_address_line' ), explode( "\n", $formatted_address ) ) ); + + // Add html breaks. + $formatted_address = implode( $separator, $formatted_address ); + + // We're done! + return $formatted_address; + } + + /** + * Trim white space and commas off a line. + * + * @param string $line Line. + * @return string + */ + private function trim_formatted_address_line( $line ) { + return trim( $line, ', ' ); + } + + /** + * Returns the fields we show by default. This can be filtered later on. + * + * @return array + */ + public function get_default_address_fields() { + $address_2_label = __( 'Apartment, suite, unit, etc.', 'woocommerce' ); + + // If necessary, append '(optional)' to the placeholder: we don't need to worry about the + // label, though, as woocommerce_form_field() takes care of that. + if ( 'optional' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { + $address_2_placeholder = __( 'Apartment, suite, unit, etc. (optional)', 'woocommerce' ); + } else { + $address_2_placeholder = $address_2_label; + } + + $fields = array( + 'first_name' => array( + 'label' => __( 'First name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-first' ), + 'autocomplete' => 'given-name', + 'priority' => 10, + ), + 'last_name' => array( + 'label' => __( 'Last name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-last' ), + 'autocomplete' => 'family-name', + 'priority' => 20, + ), + 'company' => array( + 'label' => __( 'Company name', 'woocommerce' ), + 'class' => array( 'form-row-wide' ), + 'autocomplete' => 'organization', + 'priority' => 30, + 'required' => 'required' === get_option( 'woocommerce_checkout_company_field', 'optional' ), + ), + 'country' => array( + 'type' => 'country', + 'label' => __( 'Country / Region', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field', 'update_totals_on_change' ), + 'autocomplete' => 'country', + 'priority' => 40, + ), + 'address_1' => array( + 'label' => __( 'Street address', 'woocommerce' ), + /* translators: use local order of street name and house number. */ + 'placeholder' => esc_attr__( 'House number and street name', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'autocomplete' => 'address-line1', + 'priority' => 50, + ), + 'address_2' => array( + 'label' => $address_2_label, + 'label_class' => array( 'screen-reader-text' ), + 'placeholder' => esc_attr( $address_2_placeholder ), + 'class' => array( 'form-row-wide', 'address-field' ), + 'autocomplete' => 'address-line2', + 'priority' => 60, + 'required' => 'required' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ), + ), + 'city' => array( + 'label' => __( 'Town / City', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'autocomplete' => 'address-level2', + 'priority' => 70, + ), + 'state' => array( + 'type' => 'state', + 'label' => __( 'State / County', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'validate' => array( 'state' ), + 'autocomplete' => 'address-level1', + 'priority' => 80, + ), + 'postcode' => array( + 'label' => __( 'Postcode / ZIP', 'woocommerce' ), + 'required' => true, + 'class' => array( 'form-row-wide', 'address-field' ), + 'validate' => array( 'postcode' ), + 'autocomplete' => 'postal-code', + 'priority' => 90, + ), + ); + + if ( 'hidden' === get_option( 'woocommerce_checkout_company_field', 'optional' ) ) { + unset( $fields['company'] ); + } + + if ( 'hidden' === get_option( 'woocommerce_checkout_address_2_field', 'optional' ) ) { + unset( $fields['address_2'] ); + } + + $default_address_fields = apply_filters( 'woocommerce_default_address_fields', $fields ); + // Sort each of the fields based on priority. + uasort( $default_address_fields, 'wc_checkout_fields_uasort_comparison' ); + + return $default_address_fields; + } + + /** + * Get JS selectors for fields which are shown/hidden depending on the locale. + * + * @return array + */ + public function get_country_locale_field_selectors() { + $locale_fields = array( + 'address_1' => '#billing_address_1_field, #shipping_address_1_field', + 'address_2' => '#billing_address_2_field, #shipping_address_2_field', + 'state' => '#billing_state_field, #shipping_state_field, #calc_shipping_state_field', + 'postcode' => '#billing_postcode_field, #shipping_postcode_field, #calc_shipping_postcode_field', + 'city' => '#billing_city_field, #shipping_city_field, #calc_shipping_city_field', + ); + return apply_filters( 'woocommerce_country_locale_field_selectors', $locale_fields ); + } + + /** + * Get country locale settings. + * + * These locales override the default country selections after a country is chosen. + * + * @return array + */ + public function get_country_locale() { + if ( empty( $this->locale ) ) { + $this->locale = apply_filters( + 'woocommerce_get_country_locale', + array( + 'AE' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'required' => false, + ), + ), + 'AF' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'AO' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'AT' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'AU' => array( + 'city' => array( + 'label' => __( 'Suburb', 'woocommerce' ), + ), + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'AX' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BA' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Canton', 'woocommerce' ), + 'required' => false, + 'hidden' => true, + ), + ), + 'BD' => array( + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), + ), + ), + 'BE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BH' => array( + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BI' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BO' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'BS' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'CA' => array( + 'postcode' => array( + 'label' => __( 'Postal code', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'CH' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Canton', 'woocommerce' ), + 'required' => false, + ), + ), + 'CL' => array( + 'city' => array( + 'required' => true, + ), + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'CN' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'CO' => array( + 'postcode' => array( + 'required' => false, + ), + ), + 'CW' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'required' => false, + ), + ), + 'CZ' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'DE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'DK' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'EE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'FI' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'FR' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'GH' => array( + 'postcode' => array( + 'required' => false, + ), + 'state' => array( + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'GP' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'GF' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'GR' => array( + 'state' => array( + 'required' => false, + ), + ), + 'GT' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'Department', 'woocommerce' ), + ), + ), + 'HK' => array( + 'postcode' => array( + 'required' => false, + ), + 'city' => array( + 'label' => __( 'Town / District', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'HU' => array( + 'last_name' => array( + 'class' => array( 'form-row-first' ), + 'priority' => 10, + ), + 'first_name' => array( + 'class' => array( 'form-row-last' ), + 'priority' => 20, + ), + 'postcode' => array( + 'class' => array( 'form-row-first', 'address-field' ), + 'priority' => 65, + ), + 'city' => array( + 'class' => array( 'form-row-last', 'address-field' ), + ), + 'address_1' => array( + 'priority' => 71, + ), + 'address_2' => array( + 'priority' => 72, + ), + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + ), + ), + 'ID' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'IE' => array( + 'postcode' => array( + 'required' => false, + 'label' => __( 'Eircode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + ), + ), + 'IS' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'IL' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'IM' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'IN' => array( + 'postcode' => array( + 'label' => __( 'PIN', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'IT' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => true, + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'JM' => array( + 'city' => array( + 'label' => __( 'Town / City / Post Office', 'woocommerce' ), + ), + 'postcode' => array( + 'required' => false, + 'label' => __( 'Postal Code', 'woocommerce' ), + ), + 'state' => array( + 'required' => true, + 'label' => __( 'Parish', 'woocommerce' ), + ), + ), + 'JP' => array( + 'last_name' => array( + 'class' => array( 'form-row-first' ), + 'priority' => 10, + ), + 'first_name' => array( + 'class' => array( 'form-row-last' ), + 'priority' => 20, + ), + 'postcode' => array( + 'class' => array( 'form-row-first', 'address-field' ), + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Prefecture', 'woocommerce' ), + 'class' => array( 'form-row-last', 'address-field' ), + 'priority' => 66, + ), + 'city' => array( + 'priority' => 67, + ), + 'address_1' => array( + 'priority' => 68, + ), + 'address_2' => array( + 'priority' => 69, + ), + ), + 'KR' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'KW' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'LV' => array( + 'state' => array( + 'label' => __( 'Municipality', 'woocommerce' ), + 'required' => false, + ), + ), + 'LB' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'MQ' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'MT' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'MZ' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'NL' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'NG' => array( + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'NZ' => array( + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'required' => false, + 'label' => __( 'Region', 'woocommerce' ), + ), + ), + 'NO' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'NP' => array( + 'state' => array( + 'label' => __( 'State / Zone', 'woocommerce' ), + ), + 'postcode' => array( + 'required' => false, + ), + ), + 'PL' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'PR' => array( + 'city' => array( + 'label' => __( 'Municipality', 'woocommerce' ), + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'PT' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'RE' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'RO' => array( + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + 'required' => true, + ), + ), + 'RS' => array( + 'city' => array( + 'required' => true, + ), + 'postcode' => array( + 'required' => true, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), + 'required' => false, + ), + ), + 'SG' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + 'city' => array( + 'required' => false, + ), + ), + 'SK' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'SI' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'SR' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'ES' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'LI' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Municipality', 'woocommerce' ), + 'required' => false, + ), + ), + 'LK' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'LU' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'MD' => array( + 'state' => array( + 'label' => __( 'Municipality / District', 'woocommerce' ), + ), + ), + 'SE' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'TR' => array( + 'postcode' => array( + 'priority' => 65, + ), + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'UG' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'city' => array( + 'label' => __( 'Town / Village', 'woocommerce' ), + 'required' => true, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), + 'required' => true, + ), + ), + 'US' => array( + 'postcode' => array( + 'label' => __( 'ZIP Code', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'State', 'woocommerce' ), + ), + ), + 'GB' => array( + 'postcode' => array( + 'label' => __( 'Postcode', 'woocommerce' ), + ), + 'state' => array( + 'label' => __( 'County', 'woocommerce' ), + 'required' => false, + ), + ), + 'ST' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + 'state' => array( + 'label' => __( 'District', 'woocommerce' ), + ), + ), + 'VN' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + 'postcode' => array( + 'priority' => 65, + 'required' => false, + 'hidden' => false, + ), + 'address_2' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'WS' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'YT' => array( + 'state' => array( + 'required' => false, + 'hidden' => true, + ), + ), + 'ZA' => array( + 'state' => array( + 'label' => __( 'Province', 'woocommerce' ), + ), + ), + 'ZW' => array( + 'postcode' => array( + 'required' => false, + 'hidden' => true, + ), + ), + ) + ); + + $this->locale = array_intersect_key( $this->locale, array_merge( $this->get_allowed_countries(), $this->get_shipping_countries() ) ); + + // Default Locale Can be filtered to override fields in get_address_fields(). Countries with no specific locale will use default. + $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_default', $this->get_default_address_fields() ); + + // Filter default AND shop base locales to allow overides via a single function. These will be used when changing countries on the checkout. + if ( ! isset( $this->locale[ $this->get_base_country() ] ) ) { + $this->locale[ $this->get_base_country() ] = $this->locale['default']; + } + + $this->locale['default'] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale['default'] ); + $this->locale[ $this->get_base_country() ] = apply_filters( 'woocommerce_get_country_locale_base', $this->locale[ $this->get_base_country() ] ); + } + + return $this->locale; + } + + /** + * Apply locale and get address fields. + * + * @param mixed $country Country. + * @param string $type Address type, defaults to 'billing_'. + * @return array + */ + public function get_address_fields( $country = '', $type = 'billing_' ) { + if ( ! $country ) { + $country = $this->get_base_country(); + } + + $fields = $this->get_default_address_fields(); + $locale = $this->get_country_locale(); + + if ( isset( $locale[ $country ] ) ) { + $fields = wc_array_overlay( $fields, $locale[ $country ] ); + } + + // Prepend field keys. + $address_fields = array(); + + foreach ( $fields as $key => $value ) { + if ( 'state' === $key ) { + $value['country_field'] = $type . 'country'; + $value['country'] = $country; + } + $address_fields[ $type . $key ] = $value; + } + + // Add email and phone fields. + if ( 'billing_' === $type ) { + if ( 'hidden' !== get_option( 'woocommerce_checkout_phone_field', 'required' ) ) { + $address_fields['billing_phone'] = array( + 'label' => __( 'Phone', 'woocommerce' ), + 'required' => 'required' === get_option( 'woocommerce_checkout_phone_field', 'required' ), + 'type' => 'tel', + 'class' => array( 'form-row-wide' ), + 'validate' => array( 'phone' ), + 'autocomplete' => 'tel', + 'priority' => 100, + ); + } + $address_fields['billing_email'] = array( + 'label' => __( 'Email address', 'woocommerce' ), + 'required' => true, + 'type' => 'email', + 'class' => array( 'form-row-wide' ), + 'validate' => array( 'email' ), + 'autocomplete' => 'no' === get_option( 'woocommerce_registration_generate_username' ) ? 'email' : 'email username', + 'priority' => 110, + ); + } + + /** + * Important note on this filter: Changes to address fields can and will be overridden by + * the woocommerce_default_address_fields. The locales/default locales apply on top based + * on country selection. If you want to change things like the required status of an + * address field, filter woocommerce_default_address_fields instead. + */ + $address_fields = apply_filters( 'woocommerce_' . $type . 'fields', $address_fields, $country ); + // Sort each of the fields based on priority. + uasort( $address_fields, 'wc_checkout_fields_uasort_comparison' ); + + return $address_fields; + } +} diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php new file mode 100644 index 0000000..d8cf7a0 --- /dev/null +++ b/includes/class-wc-coupon.php @@ -0,0 +1,1077 @@ + '', + 'amount' => 0, + 'date_created' => null, + 'date_modified' => null, + 'date_expires' => null, + 'discount_type' => 'fixed_cart', + 'description' => '', + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => 0, + 'usage_limit_per_user' => 0, + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '', + 'maximum_amount' => '', + 'email_restrictions' => array(), + 'used_by' => array(), + 'virtual' => false, + ); + + // Coupon message codes. + const E_WC_COUPON_INVALID_FILTERED = 100; + const E_WC_COUPON_INVALID_REMOVED = 101; + const E_WC_COUPON_NOT_YOURS_REMOVED = 102; + const E_WC_COUPON_ALREADY_APPLIED = 103; + const E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY = 104; + const E_WC_COUPON_NOT_EXIST = 105; + const E_WC_COUPON_USAGE_LIMIT_REACHED = 106; + const E_WC_COUPON_EXPIRED = 107; + const E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET = 108; + const E_WC_COUPON_NOT_APPLICABLE = 109; + const E_WC_COUPON_NOT_VALID_SALE_ITEMS = 110; + const E_WC_COUPON_PLEASE_ENTER = 111; + const E_WC_COUPON_MAX_SPEND_LIMIT_MET = 112; + const E_WC_COUPON_EXCLUDED_PRODUCTS = 113; + const E_WC_COUPON_EXCLUDED_CATEGORIES = 114; + const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK = 115; + const E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST = 116; + const WC_COUPON_SUCCESS = 200; + const WC_COUPON_REMOVED = 201; + + /** + * Cache group. + * + * @var string + */ + protected $cache_group = 'coupons'; + + /** + * Coupon constructor. Loads coupon data. + * + * @param mixed $data Coupon data, object, ID or code. + */ + public function __construct( $data = '' ) { + parent::__construct( $data ); + + // If we already have a coupon object, read it again. + if ( $data instanceof WC_Coupon ) { + $this->set_id( absint( $data->get_id() ) ); + $this->read_object_from_database(); + return; + } + + // This filter allows custom coupon objects to be created on the fly. + $coupon = apply_filters( 'woocommerce_get_shop_coupon_data', false, $data, $this ); + + if ( $coupon ) { + $this->read_manual_coupon( $data, $coupon ); + return; + } + + // Try to load coupon using ID or code. + if ( is_int( $data ) && 'shop_coupon' === get_post_type( $data ) ) { + $this->set_id( $data ); + } elseif ( ! empty( $data ) ) { + $id = wc_get_coupon_id_by_code( $data ); + // Need to support numeric strings for backwards compatibility. + if ( ! $id && 'shop_coupon' === get_post_type( $data ) ) { + $this->set_id( $data ); + } else { + $this->set_id( $id ); + $this->set_code( $data ); + } + } else { + $this->set_object_read( true ); + } + + $this->read_object_from_database(); + } + + /** + * If the object has an ID, read using the data store. + * + * @since 3.4.1 + */ + protected function read_object_from_database() { + $this->data_store = WC_Data_Store::load( 'coupon' ); + + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + /** + * Checks the coupon type. + * + * @param string $type Array or string of types. + * @return bool + */ + public function is_type( $type ) { + return ( $this->get_discount_type() === $type || ( is_array( $type ) && in_array( $this->get_discount_type(), $type, true ) ) ); + } + + /** + * Prefix for action and filter hooks on data. + * + * @since 3.0.0 + * @return string + */ + protected function get_hook_prefix() { + return 'woocommerce_coupon_get_'; + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + | + | Methods for getting data from the coupon object. + | + */ + + /** + * Get coupon code. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_code( $context = 'view' ) { + return $this->get_prop( 'code', $context ); + } + + /** + * Get coupon description. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_description( $context = 'view' ) { + return $this->get_prop( 'description', $context ); + } + + /** + * Get discount type. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_discount_type( $context = 'view' ) { + return $this->get_prop( 'discount_type', $context ); + } + + /** + * Get coupon amount. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return float + */ + public function get_amount( $context = 'view' ) { + return wc_format_decimal( $this->get_prop( 'amount', $context ) ); + } + + /** + * Get coupon expiration date. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_expires( $context = 'view' ) { + return $this->get_prop( 'date_expires', $context ); + } + + /** + * Get date_created + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Get date_modified + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|NULL object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Get coupon usage count. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_usage_count( $context = 'view' ) { + return $this->get_prop( 'usage_count', $context ); + } + + /** + * Get the "indvidual use" checkbox status. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_individual_use( $context = 'view' ) { + return $this->get_prop( 'individual_use', $context ); + } + + /** + * Get product IDs this coupon can apply to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_product_ids( $context = 'view' ) { + return $this->get_prop( 'product_ids', $context ); + } + + /** + * Get product IDs that this coupon should not apply to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_excluded_product_ids( $context = 'view' ) { + return $this->get_prop( 'excluded_product_ids', $context ); + } + + /** + * Get coupon usage limit. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_usage_limit( $context = 'view' ) { + return $this->get_prop( 'usage_limit', $context ); + } + + /** + * Get coupon usage limit per customer (for a single customer) + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_usage_limit_per_user( $context = 'view' ) { + return $this->get_prop( 'usage_limit_per_user', $context ); + } + + /** + * Usage limited to certain amount of items + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer|null + */ + public function get_limit_usage_to_x_items( $context = 'view' ) { + return $this->get_prop( 'limit_usage_to_x_items', $context ); + } + + /** + * If this coupon grants free shipping or not. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_free_shipping( $context = 'view' ) { + return $this->get_prop( 'free_shipping', $context ); + } + + /** + * Get product categories this coupon can apply to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_product_categories( $context = 'view' ) { + return $this->get_prop( 'product_categories', $context ); + } + + /** + * Get product categories this coupon cannot not apply to. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_excluded_product_categories( $context = 'view' ) { + return $this->get_prop( 'excluded_product_categories', $context ); + } + + /** + * If this coupon should exclude items on sale. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_exclude_sale_items( $context = 'view' ) { + return $this->get_prop( 'exclude_sale_items', $context ); + } + + /** + * Get minimum spend amount. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return float + */ + public function get_minimum_amount( $context = 'view' ) { + return $this->get_prop( 'minimum_amount', $context ); + } + /** + * Get maximum spend amount. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return float + */ + public function get_maximum_amount( $context = 'view' ) { + return $this->get_prop( 'maximum_amount', $context ); + } + + /** + * Get emails to check customer usage restrictions. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_email_restrictions( $context = 'view' ) { + return $this->get_prop( 'email_restrictions', $context ); + } + + /** + * Get records of all users who have used the current coupon. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_used_by( $context = 'view' ) { + return $this->get_prop( 'used_by', $context ); + } + + /** + * If the filter is added through the woocommerce_get_shop_coupon_data filter, it's virtual and not in the DB. + * + * @since 3.2.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return boolean + */ + public function get_virtual( $context = 'view' ) { + return (bool) $this->get_prop( 'virtual', $context ); + } + + /** + * Get discount amount for a cart item. + * + * @param float $discounting_amount Amount the coupon is being applied to. + * @param array|null $cart_item Cart item being discounted if applicable. + * @param boolean $single True if discounting a single qty item, false if its the line. + * @return float Amount this coupon has discounted. + */ + public function get_discount_amount( $discounting_amount, $cart_item = null, $single = false ) { + $discount = 0; + $cart_item_qty = is_null( $cart_item ) ? 1 : $cart_item['quantity']; + + if ( $this->is_type( array( 'percent' ) ) ) { + $discount = (float) $this->get_amount() * ( $discounting_amount / 100 ); + } elseif ( $this->is_type( 'fixed_cart' ) && ! is_null( $cart_item ) && WC()->cart->subtotal_ex_tax ) { + /** + * This is the most complex discount - we need to divide the discount between rows based on their price in. + * proportion to the subtotal. This is so rows with different tax rates get a fair discount, and so rows. + * with no price (free) don't get discounted. + * + * Get item discount by dividing item cost by subtotal to get a %. + * + * Uses price inc tax if prices include tax to work around https://github.com/woocommerce/woocommerce/issues/7669 and https://github.com/woocommerce/woocommerce/issues/8074. + */ + if ( wc_prices_include_tax() ) { + $discount_percent = ( wc_get_price_including_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal; + } else { + $discount_percent = ( wc_get_price_excluding_tax( $cart_item['data'] ) * $cart_item_qty ) / WC()->cart->subtotal_ex_tax; + } + $discount = ( (float) $this->get_amount() * $discount_percent ) / $cart_item_qty; + + } elseif ( $this->is_type( 'fixed_product' ) ) { + $discount = min( $this->get_amount(), $discounting_amount ); + $discount = $single ? $discount : $discount * $cart_item_qty; + } + + return apply_filters( + 'woocommerce_coupon_get_discount_amount', + NumberUtil::round( min( $discount, $discounting_amount ), wc_get_rounding_precision() ), + $discounting_amount, + $cart_item, + $single, + $this + ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + | + | Functions for setting coupon data. These should not update anything in the + | database itself and should only change what is stored in the class + | object. + | + */ + + /** + * Set coupon code. + * + * @since 3.0.0 + * @param string $code Coupon code. + */ + public function set_code( $code ) { + $this->set_prop( 'code', wc_format_coupon_code( $code ) ); + } + + /** + * Set coupon description. + * + * @since 3.0.0 + * @param string $description Description. + */ + public function set_description( $description ) { + $this->set_prop( 'description', $description ); + } + + /** + * Set discount type. + * + * @since 3.0.0 + * @param string $discount_type Discount type. + */ + public function set_discount_type( $discount_type ) { + if ( 'percent_product' === $discount_type ) { + $discount_type = 'percent'; // Backwards compatibility. + } + if ( ! in_array( $discount_type, array_keys( wc_get_coupon_types() ), true ) ) { + $this->error( 'coupon_invalid_discount_type', __( 'Invalid discount type', 'woocommerce' ) ); + } + $this->set_prop( 'discount_type', $discount_type ); + } + + /** + * Set amount. + * + * @since 3.0.0 + * @param float $amount Amount. + */ + public function set_amount( $amount ) { + $amount = wc_format_decimal( $amount ); + + if ( ! is_numeric( $amount ) ) { + $amount = 0; + } + + if ( $amount < 0 ) { + $this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) ); + } + + if ( 'percent' === $this->get_discount_type() && $amount > 100 ) { + $this->error( 'coupon_invalid_amount', __( 'Invalid discount amount', 'woocommerce' ) ); + } + + $this->set_prop( 'amount', $amount ); + } + + /** + * Set expiration date. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + */ + public function set_date_expires( $date ) { + $this->set_date_prop( 'date_expires', $date ); + } + + /** + * Set date_created + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + */ + public function set_date_created( $date ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set date_modified + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. + */ + public function set_date_modified( $date ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set how many times this coupon has been used. + * + * @since 3.0.0 + * @param int $usage_count Usage count. + */ + public function set_usage_count( $usage_count ) { + $this->set_prop( 'usage_count', absint( $usage_count ) ); + } + + /** + * Set if this coupon can only be used once. + * + * @since 3.0.0 + * @param bool $is_individual_use If is for individual use. + */ + public function set_individual_use( $is_individual_use ) { + $this->set_prop( 'individual_use', (bool) $is_individual_use ); + } + + /** + * Set the product IDs this coupon can be used with. + * + * @since 3.0.0 + * @param array $product_ids Products IDs. + */ + public function set_product_ids( $product_ids ) { + $this->set_prop( 'product_ids', array_filter( wp_parse_id_list( (array) $product_ids ) ) ); + } + + /** + * Set the product IDs this coupon cannot be used with. + * + * @since 3.0.0 + * @param array $excluded_product_ids Exclude product IDs. + */ + public function set_excluded_product_ids( $excluded_product_ids ) { + $this->set_prop( 'excluded_product_ids', array_filter( wp_parse_id_list( (array) $excluded_product_ids ) ) ); + } + + /** + * Set the amount of times this coupon can be used. + * + * @since 3.0.0 + * @param int $usage_limit Usage limit. + */ + public function set_usage_limit( $usage_limit ) { + $this->set_prop( 'usage_limit', absint( $usage_limit ) ); + } + + /** + * Set the amount of times this coupon can be used per user. + * + * @since 3.0.0 + * @param int $usage_limit Usage limit. + */ + public function set_usage_limit_per_user( $usage_limit ) { + $this->set_prop( 'usage_limit_per_user', absint( $usage_limit ) ); + } + + /** + * Set usage limit to x number of items. + * + * @since 3.0.0 + * @param int|null $limit_usage_to_x_items Limit usage to X items. + */ + public function set_limit_usage_to_x_items( $limit_usage_to_x_items ) { + $this->set_prop( 'limit_usage_to_x_items', is_null( $limit_usage_to_x_items ) ? null : absint( $limit_usage_to_x_items ) ); + } + + /** + * Set if this coupon enables free shipping or not. + * + * @since 3.0.0 + * @param bool $free_shipping If grant free shipping. + */ + public function set_free_shipping( $free_shipping ) { + $this->set_prop( 'free_shipping', (bool) $free_shipping ); + } + + /** + * Set the product category IDs this coupon can be used with. + * + * @since 3.0.0 + * @param array $product_categories List of product categories. + */ + public function set_product_categories( $product_categories ) { + $this->set_prop( 'product_categories', array_filter( wp_parse_id_list( (array) $product_categories ) ) ); + } + + /** + * Set the product category IDs this coupon cannot be used with. + * + * @since 3.0.0 + * @param array $excluded_product_categories List of excluded product categories. + */ + public function set_excluded_product_categories( $excluded_product_categories ) { + $this->set_prop( 'excluded_product_categories', array_filter( wp_parse_id_list( (array) $excluded_product_categories ) ) ); + } + + /** + * Set if this coupon should excluded sale items or not. + * + * @since 3.0.0 + * @param bool $exclude_sale_items If should exclude sale items. + */ + public function set_exclude_sale_items( $exclude_sale_items ) { + $this->set_prop( 'exclude_sale_items', (bool) $exclude_sale_items ); + } + + /** + * Set the minimum spend amount. + * + * @since 3.0.0 + * @param float $amount Minium amount. + */ + public function set_minimum_amount( $amount ) { + $this->set_prop( 'minimum_amount', wc_format_decimal( $amount ) ); + } + + /** + * Set the maximum spend amount. + * + * @since 3.0.0 + * @param float $amount Maximum amount. + */ + public function set_maximum_amount( $amount ) { + $this->set_prop( 'maximum_amount', wc_format_decimal( $amount ) ); + } + + /** + * Set email restrictions. + * + * @since 3.0.0 + * @param array $emails List of emails. + */ + public function set_email_restrictions( $emails = array() ) { + $emails = array_filter( array_map( 'sanitize_email', array_map( 'strtolower', (array) $emails ) ) ); + foreach ( $emails as $email ) { + if ( ! is_email( $email ) ) { + $this->error( 'coupon_invalid_email_address', __( 'Invalid email address restriction', 'woocommerce' ) ); + } + } + $this->set_prop( 'email_restrictions', $emails ); + } + + /** + * Set which users have used this coupon. + * + * @since 3.0.0 + * @param array $used_by List of user IDs. + */ + public function set_used_by( $used_by ) { + $this->set_prop( 'used_by', array_filter( $used_by ) ); + } + + /** + * Set coupon virtual state. + * + * @param boolean $virtual Whether it is virtual or not. + * @since 3.2.0 + */ + public function set_virtual( $virtual ) { + $this->set_prop( 'virtual', (bool) $virtual ); + } + + /* + |-------------------------------------------------------------------------- + | Other Actions + |-------------------------------------------------------------------------- + */ + + /** + * Developers can programmatically return coupons. This function will read those values into our WC_Coupon class. + * + * @since 3.0.0 + * @param string $code Coupon code. + * @param array $coupon Array of coupon properties. + */ + public function read_manual_coupon( $code, $coupon ) { + foreach ( $coupon as $key => $value ) { + switch ( $key ) { + case 'excluded_product_ids': + case 'exclude_product_ids': + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon['excluded_product_ids'] = wc_string_to_array( $value ); + } + break; + case 'exclude_product_categories': + case 'excluded_product_categories': + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon['excluded_product_categories'] = wc_string_to_array( $value ); + } + break; + case 'product_ids': + if ( ! is_array( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be an array instead of a string.', '3.0' ); + $coupon[ $key ] = wc_string_to_array( $value ); + } + break; + case 'individual_use': + case 'free_shipping': + case 'exclude_sale_items': + if ( ! is_bool( $coupon[ $key ] ) ) { + wc_doing_it_wrong( $key, $key . ' should be true or false instead of yes or no.', '3.0' ); + $coupon[ $key ] = wc_string_to_bool( $value ); + } + break; + case 'expiry_date': + $coupon['date_expires'] = $value; + break; + } + } + $this->set_props( $coupon ); + $this->set_code( $code ); + $this->set_id( 0 ); + $this->set_virtual( true ); + } + + /** + * Increase usage count for current coupon. + * + * @param string $used_by Either user ID or billing email. + * @param WC_Order $order If provided, will clear the coupons held by this order. + */ + public function increase_usage_count( $used_by = '', $order = null ) { + if ( $this->get_id() && $this->data_store ) { + $new_count = $this->data_store->increase_usage_count( $this, $used_by, $order ); + + // Bypass set_prop and remove pending changes since the data store saves the count already. + $this->data['usage_count'] = $new_count; + if ( isset( $this->changes['usage_count'] ) ) { + unset( $this->changes['usage_count'] ); + } + } + } + + /** + * Decrease usage count for current coupon. + * + * @param string $used_by Either user ID or billing email. + */ + public function decrease_usage_count( $used_by = '' ) { + if ( $this->get_id() && $this->get_usage_count() > 0 && $this->data_store ) { + $new_count = $this->data_store->decrease_usage_count( $this, $used_by ); + + // Bypass set_prop and remove pending changes since the data store saves the count already. + $this->data['usage_count'] = $new_count; + if ( isset( $this->changes['usage_count'] ) ) { + unset( $this->changes['usage_count'] ); + } + } + } + + /* + |-------------------------------------------------------------------------- + | Validation & Error Handling + |-------------------------------------------------------------------------- + */ + + /** + * Returns the error_message string. + + * @return string + */ + public function get_error_message() { + return $this->error_message; + } + + /** + * Check if a coupon is valid for the cart. + * + * @deprecated 3.2.0 In favor of WC_Discounts->is_coupon_valid. + * @return bool + */ + public function is_valid() { + $discounts = new WC_Discounts( WC()->cart ); + $valid = $discounts->is_coupon_valid( $this ); + + if ( is_wp_error( $valid ) ) { + $this->error_message = $valid->get_error_message(); + return false; + } + + return $valid; + } + + /** + * Check if a coupon is valid. + * + * @return bool + */ + public function is_valid_for_cart() { + return apply_filters( 'woocommerce_coupon_is_valid_for_cart', $this->is_type( wc_get_cart_coupon_types() ), $this ); + } + + /** + * Check if a coupon is valid for a product. + * + * @param WC_Product $product Product instance. + * @param array $values Values. + * @return bool + */ + public function is_valid_for_product( $product, $values = array() ) { + if ( ! $this->is_type( wc_get_product_coupon_types() ) ) { + return apply_filters( 'woocommerce_coupon_is_valid_for_product', false, $product, $this, $values ); + } + + $valid = false; + $product_cats = wc_get_product_cat_ids( $product->is_type( 'variation' ) ? $product->get_parent_id() : $product->get_id() ); + $product_ids = array( $product->get_id(), $product->get_parent_id() ); + + // Specific products get the discount. + if ( count( $this->get_product_ids() ) && count( array_intersect( $product_ids, $this->get_product_ids() ) ) ) { + $valid = true; + } + + // Category discounts. + if ( count( $this->get_product_categories() ) && count( array_intersect( $product_cats, $this->get_product_categories() ) ) ) { + $valid = true; + } + + // No product ids - all items discounted. + if ( ! count( $this->get_product_ids() ) && ! count( $this->get_product_categories() ) ) { + $valid = true; + } + + // Specific product IDs excluded from the discount. + if ( count( $this->get_excluded_product_ids() ) && count( array_intersect( $product_ids, $this->get_excluded_product_ids() ) ) ) { + $valid = false; + } + + // Specific categories excluded from the discount. + if ( count( $this->get_excluded_product_categories() ) && count( array_intersect( $product_cats, $this->get_excluded_product_categories() ) ) ) { + $valid = false; + } + + // Sale Items excluded from discount. + if ( $this->get_exclude_sale_items() && $product->is_on_sale() ) { + $valid = false; + } + + return apply_filters( 'woocommerce_coupon_is_valid_for_product', $valid, $product, $this, $values ); + } + + /** + * Converts one of the WC_Coupon message/error codes to a message string and. + * displays the message/error. + * + * @param int $msg_code Message/error code. + */ + public function add_coupon_message( $msg_code ) { + $msg = $msg_code < 200 ? $this->get_coupon_error( $msg_code ) : $this->get_coupon_message( $msg_code ); + + if ( ! $msg ) { + return; + } + + if ( $msg_code < 200 ) { + wc_add_notice( $msg, 'error' ); + } else { + wc_add_notice( $msg ); + } + } + + /** + * Map one of the WC_Coupon message codes to a message string. + * + * @param integer $msg_code Message code. + * @return string Message/error string. + */ + public function get_coupon_message( $msg_code ) { + switch ( $msg_code ) { + case self::WC_COUPON_SUCCESS: + $msg = __( 'Coupon code applied successfully.', 'woocommerce' ); + break; + case self::WC_COUPON_REMOVED: + $msg = __( 'Coupon code removed successfully.', 'woocommerce' ); + break; + default: + $msg = ''; + break; + } + return apply_filters( 'woocommerce_coupon_message', $msg, $msg_code, $this ); + } + + /** + * Map one of the WC_Coupon error codes to a message string. + * + * @param int $err_code Message/error code. + * @return string Message/error string + */ + public function get_coupon_error( $err_code ) { + switch ( $err_code ) { + case self::E_WC_COUPON_INVALID_FILTERED: + $err = __( 'Coupon is not valid.', 'woocommerce' ); + break; + case self::E_WC_COUPON_NOT_EXIST: + /* translators: %s: coupon code */ + $err = sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $this->get_code() ) ); + break; + case self::E_WC_COUPON_INVALID_REMOVED: + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, it seems the coupon "%s" is invalid - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) ); + break; + case self::E_WC_COUPON_NOT_YOURS_REMOVED: + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, it seems the coupon "%s" is not yours - it has now been removed from your order.', 'woocommerce' ), esc_html( $this->get_code() ) ); + break; + case self::E_WC_COUPON_ALREADY_APPLIED: + $err = __( 'Coupon code already applied!', 'woocommerce' ); + break; + case self::E_WC_COUPON_ALREADY_APPLIED_INDIV_USE_ONLY: + /* translators: %s: coupon code */ + $err = sprintf( __( 'Sorry, coupon "%s" has already been applied and cannot be used in conjunction with other coupons.', 'woocommerce' ), esc_html( $this->get_code() ) ); + break; + case self::E_WC_COUPON_USAGE_LIMIT_REACHED: + $err = __( 'Coupon usage limit has been reached.', 'woocommerce' ); + break; + case self::E_WC_COUPON_EXPIRED: + $err = __( 'This coupon has expired.', 'woocommerce' ); + break; + case self::E_WC_COUPON_MIN_SPEND_LIMIT_NOT_MET: + /* translators: %s: coupon minimum amount */ + $err = sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_minimum_amount() ) ); + break; + case self::E_WC_COUPON_MAX_SPEND_LIMIT_MET: + /* translators: %s: coupon maximum amount */ + $err = sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $this->get_maximum_amount() ) ); + break; + case self::E_WC_COUPON_NOT_APPLICABLE: + $err = __( 'Sorry, this coupon is not applicable to your cart contents.', 'woocommerce' ); + break; + case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK: + if ( is_user_logged_in() && wc_get_page_id( 'myaccount' ) > 0 ) { + /* translators: %s: myaccount page link. */ + $err = sprintf( __( 'Coupon usage limit has been reached. If you were using this coupon just now but order was not complete, you can retry or cancel the order by going to the my account page.', 'woocommerce' ), wc_get_endpoint_url( 'orders', '', wc_get_page_permalink( 'myaccount' ) ) ); + } else { + $err = $this->get_coupon_error( self::E_WC_COUPON_USAGE_LIMIT_REACHED ); + } + break; + case self::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST: + $err = __( 'Coupon usage limit has been reached. Please try again after some time, or contact us for help.', 'woocommerce' ); + break; + case self::E_WC_COUPON_EXCLUDED_PRODUCTS: + // Store excluded products that are in cart in $products. + $products = array(); + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + if ( in_array( intval( $cart_item['product_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['variation_id'] ), $this->get_excluded_product_ids(), true ) || in_array( intval( $cart_item['data']->get_parent_id() ), $this->get_excluded_product_ids(), true ) ) { + $products[] = $cart_item['data']->get_name(); + } + } + } + + /* translators: %s: products list */ + $err = sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ); + break; + case self::E_WC_COUPON_EXCLUDED_CATEGORIES: + // Store excluded categories that are in cart in $categories. + $categories = array(); + if ( ! WC()->cart->is_empty() ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $product_cats = wc_get_product_cat_ids( $cart_item['product_id'] ); + $intersect = array_intersect( $product_cats, $this->get_excluded_product_categories() ); + + if ( count( $intersect ) > 0 ) { + foreach ( $intersect as $cat_id ) { + $cat = get_term( $cat_id, 'product_cat' ); + $categories[] = $cat->name; + } + } + } + } + + /* translators: %s: categories list */ + $err = sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ); + break; + case self::E_WC_COUPON_NOT_VALID_SALE_ITEMS: + $err = __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ); + break; + default: + $err = ''; + break; + } + return apply_filters( 'woocommerce_coupon_error', $err, $err_code, $this ); + } + + /** + * Map one of the WC_Coupon error codes to an error string. + * No coupon instance will be available where a coupon does not exist, + * so this static method exists. + * + * @param int $err_code Error code. + * @return string Error string. + */ + public static function get_generic_coupon_error( $err_code ) { + switch ( $err_code ) { + case self::E_WC_COUPON_NOT_EXIST: + $err = __( 'Coupon does not exist!', 'woocommerce' ); + break; + case self::E_WC_COUPON_PLEASE_ENTER: + $err = __( 'Please enter a coupon code.', 'woocommerce' ); + break; + default: + $err = ''; + break; + } + // When using this static method, there is no $this to pass to filter. + return apply_filters( 'woocommerce_coupon_error', $err, $err_code, null ); + } +} diff --git a/includes/class-wc-customer-download-log.php b/includes/class-wc-customer-download-log.php new file mode 100644 index 0000000..cec6c05 --- /dev/null +++ b/includes/class-wc-customer-download-log.php @@ -0,0 +1,150 @@ + null, + 'permission_id' => 0, + 'user_id' => null, + 'user_ip_address' => null, + ); + + /** + * Constructor. + * + * @param int|object|array $download_log Download log ID. + */ + public function __construct( $download_log = 0 ) { + parent::__construct( $download_log ); + + if ( is_numeric( $download_log ) && $download_log > 0 ) { + $this->set_id( $download_log ); + } elseif ( $download_log instanceof self ) { + $this->set_id( $download_log->get_id() ); + } elseif ( is_object( $download_log ) && ! empty( $download_log->download_log_id ) ) { + $this->set_id( $download_log->download_log_id ); + $this->set_props( (array) $download_log ); + $this->set_object_read( true ); + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( 'customer-download-log' ); + + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get timestamp. + * + * @param string $context Get context. + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_timestamp( $context = 'view' ) { + return $this->get_prop( 'timestamp', $context ); + } + + /** + * Get permission id. + * + * @param string $context Get context. + * @return integer + */ + public function get_permission_id( $context = 'view' ) { + return $this->get_prop( 'permission_id', $context ); + } + + /** + * Get user id. + * + * @param string $context Get context. + * @return integer + */ + public function get_user_id( $context = 'view' ) { + return $this->get_prop( 'user_id', $context ); + } + + /** + * Get user ip address. + * + * @param string $context Get context. + * @return string + */ + public function get_user_ip_address( $context = 'view' ) { + return $this->get_prop( 'user_ip_address', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set timestamp. + * + * @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_timestamp( $date = null ) { + $this->set_date_prop( 'timestamp', $date ); + } + + /** + * Set permission id. + * + * @param int $value Value to set. + */ + public function set_permission_id( $value ) { + $this->set_prop( 'permission_id', absint( $value ) ); + } + + /** + * Set user id. + * + * @param int $value Value to set. + */ + public function set_user_id( $value ) { + $this->set_prop( 'user_id', absint( $value ) ); + } + + /** + * Set user ip address. + * + * @param string $value Value to set. + */ + public function set_user_ip_address( $value ) { + $this->set_prop( 'user_ip_address', $value ); + } +} diff --git a/includes/class-wc-customer-download.php b/includes/class-wc-customer-download.php new file mode 100644 index 0000000..1e1a6d3 --- /dev/null +++ b/includes/class-wc-customer-download.php @@ -0,0 +1,407 @@ + '', + 'product_id' => 0, + 'user_id' => 0, + 'user_email' => '', + 'order_id' => 0, + 'order_key' => '', + 'downloads_remaining' => '', + 'access_granted' => null, + 'access_expires' => null, + 'download_count' => 0, + ); + + /** + * Constructor. + * + * @param int|object|array $download Download ID, instance or data. + */ + public function __construct( $download = 0 ) { + parent::__construct( $download ); + + if ( is_numeric( $download ) && $download > 0 ) { + $this->set_id( $download ); + } elseif ( $download instanceof self ) { + $this->set_id( $download->get_id() ); + } elseif ( is_object( $download ) && ! empty( $download->permission_id ) ) { + $this->set_id( $download->permission_id ); + $this->set_props( (array) $download ); + $this->set_object_read( true ); + } else { + $this->set_object_read( true ); + } + + $this->data_store = WC_Data_Store::load( 'customer-download' ); + + if ( $this->get_id() > 0 ) { + $this->data_store->read( $this ); + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get download id. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_download_id( $context = 'view' ) { + return $this->get_prop( 'download_id', $context ); + } + + /** + * Get product id. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_product_id( $context = 'view' ) { + return $this->get_prop( 'product_id', $context ); + } + + /** + * Get user id. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_user_id( $context = 'view' ) { + return $this->get_prop( 'user_id', $context ); + } + + /** + * Get user_email. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_user_email( $context = 'view' ) { + return $this->get_prop( 'user_email', $context ); + } + + /** + * Get order_id. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_order_id( $context = 'view' ) { + return $this->get_prop( 'order_id', $context ); + } + + /** + * Get order_key. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_order_key( $context = 'view' ) { + return $this->get_prop( 'order_key', $context ); + } + + /** + * Get downloads_remaining. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer|string + */ + public function get_downloads_remaining( $context = 'view' ) { + return $this->get_prop( 'downloads_remaining', $context ); + } + + /** + * Get access_granted. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_access_granted( $context = 'view' ) { + return $this->get_prop( 'access_granted', $context ); + } + + /** + * Get access_expires. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_access_expires( $context = 'view' ) { + return $this->get_prop( 'access_expires', $context ); + } + + /** + * Get download_count. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return integer + */ + public function get_download_count( $context = 'view' ) { + // Check for count of download logs. + $data_store = WC_Data_Store::load( 'customer-download-log' ); + $download_log_ids = $data_store->get_download_logs_for_permission( $this->get_id() ); + + $download_log_count = 0; + if ( ! empty( $download_log_ids ) ) { + $download_log_count = count( $download_log_ids ); + } + + // Check download count in prop. + $download_count_prop = $this->get_prop( 'download_count', $context ); + + // Return the larger of the two in case they differ. + // If logs are removed for some reason, we should still respect the + // count stored in the prop. + return max( $download_log_count, $download_count_prop ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set download id. + * + * @param string $value Download ID. + */ + public function set_download_id( $value ) { + $this->set_prop( 'download_id', $value ); + } + /** + * Set product id. + * + * @param int $value Product ID. + */ + public function set_product_id( $value ) { + $this->set_prop( 'product_id', absint( $value ) ); + } + + /** + * Set user id. + * + * @param int $value User ID. + */ + public function set_user_id( $value ) { + $this->set_prop( 'user_id', absint( $value ) ); + } + + /** + * Set user_email. + * + * @param int $value User email. + */ + public function set_user_email( $value ) { + $this->set_prop( 'user_email', sanitize_email( $value ) ); + } + + /** + * Set order_id. + * + * @param int $value Order ID. + */ + public function set_order_id( $value ) { + $this->set_prop( 'order_id', absint( $value ) ); + } + + /** + * Set order_key. + * + * @param string $value Order key. + */ + public function set_order_key( $value ) { + $this->set_prop( 'order_key', $value ); + } + + /** + * Set downloads_remaining. + * + * @param integer|string $value Amount of downloads remaining. + */ + public function set_downloads_remaining( $value ) { + $this->set_prop( 'downloads_remaining', '' === $value ? '' : absint( $value ) ); + } + + /** + * Set access_granted. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_access_granted( $date = null ) { + $this->set_date_prop( 'access_granted', $date ); + } + + /** + * Set access_expires. + * + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_access_expires( $date = null ) { + $this->set_date_prop( 'access_expires', $date ); + } + + /** + * Set download_count. + * + * @param int $value Download count. + */ + public function set_download_count( $value ) { + $this->set_prop( 'download_count', absint( $value ) ); + } + + /** + * Track a download on this permission. + * + * @since 3.3.0 + * @throws Exception When permission ID is invalid. + * @param int $user_id Id of the user performing the download. + * @param string $user_ip_address IP Address of the user performing the download. + */ + public function track_download( $user_id = null, $user_ip_address = null ) { + global $wpdb; + + // Must have a permission_id to track download log. + if ( ! ( $this->get_id() > 0 ) ) { + throw new Exception( __( 'Invalid permission ID.', 'woocommerce' ) ); + } + + // Increment download count, and decrement downloads remaining. + // Use SQL to avoid possible issues with downloads in quick succession. + // If downloads_remaining is blank, leave it blank (unlimited). + // Also, ensure downloads_remaining doesn't drop below zero. + $query = $wpdb->prepare( + " +UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions +SET download_count = download_count + 1, +downloads_remaining = IF( downloads_remaining = '', '', GREATEST( 0, downloads_remaining - 1 ) ) +WHERE permission_id = %d", + $this->get_id() + ); + $wpdb->query( $query ); // WPCS: unprepared SQL ok. + + // Re-read this download from the data store to pull updated counts. + $this->data_store->read( $this ); + + // Track download in download log. + $download_log = new WC_Customer_Download_Log(); + $download_log->set_timestamp( current_time( 'timestamp', true ) ); + $download_log->set_permission_id( $this->get_id() ); + + if ( ! is_null( $user_id ) ) { + $download_log->set_user_id( $user_id ); + } + + if ( ! is_null( $user_ip_address ) ) { + $download_log->set_user_ip_address( $user_ip_address ); + } + + $download_log->save(); + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * OffsetGet. + * + * @param string $offset Offset. + * @return mixed + */ + public function offsetGet( $offset ) { + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + } + + /** + * OffsetSet. + * + * @param string $offset Offset. + * @param mixed $value Value. + */ + public function offsetSet( $offset, $value ) { + if ( is_callable( array( $this, "set_$offset" ) ) ) { + $this->{"set_$offset"}( $value ); + } + } + + /** + * OffsetUnset + * + * @param string $offset Offset. + */ + public function offsetUnset( $offset ) { + if ( is_callable( array( $this, "set_$offset" ) ) ) { + $this->{"set_$offset"}( '' ); + } + } + + /** + * OffsetExists. + * + * @param string $offset Offset. + * @return bool + */ + public function offsetExists( $offset ) { + return in_array( $offset, array_keys( $this->data ), true ); + } + + /** + * Magic __isset method for backwards compatibility. Legacy properties which could be accessed directly in the past. + * + * @param string $key Key name. + * @return bool + */ + public function __isset( $key ) { + return in_array( $key, array_keys( $this->data ), true ); + } + + /** + * Magic __get method for backwards compatibility. Maps legacy vars to new getters. + * + * @param string $key Key name. + * @return mixed + */ + public function __get( $key ) { + if ( is_callable( array( $this, "get_$key" ) ) ) { + return $this->{"get_$key"}( '' ); + } + } +} diff --git a/includes/class-wc-customer.php b/includes/class-wc-customer.php new file mode 100644 index 0000000..43587c5 --- /dev/null +++ b/includes/class-wc-customer.php @@ -0,0 +1,1149 @@ + null, + 'date_modified' => null, + 'email' => '', + 'first_name' => '', + 'last_name' => '', + 'display_name' => '', + 'role' => 'customer', + 'username' => '', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'postcode' => '', + 'country' => '', + 'state' => '', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'postcode' => '', + 'country' => '', + 'state' => '', + 'phone' => '', + ), + 'is_paying_customer' => false, + ); + + /** + * Stores a password if this needs to be changed. Write-only and hidden from _data. + * + * @var string + */ + protected $password = ''; + + /** + * Stores if user is VAT exempt for this session. + * + * @var string + */ + protected $is_vat_exempt = false; + + /** + * Stores if user has calculated shipping in this session. + * + * @var string + */ + protected $calculated_shipping = false; + + /** + * This is the name of this object type. + * + * @since 5.6.0 + * @var string + */ + protected $object_type = 'customer'; + + /** + * Load customer data based on how WC_Customer is called. + * + * If $customer is 'new', you can build a new WC_Customer object. If it's empty, some + * data will be pulled from the session for the current user/customer. + * + * @param WC_Customer|int $data Customer ID or data. + * @param bool $is_session True if this is the customer session. + * @throws Exception If customer cannot be read/found and $data is set. + */ + public function __construct( $data = 0, $is_session = false ) { + parent::__construct( $data ); + + if ( $data instanceof WC_Customer ) { + $this->set_id( absint( $data->get_id() ) ); + } elseif ( is_numeric( $data ) ) { + $this->set_id( $data ); + } + + $this->data_store = WC_Data_Store::load( 'customer' ); + + // If we have an ID, load the user from the DB. + if ( $this->get_id() ) { + try { + $this->data_store->read( $this ); + } catch ( Exception $e ) { + $this->set_id( 0 ); + $this->set_object_read( true ); + } + } else { + $this->set_object_read( true ); + } + + // If this is a session, set or change the data store to sessions. Changes do not persist in the database. + if ( $is_session && isset( WC()->session ) ) { + $this->data_store = WC_Data_Store::load( 'customer-session' ); + $this->data_store->read( $this ); + } + } + + /** + * Delete a customer and reassign posts.. + * + * @param int $reassign Reassign posts and links to new User ID. + * @since 3.0.0 + * @return bool + */ + public function delete_and_reassign( $reassign = null ) { + if ( $this->data_store ) { + $this->data_store->delete( + $this, + array( + 'force_delete' => true, + 'reassign' => $reassign, + ) + ); + $this->set_id( 0 ); + return true; + } + return false; + } + + /** + * Is customer outside base country (for tax purposes)? + * + * @return bool + */ + public function is_customer_outside_base() { + list( $country, $state ) = $this->get_taxable_address(); + if ( $country ) { + $default = wc_get_base_location(); + if ( $default['country'] !== $country ) { + return true; + } + if ( $default['state'] && $default['state'] !== $state ) { + return true; + } + } + return false; + } + + /** + * Return this customer's avatar. + * + * @since 3.0.0 + * @return string + */ + public function get_avatar_url() { + return get_avatar_url( $this->get_email() ); + } + + /** + * Get taxable address. + * + * @return array + */ + public function get_taxable_address() { + $tax_based_on = get_option( 'woocommerce_tax_based_on' ); + + // Check shipping method at this point to see if we need special handling. + if ( true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && count( array_intersect( wc_get_chosen_shipping_method_ids(), apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) ) ) ) > 0 ) { + $tax_based_on = 'base'; + } + + if ( 'base' === $tax_based_on ) { + $country = WC()->countries->get_base_country(); + $state = WC()->countries->get_base_state(); + $postcode = WC()->countries->get_base_postcode(); + $city = WC()->countries->get_base_city(); + } elseif ( 'billing' === $tax_based_on ) { + $country = $this->get_billing_country(); + $state = $this->get_billing_state(); + $postcode = $this->get_billing_postcode(); + $city = $this->get_billing_city(); + } else { + $country = $this->get_shipping_country(); + $state = $this->get_shipping_state(); + $postcode = $this->get_shipping_postcode(); + $city = $this->get_shipping_city(); + } + + return apply_filters( 'woocommerce_customer_taxable_address', array( $country, $state, $postcode, $city ) ); + } + + /** + * Gets a customer's downloadable products. + * + * @return array Array of downloadable products + */ + public function get_downloadable_products() { + $downloads = array(); + if ( $this->get_id() ) { + $downloads = wc_get_customer_available_downloads( $this->get_id() ); + } + return apply_filters( 'woocommerce_customer_get_downloadable_products', $downloads ); + } + + /** + * Is customer VAT exempt? + * + * @return bool + */ + public function is_vat_exempt() { + return $this->get_is_vat_exempt(); + } + + /** + * Has calculated shipping? + * + * @return bool + */ + public function has_calculated_shipping() { + return $this->get_calculated_shipping(); + } + + /** + * Indicates if the customer has a non-empty shipping address. + * + * Note that this does not indicate if the customer's shipping address + * is complete, only that one or more fields are populated. + * + * @since 5.3.0 + * + * @return bool + */ + public function has_shipping_address() { + foreach ( $this->get_shipping() as $address_field ) { + // Trim guards against a case where a subset of saved shipping address fields contain whitespace. + if ( strlen( trim( $address_field ) ) > 0 ) { + return true; + } + } + + return false; + } + + /** + * Get if customer is VAT exempt? + * + * @since 3.0.0 + * @return bool + */ + public function get_is_vat_exempt() { + return $this->is_vat_exempt; + } + + /** + * Get password (only used when updating the user object). + * + * @return string + */ + public function get_password() { + return $this->password; + } + + /** + * Has customer calculated shipping? + * + * @return bool + */ + public function get_calculated_shipping() { + return $this->calculated_shipping; + } + + /** + * Set if customer has tax exemption. + * + * @param bool $is_vat_exempt If is vat exempt. + */ + public function set_is_vat_exempt( $is_vat_exempt ) { + $this->is_vat_exempt = wc_string_to_bool( $is_vat_exempt ); + } + + /** + * Calculated shipping? + * + * @param bool $calculated If shipping is calculated. + */ + public function set_calculated_shipping( $calculated = true ) { + $this->calculated_shipping = wc_string_to_bool( $calculated ); + } + + /** + * Set customer's password. + * + * @since 3.0.0 + * @param string $password Password. + */ + public function set_password( $password ) { + $this->password = $password; + } + + /** + * Gets the customers last order. + * + * @return WC_Order|false + */ + public function get_last_order() { + return $this->data_store->get_last_order( $this ); + } + + /** + * Return the number of orders this customer has. + * + * @return integer + */ + public function get_order_count() { + return $this->data_store->get_order_count( $this ); + } + + /** + * Return how much money this customer has spent. + * + * @return float + */ + public function get_total_spent() { + return $this->data_store->get_total_spent( $this ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Return the customer's username. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_username( $context = 'view' ) { + return $this->get_prop( 'username', $context ); + } + + /** + * Return the customer's email. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_email( $context = 'view' ) { + return $this->get_prop( 'email', $context ); + } + + /** + * Return customer's first name. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_first_name( $context = 'view' ) { + return $this->get_prop( 'first_name', $context ); + } + + /** + * Return customer's last name. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_last_name( $context = 'view' ) { + return $this->get_prop( 'last_name', $context ); + } + + /** + * Return customer's display name. + * + * @since 3.1.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_display_name( $context = 'view' ) { + return $this->get_prop( 'display_name', $context ); + } + + /** + * Return customer's user role. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_role( $context = 'view' ) { + return $this->get_prop( 'role', $context ); + } + + /** + * Return the date this customer was created. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|null object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Return the date this customer was last updated. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return WC_DateTime|null object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Gets a prop for a getter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to get. + * @param string $address billing or shipping. + * @param string $context What the value is for. Valid values are 'view' and 'edit'. What the value is for. Valid values are view and edit. + * @return mixed + */ + protected function get_address_prop( $prop, $address = 'billing', $context = 'view' ) { + $value = null; + + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + $value = isset( $this->changes[ $address ][ $prop ] ) ? $this->changes[ $address ][ $prop ] : $this->data[ $address ][ $prop ]; + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $address . '_' . $prop, $value, $this ); + } + } + return $value; + } + + /** + * Get billing. + * + * @since 3.2.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_billing( $context = 'view' ) { + $value = null; + $prop = 'billing'; + + if ( array_key_exists( $prop, $this->data ) ) { + $changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array(); + $value = array_merge( $this->data[ $prop ], $changes ); + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); + } + } + + return $value; + } + + /** + * Get billing_first_name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'billing', $context ); + } + + /** + * Get billing_last_name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'billing', $context ); + } + + /** + * Get billing_company. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'billing', $context ); + } + + /** + * Get billing_address_1. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_address( $context = 'view' ) { + return $this->get_billing_address_1( $context ); + } + + /** + * Get billing_address_1. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'billing', $context ); + } + + /** + * Get billing_address_2. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string $value + */ + public function get_billing_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'billing', $context ); + } + + /** + * Get billing_city. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string $value + */ + public function get_billing_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'billing', $context ); + } + + /** + * Get billing_state. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'billing', $context ); + } + + /** + * Get billing_postcode. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'billing', $context ); + } + + /** + * Get billing_country. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'billing', $context ); + } + + /** + * Get billing_email. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_email( $context = 'view' ) { + return $this->get_address_prop( 'email', 'billing', $context ); + } + + /** + * Get billing_phone. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_billing_phone( $context = 'view' ) { + return $this->get_address_prop( 'phone', 'billing', $context ); + } + + /** + * Get shipping. + * + * @since 3.2.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return array + */ + public function get_shipping( $context = 'view' ) { + $value = null; + $prop = 'shipping'; + + if ( array_key_exists( $prop, $this->data ) ) { + $changes = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : array(); + $value = array_merge( $this->data[ $prop ], $changes ); + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); + } + } + + return $value; + } + + /** + * Get shipping_first_name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_first_name( $context = 'view' ) { + return $this->get_address_prop( 'first_name', 'shipping', $context ); + } + + /** + * Get shipping_last_name. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_last_name( $context = 'view' ) { + return $this->get_address_prop( 'last_name', 'shipping', $context ); + } + + /** + * Get shipping_company. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_company( $context = 'view' ) { + return $this->get_address_prop( 'company', 'shipping', $context ); + } + + /** + * Get shipping_address_1. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_address( $context = 'view' ) { + return $this->get_shipping_address_1( $context ); + } + + /** + * Get shipping_address_1. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_address_1( $context = 'view' ) { + return $this->get_address_prop( 'address_1', 'shipping', $context ); + } + + /** + * Get shipping_address_2. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_address_2( $context = 'view' ) { + return $this->get_address_prop( 'address_2', 'shipping', $context ); + } + + /** + * Get shipping_city. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_city( $context = 'view' ) { + return $this->get_address_prop( 'city', 'shipping', $context ); + } + + /** + * Get shipping_state. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_state( $context = 'view' ) { + return $this->get_address_prop( 'state', 'shipping', $context ); + } + + /** + * Get shipping_postcode. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_postcode( $context = 'view' ) { + return $this->get_address_prop( 'postcode', 'shipping', $context ); + } + + /** + * Get shipping_country. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_country( $context = 'view' ) { + return $this->get_address_prop( 'country', 'shipping', $context ); + } + + /** + * Get shipping phone. + * + * @since 5.6.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_shipping_phone( $context = 'view' ) { + return $this->get_address_prop( 'phone', 'shipping', $context ); + } + + /** + * Is the user a paying customer? + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_is_paying_customer( $context = 'view' ) { + return $this->get_prop( 'is_paying_customer', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set customer's username. + * + * @since 3.0.0 + * @param string $username Username. + */ + public function set_username( $username ) { + $this->set_prop( 'username', $username ); + } + + /** + * Set customer's email. + * + * @since 3.0.0 + * @param string $value Email. + */ + public function set_email( $value ) { + if ( $value && ! is_email( $value ) ) { + $this->error( 'customer_invalid_email', __( 'Invalid email address', 'woocommerce' ) ); + } + $this->set_prop( 'email', sanitize_email( $value ) ); + } + + /** + * Set customer's first name. + * + * @since 3.0.0 + * @param string $first_name First name. + */ + public function set_first_name( $first_name ) { + $this->set_prop( 'first_name', $first_name ); + } + + /** + * Set customer's last name. + * + * @since 3.0.0 + * @param string $last_name Last name. + */ + public function set_last_name( $last_name ) { + $this->set_prop( 'last_name', $last_name ); + } + + /** + * Set customer's display name. + * + * @since 3.1.0 + * @param string $display_name Display name. + */ + public function set_display_name( $display_name ) { + /* translators: 1: first name 2: last name */ + $this->set_prop( 'display_name', is_email( $display_name ) ? sprintf( _x( '%1$s %2$s', 'display name', 'woocommerce' ), $this->get_first_name(), $this->get_last_name() ) : $display_name ); + } + + /** + * Set customer's user role(s). + * + * @since 3.0.0 + * @param mixed $role User role. + */ + public function set_role( $role ) { + global $wp_roles; + + if ( $role && ! empty( $wp_roles->roles ) && ! in_array( $role, array_keys( $wp_roles->roles ), true ) ) { + $this->error( 'customer_invalid_role', __( 'Invalid role', 'woocommerce' ) ); + } + $this->set_prop( 'role', $role ); + } + + /** + * Set the date this customer was last updated. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_date_created( $date = null ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set the date this customer was last updated. + * + * @since 3.0.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date. + */ + public function set_date_modified( $date = null ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set customer address to match shop base address. + * + * @since 3.0.0 + */ + public function set_billing_address_to_base() { + $base = wc_get_customer_default_location(); + $this->set_billing_location( $base['country'], $base['state'], '', '' ); + } + + /** + * Set customer shipping address to base address. + * + * @since 3.0.0 + */ + public function set_shipping_address_to_base() { + $base = wc_get_customer_default_location(); + $this->set_shipping_location( $base['country'], $base['state'], '', '' ); + } + + /** + * Sets all address info at once. + * + * @param string $country Country. + * @param string $state State. + * @param string $postcode Postcode. + * @param string $city City. + */ + public function set_billing_location( $country, $state = '', $postcode = '', $city = '' ) { + $address_data = $this->get_prop( 'billing', 'edit' ); + + $address_data['address_1'] = ''; + $address_data['address_2'] = ''; + $address_data['city'] = $city; + $address_data['state'] = $state; + $address_data['postcode'] = $postcode; + $address_data['country'] = $country; + + $this->set_prop( 'billing', $address_data ); + } + + /** + * Sets all shipping info at once. + * + * @param string $country Country. + * @param string $state State. + * @param string $postcode Postcode. + * @param string $city City. + */ + public function set_shipping_location( $country, $state = '', $postcode = '', $city = '' ) { + $address_data = $this->get_prop( 'shipping', 'edit' ); + + $address_data['address_1'] = ''; + $address_data['address_2'] = ''; + $address_data['city'] = $city; + $address_data['state'] = $state; + $address_data['postcode'] = $postcode; + $address_data['country'] = $country; + + $this->set_prop( 'shipping', $address_data ); + } + + /** + * Sets a prop for a setter method. + * + * @since 3.0.0 + * @param string $prop Name of prop to set. + * @param string $address Name of address to set. billing or shipping. + * @param mixed $value Value of the prop. + */ + protected function set_address_prop( $prop, $address, $value ) { + if ( array_key_exists( $prop, $this->data[ $address ] ) ) { + if ( true === $this->object_read ) { + if ( $value !== $this->data[ $address ][ $prop ] || ( isset( $this->changes[ $address ] ) && array_key_exists( $prop, $this->changes[ $address ] ) ) ) { + $this->changes[ $address ][ $prop ] = $value; + } + } else { + $this->data[ $address ][ $prop ] = $value; + } + } + } + + /** + * Set billing_first_name. + * + * @param string $value Billing first name. + */ + public function set_billing_first_name( $value ) { + $this->set_address_prop( 'first_name', 'billing', $value ); + } + + /** + * Set billing_last_name. + * + * @param string $value Billing last name. + */ + public function set_billing_last_name( $value ) { + $this->set_address_prop( 'last_name', 'billing', $value ); + } + + /** + * Set billing_company. + * + * @param string $value Billing company. + */ + public function set_billing_company( $value ) { + $this->set_address_prop( 'company', 'billing', $value ); + } + + /** + * Set billing_address_1. + * + * @param string $value Billing address line 1. + */ + public function set_billing_address( $value ) { + $this->set_billing_address_1( $value ); + } + + /** + * Set billing_address_1. + * + * @param string $value Billing address line 1. + */ + public function set_billing_address_1( $value ) { + $this->set_address_prop( 'address_1', 'billing', $value ); + } + + /** + * Set billing_address_2. + * + * @param string $value Billing address line 2. + */ + public function set_billing_address_2( $value ) { + $this->set_address_prop( 'address_2', 'billing', $value ); + } + + /** + * Set billing_city. + * + * @param string $value Billing city. + */ + public function set_billing_city( $value ) { + $this->set_address_prop( 'city', 'billing', $value ); + } + + /** + * Set billing_state. + * + * @param string $value Billing state. + */ + public function set_billing_state( $value ) { + $this->set_address_prop( 'state', 'billing', $value ); + } + + /** + * Set billing_postcode. + * + * @param string $value Billing postcode. + */ + public function set_billing_postcode( $value ) { + $this->set_address_prop( 'postcode', 'billing', $value ); + } + + /** + * Set billing_country. + * + * @param string $value Billing country. + */ + public function set_billing_country( $value ) { + $this->set_address_prop( 'country', 'billing', $value ); + } + + /** + * Set billing_email. + * + * @param string $value Billing email. + */ + public function set_billing_email( $value ) { + if ( $value && ! is_email( $value ) ) { + $this->error( 'customer_invalid_billing_email', __( 'Invalid billing email address', 'woocommerce' ) ); + } + $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); + } + + /** + * Set billing_phone. + * + * @param string $value Billing phone. + */ + public function set_billing_phone( $value ) { + $this->set_address_prop( 'phone', 'billing', $value ); + } + + /** + * Set shipping_first_name. + * + * @param string $value Shipping first name. + */ + public function set_shipping_first_name( $value ) { + $this->set_address_prop( 'first_name', 'shipping', $value ); + } + + /** + * Set shipping_last_name. + * + * @param string $value Shipping last name. + */ + public function set_shipping_last_name( $value ) { + $this->set_address_prop( 'last_name', 'shipping', $value ); + } + + /** + * Set shipping_company. + * + * @param string $value Shipping company. + */ + public function set_shipping_company( $value ) { + $this->set_address_prop( 'company', 'shipping', $value ); + } + + /** + * Set shipping_address_1. + * + * @param string $value Shipping address line 1. + */ + public function set_shipping_address( $value ) { + $this->set_shipping_address_1( $value ); + } + + /** + * Set shipping_address_1. + * + * @param string $value Shipping address line 1. + */ + public function set_shipping_address_1( $value ) { + $this->set_address_prop( 'address_1', 'shipping', $value ); + } + + /** + * Set shipping_address_2. + * + * @param string $value Shipping address line 2. + */ + public function set_shipping_address_2( $value ) { + $this->set_address_prop( 'address_2', 'shipping', $value ); + } + + /** + * Set shipping_city. + * + * @param string $value Shipping city. + */ + public function set_shipping_city( $value ) { + $this->set_address_prop( 'city', 'shipping', $value ); + } + + /** + * Set shipping_state. + * + * @param string $value Shipping state. + */ + public function set_shipping_state( $value ) { + $this->set_address_prop( 'state', 'shipping', $value ); + } + + /** + * Set shipping_postcode. + * + * @param string $value Shipping postcode. + */ + public function set_shipping_postcode( $value ) { + $this->set_address_prop( 'postcode', 'shipping', $value ); + } + + /** + * Set shipping_country. + * + * @param string $value Shipping country. + */ + public function set_shipping_country( $value ) { + $this->set_address_prop( 'country', 'shipping', $value ); + } + + /** + * Set shipping phone. + * + * @since 5.6.0 + * @param string $value Shipping phone. + */ + public function set_shipping_phone( $value ) { + $this->set_address_prop( 'phone', 'shipping', $value ); + } + + /** + * Set if the user a paying customer. + * + * @since 3.0.0 + * @param bool $is_paying_customer If is a paying customer. + */ + public function set_is_paying_customer( $is_paying_customer ) { + $this->set_prop( 'is_paying_customer', (bool) $is_paying_customer ); + } +} diff --git a/includes/class-wc-data-exception.php b/includes/class-wc-data-exception.php new file mode 100644 index 0000000..0b29c49 --- /dev/null +++ b/includes/class-wc-data-exception.php @@ -0,0 +1,64 @@ +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 0000000..f6723b1 --- /dev/null +++ b/includes/class-wc-data-store.php @@ -0,0 +1,210 @@ + 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 available, if a store is requested like + * this and doesn't exist, then the store would fall back to 'product'. + * Ran through `woocommerce_data_stores`. + * + * @var array + */ + private $stores = array( + 'coupon' => 'WC_Coupon_Data_Store_CPT', + 'customer' => 'WC_Customer_Data_Store', + 'customer-download' => 'WC_Customer_Download_Data_Store', + 'customer-download-log' => 'WC_Customer_Download_Log_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', + 'webhook' => 'WC_Webhook_Data_Store', + ); + + /** + * Contains the name of the current data store's class name. + * + * @var string + */ + 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. + * + * @throws Exception When validation fails. + * @param string $object_type Name of object. + */ + 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. + * + * @throws Exception When validation fails. + */ + public function __wakeup() { + $this->__construct( $this->object_type ); + } + + /** + * Loads a data store. + * + * @param string $object_type Name of object. + * + * @since 3.0.0 + * @throws Exception When validation fails. + * @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 $data WooCommerce data instance. + */ + public function read( &$data ) { + $this->instance->read( $data ); + } + + /** + * Create an object in the data store. + * + * @since 3.0.0 + * @param WC_Data $data WooCommerce data instance. + */ + public function create( &$data ) { + $this->instance->create( $data ); + } + + /** + * Update an object in the data store. + * + * @since 3.0.0 + * @param WC_Data $data WooCommerce data instance. + */ + public function update( &$data ) { + $this->instance->update( $data ); + } + + /** + * Delete an object from the data store. + * + * @since 3.0.0 + * @param WC_Data $data WooCommerce data instance. + * @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 string $method Method. + * @param mixed $parameters Parameters. + * @return mixed + */ + public function __call( $method, $parameters ) { + if ( is_callable( array( $this->instance, $method ) ) ) { + $object = array_shift( $parameters ); + $parameters = array_merge( array( &$object ), $parameters ); + return $this->instance->$method( ...$parameters ); + } + } +} diff --git a/includes/class-wc-datetime.php b/includes/class-wc-datetime.php new file mode 100644 index 0000000..1778503 --- /dev/null +++ b/includes/class-wc-datetime.php @@ -0,0 +1,103 @@ +format( DATE_ATOM ); + } + + /** + * Set UTC offset - this is a fixed offset instead of a timezone. + * + * @param int $offset Offset. + */ + public function set_utc_offset( $offset ) { + $this->utc_offset = intval( $offset ); + } + + /** + * Get UTC offset if set, or default to the DateTime object's offset. + */ + public function getOffset() { + return $this->utc_offset ? $this->utc_offset : parent::getOffset(); + } + + /** + * Set timezone. + * + * @param DateTimeZone $timezone DateTimeZone instance. + * @return DateTime + */ + public function setTimezone( $timezone ) { + $this->utc_offset = 0; + return parent::setTimezone( $timezone ); + } + + /** + * Missing in PHP 5.2 so just here so it can be supported consistently. + * + * @since 3.0.0 + * @return int + */ + public function getTimestamp() { + return method_exists( 'DateTime', 'getTimestamp' ) ? parent::getTimestamp() : $this->format( 'U' ); + } + + /** + * Get the timestamp with the WordPress timezone offset added or subtracted. + * + * @since 3.0.0 + * @return int + */ + public function getOffsetTimestamp() { + return $this->getTimestamp() + $this->getOffset(); + } + + /** + * Format a date based on the offset timestamp. + * + * @since 3.0.0 + * @param string $format Date format. + * @return string + */ + public function date( $format ) { + return gmdate( $format, $this->getOffsetTimestamp() ); + } + + /** + * Return a localised date based on offset timestamp. Wrapper for date_i18n function. + * + * @since 3.0.0 + * @param string $format Date format. + * @return string + */ + public function date_i18n( $format = 'Y-m-d' ) { + return date_i18n( $format, $this->getOffsetTimestamp() ); + } +} diff --git a/includes/class-wc-deprecated-action-hooks.php b/includes/class-wc-deprecated-action-hooks.php new file mode 100644 index 0000000..39bc932 --- /dev/null +++ b/includes/class-wc-deprecated-action-hooks.php @@ -0,0 +1,191 @@ + 'old'. + * + * @var array + */ + protected $deprecated_hooks = array( + 'woocommerce_new_order_item' => 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', + 'woocommerce_order_details_after_order_table_items' => 'woocommerce_order_items_table', + + 'woocommerce_settings_advanced_page_options' => array( + 'woocommerce_settings_checkout_page_options', + 'woocommerce_settings_account_page_options', + ), + 'woocommerce_settings_advanced_page_options_end' => array( + 'woocommerce_settings_checkout_page_options_end', + 'woocommerce_settings_account_page_options_end', + ), + 'woocommerce_settings_advanced_page_options_after' => array( + 'woocommerce_settings_checkout_page_options_after', + 'woocommerce_settings_account_page_options_after', + ), + ); + + /** + * Array of versions on each hook has been deprecated. + * + * @var array + */ + protected $deprecated_version = array( + 'woocommerce_order_add_shipping' => '3.0.0', + 'woocommerce_order_add_coupon' => '3.0.0', + 'woocommerce_order_add_tax' => '3.0.0', + 'woocommerce_order_add_fee' => '3.0.0', + 'woocommerce_add_shipping_order_item' => '3.0.0', + 'woocommerce_add_order_item_meta' => '3.0.0', + 'woocommerce_add_order_fee_meta' => '3.0.0', + 'woocommerce_order_edit_product' => '3.0.0', + 'woocommerce_order_update_coupon' => '3.0.0', + 'woocommerce_order_update_shipping' => '3.0.0', + 'woocommerce_order_update_fee' => '3.0.0', + 'woocommerce_order_update_tax' => '3.0.0', + 'woocommerce_payment_token_created' => '3.0.0', + 'woocommerce_create_product_variation' => '3.0.0', + 'woocommerce_order_items_table' => '3.0.0', + 'woocommerce_settings_checkout_page_options' => '3.4.0', + 'woocommerce_settings_account_page_options' => '3.4.0', + 'woocommerce_settings_checkout_page_options_end' => '3.4.0', + 'woocommerce_settings_account_page_options_end' => '3.4.0', + 'woocommerce_settings_checkout_page_options_after' => '3.4.0', + 'woocommerce_settings_account_page_options_after' => '3.4.0', + ); + + /** + * Hook into the new hook so we can handle deprecated hooks once fired. + * + * @param string $hook_name 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 New hook name. + * @param string $old_hook Old hook name. + * @param array $new_callback_args New callback args. + * @param mixed $return_value Returned 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 Old hook name. + * @param array $new_callback_args 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 0000000..ddfcb75 --- /dev/null +++ b/includes/class-wc-deprecated-filter-hooks.php @@ -0,0 +1,144 @@ + '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', + 'woocommerce_settings_tabs_advanced' => 'woocommerce_settings_tabs_api', + 'woocommerce_settings_advanced' => 'woocommerce_settings_api', + ); + + /** + * Array of versions on each hook has been deprecated. + * + * @var array + */ + protected $deprecated_version = array( + 'woocommerce_email_order_schema_markup' => '3.0.0', + 'add_to_cart_fragments' => '3.0.0', + 'add_to_cart_redirect' => '3.0.0', + 'woocommerce_product_width' => '3.0.0', + 'woocommerce_product_height' => '3.0.0', + 'woocommerce_product_length' => '3.0.0', + 'woocommerce_product_weight' => '3.0.0', + 'woocommerce_get_sku' => '3.0.0', + 'woocommerce_get_price' => '3.0.0', + 'woocommerce_get_regular_price' => '3.0.0', + 'woocommerce_get_sale_price' => '3.0.0', + 'woocommerce_product_tax_class' => '3.0.0', + 'woocommerce_get_stock_quantity' => '3.0.0', + 'woocommerce_get_product_attributes' => '3.0.0', + 'woocommerce_product_gallery_attachment_ids' => '3.0.0', + 'woocommerce_product_review_count' => '3.0.0', + 'woocommerce_product_files' => '3.0.0', + 'woocommerce_get_currency' => '3.0.0', + 'woocommerce_order_amount_discount_total' => '3.0.0', + 'woocommerce_order_amount_discount_tax' => '3.0.0', + 'woocommerce_order_amount_shipping_total' => '3.0.0', + 'woocommerce_order_amount_shipping_tax' => '3.0.0', + 'woocommerce_order_amount_cart_tax' => '3.0.0', + 'woocommerce_order_amount_total' => '3.0.0', + 'woocommerce_order_amount_total_tax' => '3.0.0', + 'woocommerce_order_amount_total_discount' => '3.0.0', + 'woocommerce_order_amount_subtotal' => '3.0.0', + 'woocommerce_order_tax_totals' => '3.0.0', + 'woocommerce_refund_amount' => '3.0.0', + 'woocommerce_refund_reason' => '3.0.0', + 'default_checkout_country' => '3.0.0', + 'default_checkout_state' => '3.0.0', + 'default_checkout_postcode' => '3.0.0', + 'woocommerce_debug_posting' => '3.0.0', + 'wocommerce_credit_card_type_labels' => '3.0.0', + 'woocommerce_settings_tabs_api' => '3.4.0', + 'woocommerce_settings_api' => '3.4.0', + ); + + /** + * Hook into the new hook so we can handle deprecated hooks once fired. + * + * @param string $hook_name 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 New hook name. + * @param string $old_hook Old hook name. + * @param array $new_callback_args New callback args. + * @param mixed $return_value Returned 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 Old hook name. + * @param array $new_callback_args 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-discounts.php b/includes/class-wc-discounts.php new file mode 100644 index 0000000..8b2c5b4 --- /dev/null +++ b/includes/class-wc-discounts.php @@ -0,0 +1,1021 @@ + Item Key => Value + */ + protected $discounts = array(); + + /** + * WC_Discounts Constructor. + * + * @param WC_Cart|WC_Order $object Cart or order object. + */ + public function __construct( $object = null ) { + if ( is_a( $object, 'WC_Cart' ) ) { + $this->set_items_from_cart( $object ); + } elseif ( is_a( $object, 'WC_Order' ) ) { + $this->set_items_from_order( $object ); + } + } + + /** + * Set items directly. Used by WC_Cart_Totals. + * + * @since 3.2.3 + * @param array $items Items to set. + */ + public function set_items( $items ) { + $this->items = $items; + $this->discounts = array(); + uasort( $this->items, array( $this, 'sort_by_price' ) ); + } + + /** + * Normalise cart items which will be discounted. + * + * @since 3.2.0 + * @param WC_Cart $cart Cart object. + */ + public function set_items_from_cart( $cart ) { + $this->items = array(); + $this->discounts = array(); + + if ( ! is_a( $cart, 'WC_Cart' ) ) { + return; + } + + $this->object = $cart; + + foreach ( $cart->get_cart() as $key => $cart_item ) { + $item = new stdClass(); + $item->key = $key; + $item->object = $cart_item; + $item->product = $cart_item['data']; + $item->quantity = $cart_item['quantity']; + $item->price = wc_add_number_precision_deep( (float) $item->product->get_price() * (float) $item->quantity ); + $this->items[ $key ] = $item; + } + + uasort( $this->items, array( $this, 'sort_by_price' ) ); + } + + /** + * Normalise order items which will be discounted. + * + * @since 3.2.0 + * @param WC_Order $order Order object. + */ + public function set_items_from_order( $order ) { + $this->items = array(); + $this->discounts = array(); + + if ( ! is_a( $order, 'WC_Order' ) ) { + return; + } + + $this->object = $order; + + foreach ( $order->get_items() as $order_item ) { + $item = new stdClass(); + $item->key = $order_item->get_id(); + $item->object = $order_item; + $item->product = $order_item->get_product(); + $item->quantity = $order_item->get_quantity(); + $item->price = wc_add_number_precision_deep( $order_item->get_subtotal() ); + + if ( $order->get_prices_include_tax() ) { + $item->price += wc_add_number_precision_deep( $order_item->get_subtotal_tax() ); + } + + $this->items[ $order_item->get_id() ] = $item; + } + + uasort( $this->items, array( $this, 'sort_by_price' ) ); + } + + /** + * Get the object concerned. + * + * @since 3.3.2 + * @return object + */ + public function get_object() { + return $this->object; + } + + /** + * Get items. + * + * @since 3.2.0 + * @return object[] + */ + public function get_items() { + return $this->items; + } + + /** + * Get items to validate. + * + * @since 3.3.2 + * @return object[] + */ + public function get_items_to_validate() { + return apply_filters( 'woocommerce_coupon_get_items_to_validate', $this->get_items(), $this ); + } + + /** + * Get discount by key with or without precision. + * + * @since 3.2.0 + * @param string $key name of discount row to return. + * @param bool $in_cents Should the totals be returned in cents, or without precision. + * @return float + */ + public function get_discount( $key, $in_cents = false ) { + $item_discount_totals = $this->get_discounts_by_item( $in_cents ); + return isset( $item_discount_totals[ $key ] ) ? $item_discount_totals[ $key ] : 0; + } + + /** + * Get all discount totals. + * + * @since 3.2.0 + * @param bool $in_cents Should the totals be returned in cents, or without precision. + * @return array + */ + public function get_discounts( $in_cents = false ) { + $discounts = $this->discounts; + return $in_cents ? $discounts : wc_remove_number_precision_deep( $discounts ); + } + + /** + * Get all discount totals per item. + * + * @since 3.2.0 + * @param bool $in_cents Should the totals be returned in cents, or without precision. + * @return array + */ + public function get_discounts_by_item( $in_cents = false ) { + $discounts = $this->discounts; + $item_discount_totals = (array) array_shift( $discounts ); + + foreach ( $discounts as $item_discounts ) { + foreach ( $item_discounts as $item_key => $item_discount ) { + $item_discount_totals[ $item_key ] += $item_discount; + } + } + + return $in_cents ? $item_discount_totals : wc_remove_number_precision_deep( $item_discount_totals ); + } + + /** + * Get all discount totals per coupon. + * + * @since 3.2.0 + * @param bool $in_cents Should the totals be returned in cents, or without precision. + * @return array + */ + public function get_discounts_by_coupon( $in_cents = false ) { + $coupon_discount_totals = array_map( 'array_sum', $this->discounts ); + + return $in_cents ? $coupon_discount_totals : wc_remove_number_precision_deep( $coupon_discount_totals ); + } + + /** + * Get discounted price of an item without precision. + * + * @since 3.2.0 + * @param object $item Get data for this item. + * @return float + */ + public function get_discounted_price( $item ) { + return wc_remove_number_precision_deep( $this->get_discounted_price_in_cents( $item ) ); + } + + /** + * Get discounted price of an item to precision (in cents). + * + * @since 3.2.0 + * @param object $item Get data for this item. + * @return int + */ + public function get_discounted_price_in_cents( $item ) { + return absint( NumberUtil::round( $item->price - $this->get_discount( $item->key, true ) ) ); + } + + /** + * Apply a discount to all items using a coupon. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object being applied to the items. + * @param bool $validate Set to false to skip coupon validation. + * @throws Exception Error message when coupon isn't valid. + * @return bool|WP_Error True if applied or WP_Error instance in failure. + */ + public function apply_coupon( $coupon, $validate = true ) { + if ( ! is_a( $coupon, 'WC_Coupon' ) ) { + return new WP_Error( 'invalid_coupon', __( 'Invalid coupon', 'woocommerce' ) ); + } + + $is_coupon_valid = $validate ? $this->is_coupon_valid( $coupon ) : true; + + if ( is_wp_error( $is_coupon_valid ) ) { + return $is_coupon_valid; + } + + if ( ! isset( $this->discounts[ $coupon->get_code() ] ) ) { + $this->discounts[ $coupon->get_code() ] = array_fill_keys( array_keys( $this->items ), 0 ); + } + + $items_to_apply = $this->get_items_to_apply_coupon( $coupon ); + + // Core discounts are handled here as of 3.2. + switch ( $coupon->get_discount_type() ) { + case 'percent': + $this->apply_coupon_percent( $coupon, $items_to_apply ); + break; + case 'fixed_product': + $this->apply_coupon_fixed_product( $coupon, $items_to_apply ); + break; + case 'fixed_cart': + $this->apply_coupon_fixed_cart( $coupon, $items_to_apply ); + break; + default: + $this->apply_coupon_custom( $coupon, $items_to_apply ); + break; + } + + return true; + } + + /** + * Sort by price. + * + * @since 3.2.0 + * @param array $a First element. + * @param array $b Second element. + * @return int + */ + protected function sort_by_price( $a, $b ) { + $price_1 = $a->price * $a->quantity; + $price_2 = $b->price * $b->quantity; + if ( $price_1 === $price_2 ) { + return 0; + } + return ( $price_1 < $price_2 ) ? 1 : -1; + } + + /** + * Filter out all products which have been fully discounted to 0. + * Used as array_filter callback. + * + * @since 3.2.0 + * @param object $item Get data for this item. + * @return bool + */ + protected function filter_products_with_price( $item ) { + return $this->get_discounted_price_in_cents( $item ) > 0; + } + + /** + * Get items which the coupon should be applied to. + * + * @since 3.2.0 + * @param object $coupon Coupon object. + * @return array + */ + protected function get_items_to_apply_coupon( $coupon ) { + $items_to_apply = array(); + + foreach ( $this->get_items_to_validate() as $item ) { + $item_to_apply = clone $item; // Clone the item so changes to this item do not affect the originals. + + if ( 0 === $this->get_discounted_price_in_cents( $item_to_apply ) || 0 >= $item_to_apply->quantity ) { + continue; + } + + if ( ! $coupon->is_valid_for_product( $item_to_apply->product, $item_to_apply->object ) && ! $coupon->is_valid_for_cart() ) { + continue; + } + + $items_to_apply[] = $item_to_apply; + } + return $items_to_apply; + } + + /** + * Apply percent discount to items and return an array of discounts granted. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @return int Total discounted. + */ + protected function apply_coupon_percent( $coupon, $items_to_apply ) { + $total_discount = 0; + $cart_total = 0; + $limit_usage_qty = 0; + $applied_count = 0; + $adjust_final_discount = true; + + if ( null !== $coupon->get_limit_usage_to_x_items() ) { + $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); + } + + $coupon_amount = $coupon->get_amount(); + + foreach ( $items_to_apply as $item ) { + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); + + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : NumberUtil::round( $item->price ); + + // See how many and what price to apply to. + $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; + $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); + $price_to_discount = ( $price_to_discount / $item->quantity ) * $apply_quantity; + + // Run coupon calculations. + $discount = floor( $price_to_discount * ( $coupon_amount / 100 ) ); + + if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { + // Send through the legacy filter, but not as cents. + $filtered_discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); + + if ( $filtered_discount !== $discount ) { + $discount = $filtered_discount; + $adjust_final_discount = false; + } + } + + $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); + $cart_total = $cart_total + $price_to_discount; + $total_discount = $total_discount + $discount; + $applied_count = $applied_count + $apply_quantity; + + // Store code and discount amount per item. + $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; + } + + // Work out how much discount would have been given to the cart as a whole and compare to what was discounted on all line items. + $cart_total_discount = wc_round_discount( $cart_total * ( $coupon_amount / 100 ), 0 ); + + if ( $total_discount < $cart_total_discount && $adjust_final_discount ) { + $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $cart_total_discount - $total_discount ); + } + + return $total_discount; + } + + /** + * Apply fixed product discount to items. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. + * @return int Total discounted. + */ + protected function apply_coupon_fixed_product( $coupon, $items_to_apply, $amount = null ) { + $total_discount = 0; + $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); + $limit_usage_qty = 0; + $applied_count = 0; + + if ( null !== $coupon->get_limit_usage_to_x_items() ) { + $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); + } + + foreach ( $items_to_apply as $item ) { + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); + + // Get the price we actually want to discount, based on settings. + $price_to_discount = ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price; + + // Run coupon calculations. + if ( $limit_usage_qty ) { + $apply_quantity = $limit_usage_qty - $applied_count < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; + $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); + $discount = min( $amount, $item->price / $item->quantity ) * $apply_quantity; + } else { + $apply_quantity = apply_filters( 'woocommerce_coupon_get_apply_quantity', $item->quantity, $item, $coupon, $this ); + $discount = $amount * $apply_quantity; + } + + if ( is_a( $this->object, 'WC_Cart' ) && has_filter( 'woocommerce_coupon_get_discount_amount' ) ) { + // Send through the legacy filter, but not as cents. + $discount = wc_add_number_precision( apply_filters( 'woocommerce_coupon_get_discount_amount', wc_remove_number_precision( $discount ), wc_remove_number_precision( $price_to_discount ), $item->object, false, $coupon ) ); + } + + $discount = min( $discounted_price, $discount ); + $total_discount = $total_discount + $discount; + $applied_count = $applied_count + $apply_quantity; + + // Store code and discount amount per item. + $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; + } + return $total_discount; + } + + /** + * Apply fixed cart discount to items. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $amount Fixed discount amount to apply in cents. Leave blank to pull from coupon. + * @return int Total discounted. + */ + protected function apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount = null ) { + $total_discount = 0; + $amount = $amount ? $amount : wc_add_number_precision( $coupon->get_amount() ); + $items_to_apply = array_filter( $items_to_apply, array( $this, 'filter_products_with_price' ) ); + $item_count = array_sum( wp_list_pluck( $items_to_apply, 'quantity' ) ); + + if ( ! $item_count ) { + return $total_discount; + } + + if ( ! $amount ) { + // If there is no amount we still send it through so filters are fired. + $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, 0 ); + } else { + $per_item_discount = absint( $amount / $item_count ); // round it down to the nearest cent. + + if ( $per_item_discount > 0 ) { + $total_discount = $this->apply_coupon_fixed_product( $coupon, $items_to_apply, $per_item_discount ); + + /** + * If there is still discount remaining, repeat the process. + */ + if ( $total_discount > 0 && $total_discount < $amount ) { + $total_discount += $this->apply_coupon_fixed_cart( $coupon, $items_to_apply, $amount - $total_discount ); + } + } elseif ( $amount > 0 ) { + $total_discount += $this->apply_coupon_remainder( $coupon, $items_to_apply, $amount ); + } + } + return $total_discount; + } + + /** + * Apply custom coupon discount to items. + * + * @since 3.3 + * @param WC_Coupon $coupon Coupon object. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @return int Total discounted. + */ + protected function apply_coupon_custom( $coupon, $items_to_apply ) { + $limit_usage_qty = 0; + $applied_count = 0; + + if ( null !== $coupon->get_limit_usage_to_x_items() ) { + $limit_usage_qty = $coupon->get_limit_usage_to_x_items(); + } + + // Apply the coupon to each item. + foreach ( $items_to_apply as $item ) { + // Find out how much price is available to discount for the item. + $discounted_price = $this->get_discounted_price_in_cents( $item ); + + // Get the price we actually want to discount, based on settings. + $price_to_discount = wc_remove_number_precision( ( 'yes' === get_option( 'woocommerce_calc_discounts_sequentially', 'no' ) ) ? $discounted_price : $item->price ); + + // See how many and what price to apply to. + $apply_quantity = $limit_usage_qty && ( $limit_usage_qty - $applied_count ) < $item->quantity ? $limit_usage_qty - $applied_count : $item->quantity; + $apply_quantity = max( 0, apply_filters( 'woocommerce_coupon_get_apply_quantity', $apply_quantity, $item, $coupon, $this ) ); + + // Run coupon calculations. + $discount = wc_add_number_precision( $coupon->get_discount_amount( $price_to_discount / $item->quantity, $item->object, true ) ) * $apply_quantity; + $discount = wc_round_discount( min( $discounted_price, $discount ), 0 ); + $applied_count = $applied_count + $apply_quantity; + + // Store code and discount amount per item. + $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; + } + + // Allow post-processing for custom coupon types (e.g. calculating discrepancy, etc). + $this->discounts[ $coupon->get_code() ] = apply_filters( 'woocommerce_coupon_custom_discounts_array', $this->discounts[ $coupon->get_code() ], $coupon ); + + return array_sum( $this->discounts[ $coupon->get_code() ] ); + } + + /** + * Deal with remaining fractional discounts by splitting it over items + * until the amount is expired, discounting 1 cent at a time. + * + * @since 3.2.0 + * @param WC_Coupon $coupon Coupon object if appliable. Passed through filters. + * @param array $items_to_apply Array of items to apply the coupon to. + * @param int $amount Fixed discount amount to apply. + * @return int Total discounted. + */ + protected function apply_coupon_remainder( $coupon, $items_to_apply, $amount ) { + $total_discount = 0; + + foreach ( $items_to_apply as $item ) { + for ( $i = 0; $i < $item->quantity; $i ++ ) { + // Find out how much price is available to discount for the item. + $price_to_discount = $this->get_discounted_price_in_cents( $item ); + + // Run coupon calculations. + $discount = min( $price_to_discount, 1 ); + + // Store totals. + $total_discount += $discount; + + // Store code and discount amount per item. + $this->discounts[ $coupon->get_code() ][ $item->key ] += $discount; + + if ( $total_discount >= $amount ) { + break 2; + } + } + if ( $total_discount >= $amount ) { + break; + } + } + return $total_discount; + } + + /** + * Ensure coupon exists or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_exists( $coupon ) { + if ( ! $coupon->get_id() && ! $coupon->get_virtual() ) { + /* translators: %s: coupon code */ + throw new Exception( sprintf( __( 'Coupon "%s" does not exist!', 'woocommerce' ), esc_html( $coupon->get_code() ) ), 105 ); + } + + return true; + } + + /** + * Ensure coupon usage limit is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_usage_limit( $coupon ) { + if ( ! $coupon->get_usage_limit() ) { + return true; + } + $usage_count = $coupon->get_usage_count(); + $data_store = $coupon->get_data_store(); + $tentative_usage_count = is_callable( array( $data_store, 'get_tentative_usage_count' ) ) ? $data_store->get_tentative_usage_count( $coupon->get_id() ) : 0; + if ( $usage_count + $tentative_usage_count < $coupon->get_usage_limit() ) { + // All good. + return true; + } + // Coupon usage limit is reached. Let's show as informative error message as we can. + if ( 0 === $tentative_usage_count ) { + // No held coupon, usage limit is indeed reached. + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; + } elseif ( is_user_logged_in() ) { + $recent_pending_orders = wc_get_orders( + array( + 'limit' => 1, + 'post_status' => array( 'wc-failed', 'wc-pending' ), + 'customer' => get_current_user_id(), + 'return' => 'ids', + ) + ); + if ( count( $recent_pending_orders ) > 0 ) { + // User logged in and have a pending order, maybe they are trying to use the coupon. + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; + } else { + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; + } + } else { + // Maybe this user was trying to use the coupon but got stuck. We can't know for sure (performantly). Show a slightly better error message. + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK_GUEST; + } + throw new Exception( $coupon->get_coupon_error( $error_code ), $error_code ); + } + + /** + * Ensure coupon user usage limit is valid or throw exception. + * + * Per user usage limit - check here if user is logged in (against user IDs). + * Checked again for emails later on in WC_Cart::check_customer_coupons(). + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @param int $user_id User ID. + * @return bool + */ + protected function validate_coupon_user_usage_limit( $coupon, $user_id = 0 ) { + if ( empty( $user_id ) ) { + if ( $this->object instanceof WC_Order ) { + $user_id = $this->object->get_customer_id(); + } else { + $user_id = get_current_user_id(); + } + } + + if ( $coupon && $user_id && apply_filters( 'woocommerce_coupon_validate_user_usage_limit', $coupon->get_usage_limit_per_user() > 0, $user_id, $coupon, $this ) && $coupon->get_id() && $coupon->get_data_store() ) { + $data_store = $coupon->get_data_store(); + $usage_count = $data_store->get_usage_by_user_id( $coupon, $user_id ); + if ( $usage_count >= $coupon->get_usage_limit_per_user() ) { + if ( $data_store->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ) > 0 ) { + $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK ); + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_COUPON_STUCK; + } else { + $error_message = $coupon->get_coupon_error( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ); + $error_code = WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED; + } + throw new Exception( $error_message, $error_code ); + } + } + + return true; + } + + /** + * Ensure coupon date is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_expiry_date( $coupon ) { + if ( $coupon->get_date_expires() && apply_filters( 'woocommerce_coupon_validate_expiry_date', time() > $coupon->get_date_expires()->getTimestamp(), $coupon, $this ) ) { + throw new Exception( __( 'This coupon has expired.', 'woocommerce' ), 107 ); + } + + return true; + } + + /** + * Ensure coupon amount is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_minimum_amount( $coupon ) { + $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); + + if ( $coupon->get_minimum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_minimum_amount', $coupon->get_minimum_amount() > $subtotal, $coupon, $subtotal ) ) { + /* translators: %s: coupon minimum amount */ + throw new Exception( sprintf( __( 'The minimum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_minimum_amount() ) ), 108 ); + } + + return true; + } + + /** + * Ensure coupon amount is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_maximum_amount( $coupon ) { + $subtotal = wc_remove_number_precision( $this->get_object_subtotal() ); + + if ( $coupon->get_maximum_amount() > 0 && apply_filters( 'woocommerce_coupon_validate_maximum_amount', $coupon->get_maximum_amount() < $subtotal, $coupon ) ) { + /* translators: %s: coupon maximum amount */ + throw new Exception( sprintf( __( 'The maximum spend for this coupon is %s.', 'woocommerce' ), wc_price( $coupon->get_maximum_amount() ) ), 112 ); + } + + return true; + } + + /** + * Ensure coupon is valid for products in the list is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_product_ids( $coupon ) { + if ( count( $coupon->get_product_ids() ) > 0 ) { + $valid = false; + + foreach ( $this->get_items_to_validate() as $item ) { + if ( $item->product && in_array( $item->product->get_id(), $coupon->get_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_product_ids(), true ) ) { + $valid = true; + break; + } + } + + if ( ! $valid ) { + throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); + } + } + + return true; + } + + /** + * Ensure coupon is valid for product categories in the list is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_product_categories( $coupon ) { + if ( count( $coupon->get_product_categories() ) > 0 ) { + $valid = false; + + foreach ( $this->get_items_to_validate() as $item ) { + if ( $coupon->get_exclude_sale_items() && $item->product && $item->product->is_on_sale() ) { + continue; + } + + $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); + + if ( $item->product->get_parent_id() ) { + $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); + } + + // If we find an item with a cat in our allowed cat list, the coupon is valid. + if ( count( array_intersect( $product_cats, $coupon->get_product_categories() ) ) > 0 ) { + $valid = true; + break; + } + } + + if ( ! $valid ) { + throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); + } + } + + return true; + } + + /** + * Ensure coupon is valid for sale items in the list is valid or throw exception. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_sale_items( $coupon ) { + if ( $coupon->get_exclude_sale_items() ) { + $valid = true; + + foreach ( $this->get_items_to_validate() as $item ) { + if ( $item->product && $item->product->is_on_sale() ) { + $valid = false; + break; + } + } + + if ( ! $valid ) { + throw new Exception( __( 'Sorry, this coupon is not valid for sale items.', 'woocommerce' ), 110 ); + } + } + + return true; + } + + /** + * All exclusion rules must pass at the same time for a product coupon to be valid. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_excluded_items( $coupon ) { + $items = $this->get_items_to_validate(); + if ( ! empty( $items ) && $coupon->is_type( wc_get_product_coupon_types() ) ) { + $valid = false; + + foreach ( $items as $item ) { + if ( $item->product && $coupon->is_valid_for_product( $item->product, $item->object ) ) { + $valid = true; + break; + } + } + + if ( ! $valid ) { + throw new Exception( __( 'Sorry, this coupon is not applicable to selected products.', 'woocommerce' ), 109 ); + } + } + + return true; + } + + /** + * Cart discounts cannot be added if non-eligible product is found. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_eligible_items( $coupon ) { + if ( ! $coupon->is_type( wc_get_product_coupon_types() ) ) { + $this->validate_coupon_sale_items( $coupon ); + $this->validate_coupon_excluded_product_ids( $coupon ); + $this->validate_coupon_excluded_product_categories( $coupon ); + } + + return true; + } + + /** + * Exclude products. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_excluded_product_ids( $coupon ) { + // Exclude Products. + if ( count( $coupon->get_excluded_product_ids() ) > 0 ) { + $products = array(); + + foreach ( $this->get_items_to_validate() as $item ) { + if ( $item->product && in_array( $item->product->get_id(), $coupon->get_excluded_product_ids(), true ) || in_array( $item->product->get_parent_id(), $coupon->get_excluded_product_ids(), true ) ) { + $products[] = $item->product->get_name(); + } + } + + if ( ! empty( $products ) ) { + /* translators: %s: products list */ + throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the products: %s.', 'woocommerce' ), implode( ', ', $products ) ), 113 ); + } + } + + return true; + } + + /** + * Exclude categories from product list. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool + */ + protected function validate_coupon_excluded_product_categories( $coupon ) { + if ( count( $coupon->get_excluded_product_categories() ) > 0 ) { + $categories = array(); + + foreach ( $this->get_items_to_validate() as $item ) { + if ( ! $item->product ) { + continue; + } + + $product_cats = wc_get_product_cat_ids( $item->product->get_id() ); + + if ( $item->product->get_parent_id() ) { + $product_cats = array_merge( $product_cats, wc_get_product_cat_ids( $item->product->get_parent_id() ) ); + } + + $cat_id_list = array_intersect( $product_cats, $coupon->get_excluded_product_categories() ); + if ( count( $cat_id_list ) > 0 ) { + foreach ( $cat_id_list as $cat_id ) { + $cat = get_term( $cat_id, 'product_cat' ); + $categories[] = $cat->name; + } + } + } + + if ( ! empty( $categories ) ) { + /* translators: %s: categories list */ + throw new Exception( sprintf( __( 'Sorry, this coupon is not applicable to the categories: %s.', 'woocommerce' ), implode( ', ', array_unique( $categories ) ) ), 114 ); + } + } + + return true; + } + + /** + * Get the object subtotal + * + * @return int + */ + protected function get_object_subtotal() { + if ( is_a( $this->object, 'WC_Cart' ) ) { + return wc_add_number_precision( $this->object->get_displayed_subtotal() ); + } elseif ( is_a( $this->object, 'WC_Order' ) ) { + $subtotal = wc_add_number_precision( $this->object->get_subtotal() ); + + if ( $this->object->get_prices_include_tax() ) { + // Add tax to tax-exclusive subtotal. + $subtotal = $subtotal + wc_add_number_precision( NumberUtil::round( $this->object->get_total_tax(), wc_get_price_decimals() ) ); + } + + return $subtotal; + } else { + return array_sum( wp_list_pluck( $this->items, 'price' ) ); + } + } + + /** + * Check if a coupon is valid. + * + * Error Codes: + * - 100: Invalid filtered. + * - 101: Invalid removed. + * - 102: Not yours removed. + * - 103: Already applied. + * - 104: Individual use only. + * - 105: Not exists. + * - 106: Usage limit reached. + * - 107: Expired. + * - 108: Minimum spend limit not met. + * - 109: Not applicable. + * - 110: Not valid for sale items. + * - 111: Missing coupon code. + * - 112: Maximum spend limit met. + * - 113: Excluded products. + * - 114: Excluded categories. + * + * @since 3.2.0 + * @throws Exception Error message. + * @param WC_Coupon $coupon Coupon data. + * @return bool|WP_Error + */ + public function is_coupon_valid( $coupon ) { + try { + $this->validate_coupon_exists( $coupon ); + $this->validate_coupon_usage_limit( $coupon ); + $this->validate_coupon_user_usage_limit( $coupon ); + $this->validate_coupon_expiry_date( $coupon ); + $this->validate_coupon_minimum_amount( $coupon ); + $this->validate_coupon_maximum_amount( $coupon ); + $this->validate_coupon_product_ids( $coupon ); + $this->validate_coupon_product_categories( $coupon ); + $this->validate_coupon_excluded_items( $coupon ); + $this->validate_coupon_eligible_items( $coupon ); + + if ( ! apply_filters( 'woocommerce_coupon_is_valid', true, $coupon, $this ) ) { + throw new Exception( __( 'Coupon is not valid.', 'woocommerce' ), 100 ); + } + } catch ( Exception $e ) { + /** + * Filter the coupon error message. + * + * @param string $error_message Error message. + * @param int $error_code Error code. + * @param WC_Coupon $coupon Coupon data. + */ + $message = apply_filters( 'woocommerce_coupon_error', is_numeric( $e->getMessage() ) ? $coupon->get_coupon_error( $e->getMessage() ) : $e->getMessage(), $e->getCode(), $coupon ); + + return new WP_Error( + 'invalid_coupon', + $message, + array( + 'status' => 400, + ) + ); + } + return true; + } +} diff --git a/includes/class-wc-download-handler.php b/includes/class-wc-download-handler.php new file mode 100644 index 0000000..c5241e1 --- /dev/null +++ b/includes/class-wc-download-handler.php @@ -0,0 +1,654 @@ +get_billing_email() : null; + + // Prepare email address hash. + $email_hash = function_exists( 'hash' ) ? hash( 'sha256', $email_address ) : sha1( $email_address ); + + if ( is_null( $email_address ) || ! hash_equals( wp_unslash( $_GET['uid'] ), $email_hash ) ) { // WPCS: input var ok, CSRF ok, sanitization ok. + self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); + } + } + + $download_ids = $data_store->get_downloads( + array( + 'user_email' => sanitize_email( str_replace( ' ', '+', $email_address ) ), + 'order_key' => wc_clean( wp_unslash( $_GET['order'] ) ), // WPCS: input var ok, CSRF ok. + 'product_id' => $product_id, + 'download_id' => wc_clean( preg_replace( '/\s+/', ' ', wp_unslash( $_GET['key'] ) ) ), // WPCS: input var ok, CSRF ok, sanitization ok. + 'orderby' => 'downloads_remaining', + 'order' => 'DESC', + 'limit' => 1, + 'return' => 'ids', + ) + ); + + if ( empty( $download_ids ) ) { + self::download_error( __( 'Invalid download link.', 'woocommerce' ) ); + } + + $download = new WC_Customer_Download( current( $download_ids ) ); + + /** + * Filter download filepath. + * + * @since 4.0.0 + * @param string $file_path File path. + * @param string $email_address Email address. + * @param WC_Order|bool $order Order object or false. + * @param WC_Product $product Product object. + * @param WC_Customer_Download $download Download data. + */ + $file_path = apply_filters( + 'woocommerce_download_product_filepath', + $product->get_file_download_path( $download->get_download_id() ), + $email_address, + $order, + $product, + $download + ); + + $parsed_file_path = self::parse_file_path( $file_path ); + $download_range = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine. + + self::check_order_is_valid( $download ); + if ( ! $download_range['is_range_request'] ) { + // If the remaining download count goes to 0, allow range requests to be able to finish streaming from iOS devices. + self::check_downloads_remaining( $download ); + } + self::check_download_expiry( $download ); + self::check_download_login_required( $download ); + + do_action( + 'woocommerce_download_product', + $download->get_user_email(), + $download->get_order_key(), + $download->get_product_id(), + $download->get_user_id(), + $download->get_download_id(), + $download->get_order_id() + ); + $download->save(); + + // Track the download in logs and change remaining/counts. + $current_user_id = get_current_user_id(); + $ip_address = WC_Geolocation::get_ip_address(); + if ( ! $download_range['is_range_request'] ) { + $download->track_download( $current_user_id > 0 ? $current_user_id : null, ! empty( $ip_address ) ? $ip_address : null ); + } + + self::download( $file_path, $download->get_product_id() ); + } + + /** + * Check if an order is valid for downloading from. + * + * @param WC_Customer_Download $download Download instance. + */ + private static function check_order_is_valid( $download ) { + if ( $download->get_order_id() ) { + $order = wc_get_order( $download->get_order_id() ); + + if ( $order && ! $order->is_download_permitted() ) { + self::download_error( __( 'Invalid order.', 'woocommerce' ), '', 403 ); + } + } + } + + /** + * Check if there are downloads remaining. + * + * @param WC_Customer_Download $download Download instance. + */ + private static function check_downloads_remaining( $download ) { + if ( '' !== $download->get_downloads_remaining() && 0 >= $download->get_downloads_remaining() ) { + self::download_error( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ), '', 403 ); + } + } + + /** + * Check if the download has expired. + * + * @param WC_Customer_Download $download Download instance. + */ + private static function check_download_expiry( $download ) { + if ( ! is_null( $download->get_access_expires() ) && $download->get_access_expires()->getTimestamp() < strtotime( 'midnight', time() ) ) { + self::download_error( __( 'Sorry, this download has expired', 'woocommerce' ), '', 403 ); + } + } + + /** + * Check if a download requires the user to login first. + * + * @param WC_Customer_Download $download Download instance. + */ + private static function check_download_login_required( $download ) { + if ( $download->get_user_id() && 'yes' === get_option( 'woocommerce_downloads_require_login' ) ) { + if ( ! is_user_logged_in() ) { + if ( wc_get_page_id( 'myaccount' ) ) { + wp_safe_redirect( add_query_arg( 'wc_error', rawurlencode( __( 'You must be logged in to download files.', 'woocommerce' ) ), wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } else { + self::download_error( __( 'You must be logged in to download files.', 'woocommerce' ) . ' ' . __( 'Login', 'woocommerce' ) . '', __( 'Log in to Download Files', 'woocommerce' ), 403 ); + } + } elseif ( ! current_user_can( 'download_file', $download ) ) { + self::download_error( __( 'This is not your download link.', 'woocommerce' ), '', 403 ); + } + } + } + + /** + * Count download. + * + * @deprecated 4.4.0 + * @param array $download_data Download data. + */ + public static function count_download( $download_data ) { + wc_deprecated_function( 'WC_Download_Handler::count_download', '4.4.0', '' ); + } + + /** + * Download a file - hook into init function. + * + * @param string $file_path URL to file. + * @param integer $product_id Product ID of the product being downloaded. + */ + public static function download( $file_path, $product_id ) { + if ( ! $file_path ) { + self::download_error( __( 'No file defined', 'woocommerce' ) ); + } + + $filename = basename( $file_path ); + + if ( strstr( $filename, '?' ) ) { + $filename = current( explode( '?', $filename ) ); + } + + $filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id ); + + /** + * Filter download method. + * + * @since 4.5.0 + * @param string $method Download method. + * @param int $product_id Product ID. + * @param string $file_path URL to file. + */ + $file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id, $file_path ); + + // Add action to prevent issues in IE. + add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) ); + + // Trigger download via one of the methods. + do_action( 'woocommerce_download_file_' . $file_download_method, $file_path, $filename ); + } + + /** + * Redirect to a file to start the download. + * + * @param string $file_path File path. + * @param string $filename File name. + */ + public static function download_file_redirect( $file_path, $filename = '' ) { + header( 'Location: ' . $file_path ); + exit; + } + + /** + * Parse file path and see if its remote or local. + * + * @param string $file_path File path. + * @return array + */ + public static function parse_file_path( $file_path ) { + $wp_uploads = wp_upload_dir(); + $wp_uploads_dir = $wp_uploads['basedir']; + $wp_uploads_url = $wp_uploads['baseurl']; + + /** + * Replace uploads dir, site url etc with absolute counterparts if we can. + * Note the str_replace on site_url is on purpose, so if https is forced + * via filters we can still do the string replacement on a HTTP file. + */ + $replacements = array( + $wp_uploads_url => $wp_uploads_dir, + network_site_url( '/', 'https' ) => ABSPATH, + str_replace( 'https:', 'http:', network_site_url( '/', 'http' ) ) => ABSPATH, + site_url( '/', 'https' ) => ABSPATH, + str_replace( 'https:', 'http:', site_url( '/', 'http' ) ) => ABSPATH, + ); + + $count = 0; + $file_path = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path ); + $parsed_file_path = wp_parse_url( $file_path ); + $remote_file = null === $count || 0 === $count; // Remote file only if there were no replacements. + + // Paths that begin with '//' are always remote URLs. + if ( '//' === substr( $file_path, 0, 2 ) ) { + return array( + 'remote_file' => true, + 'file_path' => is_ssl() ? 'https:' . $file_path : 'http:' . $file_path, + ); + } + + // See if path needs an abspath prepended to work. + if ( file_exists( ABSPATH . $file_path ) ) { + $remote_file = false; + $file_path = ABSPATH . $file_path; + + } elseif ( '/wp-content' === substr( $file_path, 0, 11 ) ) { + $remote_file = false; + $file_path = realpath( WP_CONTENT_DIR . substr( $file_path, 11 ) ); + + // Check if we have an absolute path. + } elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ), true ) ) && isset( $parsed_file_path['path'] ) ) { + $remote_file = false; + $file_path = $parsed_file_path['path']; + } + + return array( + 'remote_file' => $remote_file, + 'file_path' => $file_path, + ); + } + + /** + * Download a file using X-Sendfile, X-Lighttpd-Sendfile, or X-Accel-Redirect if available. + * + * @param string $file_path File path. + * @param string $filename File name. + */ + public static function download_file_xsendfile( $file_path, $filename ) { + $parsed_file_path = self::parse_file_path( $file_path ); + + /** + * Fallback on force download method for remote files. This is because: + * 1. xsendfile needs proxy configuration to work for remote files, which cannot be assumed to be available on most hosts. + * 2. Force download method is more secure than redirect method if `allow_url_fopen` is enabled in `php.ini`. + */ + if ( $parsed_file_path['remote_file'] && ! apply_filters( 'woocommerce_use_xsendfile_for_remote', false ) ) { + do_action( 'woocommerce_download_file_force', $file_path, $filename ); + return; + } + + if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules(), true ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + $filepath = apply_filters( 'woocommerce_download_file_xsendfile_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path ); + header( 'X-Sendfile: ' . $filepath ); + exit; + } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + $filepath = apply_filters( 'woocommerce_download_file_xsendfile_lighttpd_file_path', $parsed_file_path['file_path'], $file_path, $filename, $parsed_file_path ); + header( 'X-Lighttpd-Sendfile: ' . $filepath ); + exit; + } elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) { + self::download_headers( $parsed_file_path['file_path'], $filename ); + $xsendfile_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`', '', $parsed_file_path['file_path'] ), '/' ); + $xsendfile_path = apply_filters( 'woocommerce_download_file_xsendfile_x_accel_redirect_file_path', $xsendfile_path, $file_path, $filename, $parsed_file_path ); + header( "X-Accel-Redirect: /$xsendfile_path" ); + exit; + } + + // Fallback. + wc_get_logger()->warning( + sprintf( + /* translators: %1$s contains the filepath of the digital asset. */ + __( '%1$s could not be served using the X-Accel-Redirect/X-Sendfile method. A Force Download will be used instead.', 'woocommerce' ), + $file_path + ) + ); + self::download_file_force( $file_path, $filename ); + } + + /** + * Parse the HTTP_RANGE request from iOS devices. + * Does not support multi-range requests. + * + * @param int $file_size Size of file in bytes. + * @return array { + * Information about range download request: beginning and length of + * file chunk, whether the range is valid/supported and whether the request is a range request. + * + * @type int $start Byte offset of the beginning of the range. Default 0. + * @type int $length Length of the requested file chunk in bytes. Optional. + * @type bool $is_range_valid Whether the requested range is a valid and supported range. + * @type bool $is_range_request Whether the request is a range request. + * } + */ + protected static function get_download_range( $file_size ) { + $start = 0; + $download_range = array( + 'start' => $start, + 'is_range_valid' => false, + 'is_range_request' => false, + ); + + if ( ! $file_size ) { + return $download_range; + } + + $end = $file_size - 1; + $download_range['length'] = $file_size; + + if ( isset( $_SERVER['HTTP_RANGE'] ) ) { // @codingStandardsIgnoreLine. + $http_range = sanitize_text_field( wp_unslash( $_SERVER['HTTP_RANGE'] ) ); // WPCS: input var ok. + $download_range['is_range_request'] = true; + + $c_start = $start; + $c_end = $end; + // Extract the range string. + list( , $range ) = explode( '=', $http_range, 2 ); + // Make sure the client hasn't sent us a multibyte range. + if ( strpos( $range, ',' ) !== false ) { + return $download_range; + } + + /* + * If the range starts with an '-' we start from the beginning. + * If not, we forward the file pointer + * and make sure to get the end byte if specified. + */ + if ( '-' === $range[0] ) { + // The n-number of the last bytes is requested. + $c_start = $file_size - substr( $range, 1 ); + } else { + $range = explode( '-', $range ); + $c_start = ( isset( $range[0] ) && is_numeric( $range[0] ) ) ? (int) $range[0] : 0; + $c_end = ( isset( $range[1] ) && is_numeric( $range[1] ) ) ? (int) $range[1] : $file_size; + } + + /* + * Check the range and make sure it's treated according to the specs: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html. + * End bytes can not be larger than $end. + */ + $c_end = ( $c_end > $end ) ? $end : $c_end; + // Validate the requested range and return an error if it's not correct. + if ( $c_start > $c_end || $c_start > $file_size - 1 || $c_end >= $file_size ) { + return $download_range; + } + $start = $c_start; + $end = $c_end; + $length = $end - $start + 1; + + $download_range['start'] = $start; + $download_range['length'] = $length; + $download_range['is_range_valid'] = true; + } + return $download_range; + } + + /** + * Force download - this is the default method. + * + * @param string $file_path File path. + * @param string $filename File name. + */ + public static function download_file_force( $file_path, $filename ) { + $parsed_file_path = self::parse_file_path( $file_path ); + $download_range = self::get_download_range( @filesize( $parsed_file_path['file_path'] ) ); // @codingStandardsIgnoreLine. + + self::download_headers( $parsed_file_path['file_path'], $filename, $download_range ); + + $start = isset( $download_range['start'] ) ? $download_range['start'] : 0; + $length = isset( $download_range['length'] ) ? $download_range['length'] : 0; + if ( ! self::readfile_chunked( $parsed_file_path['file_path'], $start, $length ) ) { + if ( $parsed_file_path['remote_file'] && 'yes' === get_option( 'woocommerce_downloads_redirect_fallback_allowed' ) ) { + wc_get_logger()->warning( + sprintf( + /* translators: %1$s contains the filepath of the digital asset. */ + __( '%1$s could not be served using the Force Download method. A redirect will be used instead.', 'woocommerce' ), + $file_path + ) + ); + self::download_file_redirect( $file_path ); + } else { + self::download_error( __( 'File not found', 'woocommerce' ) ); + } + } + + exit; + } + + /** + * Get content type of a download. + * + * @param string $file_path File path. + * @return string + */ + private static function get_download_content_type( $file_path ) { + $file_extension = strtolower( substr( strrchr( $file_path, '.' ), 1 ) ); + $ctype = 'application/force-download'; + + foreach ( get_allowed_mime_types() as $mime => $type ) { + $mimes = explode( '|', $mime ); + if ( in_array( $file_extension, $mimes, true ) ) { + $ctype = $type; + break; + } + } + + return $ctype; + } + + /** + * Set headers for the download. + * + * @param string $file_path File path. + * @param string $filename File name. + * @param array $download_range Array containing info about range download request (see {@see get_download_range} for structure). + */ + private static function download_headers( $file_path, $filename, $download_range = array() ) { + self::check_server_config(); + self::clean_buffers(); + wc_nocache_headers(); + + header( 'X-Robots-Tag: noindex, nofollow', true ); + header( 'Content-Type: ' . self::get_download_content_type( $file_path ) ); + header( 'Content-Description: File Transfer' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '";' ); + header( 'Content-Transfer-Encoding: binary' ); + + $file_size = @filesize( $file_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + if ( ! $file_size ) { + return; + } + + if ( isset( $download_range['is_range_request'] ) && true === $download_range['is_range_request'] ) { + if ( false === $download_range['is_range_valid'] ) { + header( 'HTTP/1.1 416 Requested Range Not Satisfiable' ); + header( 'Content-Range: bytes 0-' . ( $file_size - 1 ) . '/' . $file_size ); + exit; + } + + $start = $download_range['start']; + $end = $download_range['start'] + $download_range['length'] - 1; + $length = $download_range['length']; + + header( 'HTTP/1.1 206 Partial Content' ); + header( "Accept-Ranges: 0-$file_size" ); + header( "Content-Range: bytes $start-$end/$file_size" ); + header( "Content-Length: $length" ); + } else { + header( 'Content-Length: ' . $file_size ); + } + } + + /** + * Check and set certain server config variables to ensure downloads work as intended. + */ + private static function check_server_config() { + wc_set_time_limit( 0 ); + if ( function_exists( 'apache_setenv' ) ) { + @apache_setenv( 'no-gzip', 1 ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_apache_setenv + } + @ini_set( 'zlib.output_compression', 'Off' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_ini_set + @session_write_close(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.VIP.SessionFunctionsUsage.session_session_write_close + } + + /** + * Clean all output buffers. + * + * Can prevent errors, for example: transfer closed with 3 bytes remaining to read. + */ + private static function clean_buffers() { + if ( ob_get_level() ) { + $levels = ob_get_level(); + for ( $i = 0; $i < $levels; $i++ ) { + @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + } + } else { + @ob_end_clean(); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + } + } + + /** + * Read file chunked. + * + * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/. + * + * @param string $file File. + * @param int $start Byte offset/position of the beginning from which to read from the file. + * @param int $length Length of the chunk to be read from the file in bytes, 0 means full file. + * @return bool Success or fail + */ + public static function readfile_chunked( $file, $start = 0, $length = 0 ) { + if ( ! defined( 'WC_CHUNK_SIZE' ) ) { + define( 'WC_CHUNK_SIZE', 1024 * 1024 ); + } + $handle = @fopen( $file, 'r' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen + + if ( false === $handle ) { + return false; + } + + if ( ! $length ) { + $length = @filesize( $file ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + } + + $read_length = (int) WC_CHUNK_SIZE; + + if ( $length ) { + $end = $start + $length - 1; + + @fseek( $handle, $start ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + $p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + + while ( ! @feof( $handle ) && $p <= $end ) { // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + // Don't run past the end of file. + if ( $p + $read_length > $end ) { + $read_length = $end - $p + 1; + } + + echo @fread( $handle, $read_length ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_system_read_fread + $p = @ftell( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged + + if ( ob_get_length() ) { + ob_flush(); + flush(); + } + } + } else { + while ( ! @feof( $handle ) ) { // @codingStandardsIgnoreLine. + echo @fread( $handle, $read_length ); // @codingStandardsIgnoreLine. + if ( ob_get_length() ) { + ob_flush(); + flush(); + } + } + } + + return @fclose( $handle ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fclose + } + + /** + * Filter headers for IE to fix issues over SSL. + * + * IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set. + * + * @param array $headers HTTP headers. + * @return array + */ + public static function ie_nocache_headers_fix( $headers ) { + if ( is_ssl() && ! empty( $GLOBALS['is_IE'] ) ) { + $headers['Cache-Control'] = 'private'; + unset( $headers['Pragma'] ); + } + return $headers; + } + + /** + * Die with an error message if the download fails. + * + * @param string $message Error message. + * @param string $title Error title. + * @param integer $status Error status. + */ + private static function download_error( $message, $title = '', $status = 404 ) { + /* + * Since we will now render a message instead of serving a download, we should unwind some of the previously set + * headers. + */ + header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) ); + header_remove( 'Content-Description;' ); + header_remove( 'Content-Disposition' ); + header_remove( 'Content-Transfer-Encoding' ); + + if ( ! strstr( $message, '' . esc_html__( 'Go to shop', 'woocommerce' ) . ''; + } + wp_die( $message, $title, array( 'response' => $status ) ); // WPCS: XSS ok. + } +} + +WC_Download_Handler::init(); diff --git a/includes/class-wc-emails.php b/includes/class-wc-emails.php new file mode 100644 index 0000000..e4aeb5c --- /dev/null +++ b/includes/class-wc-emails.php @@ -0,0 +1,731 @@ +push_to_queue( + array( + 'filter' => current_filter(), + 'args' => func_get_args(), + ) + ); + } else { + self::send_transactional_email( ...$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 ) { + $error = 'Transactional email triggered fatal error for callback ' . current_filter(); + $logger = wc_get_logger(); + $logger->critical( + $error . PHP_EOL, + array( + 'source' => 'transactional-emails', + ) + ); + if ( Constants::is_true( 'WP_DEBUG' ) ) { + trigger_error( $error, E_USER_WARNING ); // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped, WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + } + } + } + + /** + * Constructor for the email class hooks in all emails that can be sent. + */ + 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_downloads' ), 10, 4 ); + 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' ) ); + add_action( 'woocommerce_no_stock_notification', array( $this, 'no_stock' ) ); + add_action( 'woocommerce_product_on_backorder_notification', array( $this, 'backorder' ) ); + add_action( 'woocommerce_created_customer_notification', array( $this, 'customer_new_account' ), 10, 3 ); + + // Hook for replacing {site_title} in email-footer. + add_filter( 'woocommerce_email_footer_text', array( $this, 'replace_placeholders' ) ); + + // Let 3rd parties unhook the above via this hook. + do_action( 'woocommerce_email', $this ); + } + + /** + * Init email classes. + */ + public function init() { + // Include email classes. + include_once dirname( __FILE__ ) . '/emails/class-wc-email.php'; + + $this->emails['WC_Email_New_Order'] = include __DIR__ . '/emails/class-wc-email-new-order.php'; + $this->emails['WC_Email_Cancelled_Order'] = include __DIR__ . '/emails/class-wc-email-cancelled-order.php'; + $this->emails['WC_Email_Failed_Order'] = include __DIR__ . '/emails/class-wc-email-failed-order.php'; + $this->emails['WC_Email_Customer_On_Hold_Order'] = include __DIR__ . '/emails/class-wc-email-customer-on-hold-order.php'; + $this->emails['WC_Email_Customer_Processing_Order'] = include __DIR__ . '/emails/class-wc-email-customer-processing-order.php'; + $this->emails['WC_Email_Customer_Completed_Order'] = include __DIR__ . '/emails/class-wc-email-customer-completed-order.php'; + $this->emails['WC_Email_Customer_Refunded_Order'] = include __DIR__ . '/emails/class-wc-email-customer-refunded-order.php'; + $this->emails['WC_Email_Customer_Invoice'] = include __DIR__ . '/emails/class-wc-email-customer-invoice.php'; + $this->emails['WC_Email_Customer_Note'] = include __DIR__ . '/emails/class-wc-email-customer-note.php'; + $this->emails['WC_Email_Customer_Reset_Password'] = include __DIR__ . '/emails/class-wc-email-customer-reset-password.php'; + $this->emails['WC_Email_Customer_New_Account'] = include __DIR__ . '/emails/class-wc-email-customer-new-account.php'; + + $this->emails = apply_filters( 'woocommerce_email_classes', $this->emails ); + } + + /** + * Return the email classes - used in admin to load settings. + * + * @return WC_Email[] + */ + public function get_emails() { + return $this->emails; + } + + /** + * Get from name for email. + * + * @return string + */ + public function get_from_name() { + return wp_specialchars_decode( get_option( 'woocommerce_email_from_name' ), ENT_QUOTES ); + } + + /** + * Get from email address. + * + * @return string + */ + public function get_from_address() { + return sanitize_email( get_option( 'woocommerce_email_from_address' ) ); + } + + /** + * Get the email header. + * + * @param mixed $email_heading Heading for the email. + */ + public function email_header( $email_heading ) { + wc_get_template( 'emails/email-header.php', array( 'email_heading' => $email_heading ) ); + } + + /** + * Get the email footer. + */ + public function email_footer() { + wc_get_template( 'emails/email-footer.php' ); + } + + /** + * Replace placeholder text in strings. + * + * @since 3.7.0 + * @param string $string Email footer text. + * @return string Email footer text with any replacements done. + */ + public function replace_placeholders( $string ) { + $domain = wp_parse_url( home_url(), PHP_URL_HOST ); + + return str_replace( + array( + '{site_title}', + '{site_address}', + '{site_url}', + '{woocommerce}', + '{WooCommerce}', + ), + array( + $this->get_blogname(), + $domain, + $domain, + 'WooCommerce', + 'WooCommerce', + ), + $string + ); + } + + /** + * Filter callback to replace {site_title} in email footer + * + * @since 3.3.0 + * @deprecated 3.7.0 + * @param string $string Email footer text. + * @return string Email footer text with any replacements done. + */ + public function email_footer_replace_site_title( $string ) { + wc_deprecated_function( 'WC_Emails::email_footer_replace_site_title', '3.7.0', 'WC_Emails::replace_placeholders' ); + return $this->replace_placeholders( $string ); + } + + /** + * Wraps a message in the woocommerce mail template. + * + * @param string $email_heading Heading text. + * @param string $message Email message. + * @param bool $plain_text Set true to send as plain text. Default to false. + * + * @return string + */ + public function wrap_message( $email_heading, $message, $plain_text = false ) { + // Buffer. + ob_start(); + + do_action( 'woocommerce_email_header', $email_heading, null ); + + echo wpautop( wptexturize( $message ) ); // WPCS: XSS ok. + + do_action( 'woocommerce_email_footer', null ); + + // Get contents. + $message = ob_get_clean(); + + return $message; + } + + /** + * Send the email. + * + * @param mixed $to Receiver. + * @param mixed $subject Email subject. + * @param mixed $message Message. + * @param string $headers Email headers (default: "Content-Type: text/html\r\n"). + * @param string $attachments Attachments (default: ""). + * @return bool + */ + public function send( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) { + // Send. + $email = new WC_Email(); + return $email->send( $to, $subject, $message, $headers, $attachments ); + } + + /** + * Prepare and send the customer invoice email on demand. + * + * @param int|WC_Order $order Order instance or ID. + */ + public function customer_invoice( $order ) { + $email = $this->emails['WC_Email_Customer_Invoice']; + + if ( ! is_object( $order ) ) { + $order = wc_get_order( absint( $order ) ); + } + + $email->trigger( $order->get_id(), $order ); + } + + /** + * Customer new account welcome email. + * + * @param int $customer_id Customer ID. + * @param array $new_customer_data New customer data. + * @param bool $password_generated If password is generated. + */ + 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'] : ''; + + $email = $this->emails['WC_Email_Customer_New_Account']; + $email->trigger( $customer_id, $user_pass, $password_generated ); + } + + /** + * Show the order details table + * + * @param WC_Order $order Order instance. + * @param bool $sent_to_admin If should sent to admin. + * @param bool $plain_text If is plain text email. + * @param string $email Email address. + */ + 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, + ) + ); + } + } + + /** + * Show order downloads in a table. + * + * @since 3.2.0 + * @param WC_Order $order Order instance. + * @param bool $sent_to_admin If should sent to admin. + * @param bool $plain_text If is plain text email. + * @param string $email Email address. + */ + public function order_downloads( $order, $sent_to_admin = false, $plain_text = false, $email = '' ) { + $show_downloads = $order->has_downloadable_item() && $order->is_download_permitted() && ! $sent_to_admin && ! is_a( $email, 'WC_Email_Customer_Refunded_Order' ); + + if ( ! $show_downloads ) { + return; + } + + $downloads = $order->get_downloadable_items(); + $columns = apply_filters( + 'woocommerce_email_downloads_columns', + array( + 'download-product' => __( 'Product', 'woocommerce' ), + 'download-expires' => __( 'Expires', 'woocommerce' ), + 'download-file' => __( 'Download', 'woocommerce' ), + ) + ); + + if ( $plain_text ) { + wc_get_template( + 'emails/plain/email-downloads.php', + array( + 'order' => $order, + 'sent_to_admin' => $sent_to_admin, + 'plain_text' => $plain_text, + 'email' => $email, + 'downloads' => $downloads, + 'columns' => $columns, + ) + ); + } else { + wc_get_template( + 'emails/email-downloads.php', + array( + 'order' => $order, + 'sent_to_admin' => $sent_to_admin, + 'plain_text' => $plain_text, + 'email' => $email, + 'downloads' => $downloads, + 'columns' => $columns, + ) + ); + } + } + + /** + * Add order meta to email templates. + * + * @param WC_Order $order Order instance. + * @param bool $sent_to_admin If should sent to admin. + * @param bool $plain_text If is plain text email. + */ + 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 ); + + /** + * Deprecated woocommerce_email_order_meta_keys filter. + * + * @since 2.3.0 + */ + $_fields = apply_filters( 'woocommerce_email_order_meta_keys', array(), $sent_to_admin ); + + if ( $_fields ) { + foreach ( $_fields as $key => $field ) { + if ( is_numeric( $key ) ) { + $key = $field; + } + + $fields[ $key ] = array( + 'label' => wptexturize( $key ), + 'value' => wptexturize( get_post_meta( $order->get_id(), $field, true ) ), + ); + } + } + + if ( $fields ) { + + if ( $plain_text ) { + + foreach ( $fields as $field ) { + if ( isset( $field['label'] ) && isset( $field['value'] ) && $field['value'] ) { + echo $field['label'] . ': ' . $field['value'] . "\n"; // WPCS: XSS ok. + } + } + } else { + + foreach ( $fields as $field ) { + if ( isset( $field['label'] ) && isset( $field['value'] ) && $field['value'] ) { + echo '

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

    '; // WPCS: XSS ok. + } + } + } + } + } + + /** + * Is customer detail field valid? + * + * @param array $field Field data to check if is valid. + * @return boolean + */ + public function customer_detail_field_is_valid( $field ) { + return isset( $field['label'] ) && ! empty( $field['value'] ); + } + + /** + * Allows developers to add additional customer details to templates. + * + * In versions prior to 3.2 this was used for notes, phone and email but this data has moved. + * + * @param WC_Order $order Order instance. + * @param bool $sent_to_admin If should sent to admin. + * @param bool $plain_text If is plain text email. + */ + public function customer_details( $order, $sent_to_admin = false, $plain_text = false ) { + if ( ! is_a( $order, 'WC_Order' ) ) { + return; + } + + $fields = array_filter( apply_filters( 'woocommerce_email_customer_details_fields', array(), $sent_to_admin, $order ), array( $this, 'customer_detail_field_is_valid' ) ); + + if ( ! empty( $fields ) ) { + 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 Order instance. + * @param bool $sent_to_admin If should sent to admin. + * @param bool $plain_text If is plain text email. + */ + 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, + 'sent_to_admin' => $sent_to_admin, + ) + ); + } else { + wc_get_template( + 'emails/email-addresses.php', + array( + 'order' => $order, + 'sent_to_admin' => $sent_to_admin, + ) + ); + } + } + + /** + * 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. + * + * @param WC_Product $product Product instance. + */ + public function low_stock( $product ) { + if ( 'no' === get_option( 'woocommerce_notify_low_stock', 'yes' ) ) { + return; + } + + /** + * Determine if the current product should trigger a low stock notification + * + * @param int $product_id - The low stock product id + * + * @since 4.7.0 + */ + if ( false === apply_filters( 'woocommerce_should_send_low_stock_notification', true, $product->get_id() ) ) { + return; + } + + $subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product low in stock', 'woocommerce' ) ); + $message = sprintf( + /* translators: 1: product name 2: items in stock */ + __( '%1$s is low in stock. There are %2$d left.', 'woocommerce' ), + html_entity_decode( wp_strip_all_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ), + html_entity_decode( wp_strip_all_tags( $product->get_stock_quantity() ) ) + ); + + wp_mail( + apply_filters( 'woocommerce_email_recipient_low_stock', get_option( 'woocommerce_stock_email_recipient' ), $product, null ), + apply_filters( 'woocommerce_email_subject_low_stock', $subject, $product, null ), + apply_filters( 'woocommerce_email_content_low_stock', $message, $product ), + apply_filters( 'woocommerce_email_headers', '', 'low_stock', $product, null ), + apply_filters( 'woocommerce_email_attachments', array(), 'low_stock', $product, null ) + ); + } + + /** + * No stock notification email. + * + * @param WC_Product $product Product instance. + */ + public function no_stock( $product ) { + if ( 'no' === get_option( 'woocommerce_notify_no_stock', 'yes' ) ) { + return; + } + + /** + * Determine if the current product should trigger a no stock notification + * + * @param int $product_id - The out of stock product id + * + * @since 4.6.0 + */ + if ( false === apply_filters( 'woocommerce_should_send_no_stock_notification', true, $product->get_id() ) ) { + return; + } + + $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( wp_strip_all_tags( $product->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ) ); + + wp_mail( + apply_filters( 'woocommerce_email_recipient_no_stock', get_option( 'woocommerce_stock_email_recipient' ), $product, null ), + apply_filters( 'woocommerce_email_subject_no_stock', $subject, $product, null ), + apply_filters( 'woocommerce_email_content_no_stock', $message, $product ), + apply_filters( 'woocommerce_email_headers', '', 'no_stock', $product, null ), + apply_filters( 'woocommerce_email_attachments', array(), 'no_stock', $product, null ) + ); + } + + /** + * Backorder notification email. + * + * @param array $args Arguments. + */ + public function backorder( $args ) { + $args = wp_parse_args( + $args, + array( + 'product' => '', + 'quantity' => '', + 'order_id' => '', + ) + ); + + $order = wc_get_order( $args['order_id'] ); + if ( + ! $args['product'] || + ! is_object( $args['product'] ) || + ! $args['quantity'] || + ! $order + ) { + return; + } + + $subject = sprintf( '[%s] %s', $this->get_blogname(), __( 'Product backorder', 'woocommerce' ) ); + /* translators: 1: product quantity 2: product name 3: order number */ + $message = sprintf( __( '%1$s units of %2$s have been backordered in order #%3$s.', 'woocommerce' ), $args['quantity'], html_entity_decode( wp_strip_all_tags( $args['product']->get_formatted_name() ), ENT_QUOTES, get_bloginfo( 'charset' ) ), $order->get_order_number() ); + + wp_mail( + apply_filters( 'woocommerce_email_recipient_backorder', get_option( 'woocommerce_stock_email_recipient' ), $args, null ), + apply_filters( 'woocommerce_email_subject_backorder', $subject, $args, null ), + apply_filters( 'woocommerce_email_content_backorder', $message, $args ), + apply_filters( 'woocommerce_email_headers', '', 'backorder', $args, null ), + apply_filters( 'woocommerce_email_attachments', array(), 'backorder', $args, null ) + ); + } + + /** + * 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 WC_Order $order Order instance. + * @param bool $sent_to_admin If should sent to admin. + * @param bool $plain_text If is plain text email. + */ + 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 0000000..da14279 --- /dev/null +++ b/includes/class-wc-embed.php @@ -0,0 +1,178 @@ +' . $_product->get_price_html() . '

    '; // WPCS: XSS ok. + + 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() ) { + return; + } + + $_product = wc_get_product( get_the_ID() ); + + if ( $_product && $_product->get_average_rating() > 0 ) { + ?> +
    + get_average_rating() ) + ); + ?> +
    + + + ID : 0; + } else { + $user_id = absint( $_GET['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + // If the reset token is not for the current user, ignore the reset request (don't redirect). + $logged_in_user_id = get_current_user_id(); + if ( $logged_in_user_id && $logged_in_user_id !== $user_id ) { + wc_add_notice( __( 'This password reset key is for a different user account. Please log out and try again.', 'woocommerce' ), 'error' ); + return; + } + + $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; + $value = sprintf( '%d:%s', $user_id, wp_unslash( $_GET['key'] ) ); // phpcs:ignore + WC_Shortcode_My_Account::set_reset_password_cookie( $value ); + wp_safe_redirect( + add_query_arg( + array( + 'show-reset-form' => 'true', + 'action' => $action, + ), + wc_lostpassword_url() + ) + ); + exit; + } + } + + /** + * Save and and update a billing or shipping address if the + * form was submitted through the user account page. + */ + public static function save_address() { + global $wp; + + $nonce_value = wc_get_var( $_REQUEST['woocommerce-edit-address-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-edit_address' ) ) { + return; + } + + if ( empty( $_POST['action'] ) || 'edit_address' !== $_POST['action'] ) { + return; + } + + wc_nocache_headers(); + + $user_id = get_current_user_id(); + + if ( $user_id <= 0 ) { + return; + } + + $customer = new WC_Customer( $user_id ); + + if ( ! $customer ) { + return; + } + + $load_address = isset( $wp->query_vars['edit-address'] ) ? wc_edit_address_i18n( sanitize_title( $wp->query_vars['edit-address'] ), true ) : 'billing'; + + if ( ! isset( $_POST[ $load_address . '_country' ] ) ) { + return; + } + + $address = WC()->countries->get_address_fields( wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ), $load_address . '_' ); + + foreach ( $address as $key => $field ) { + if ( ! isset( $field['type'] ) ) { + $field['type'] = 'text'; + } + + // Get Value. + if ( 'checkbox' === $field['type'] ) { + $value = (int) isset( $_POST[ $key ] ); + } else { + $value = isset( $_POST[ $key ] ) ? wc_clean( wp_unslash( $_POST[ $key ] ) ) : ''; + } + + // Hook to allow modification of value. + $value = apply_filters( 'woocommerce_process_myaccount_field_' . $key, $value ); + + // Validation: Required fields. + if ( ! empty( $field['required'] ) && empty( $value ) ) { + /* translators: %s: Field name. */ + wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), $field['label'] ), 'error', array( 'id' => $key ) ); + } + + if ( ! empty( $value ) ) { + // Validation and formatting rules. + if ( ! empty( $field['validate'] ) && is_array( $field['validate'] ) ) { + foreach ( $field['validate'] as $rule ) { + switch ( $rule ) { + case 'postcode': + $country = wc_clean( wp_unslash( $_POST[ $load_address . '_country' ] ) ); + $value = wc_format_postcode( $value, $country ); + + if ( '' !== $value && ! WC_Validation::is_postcode( $value, $country ) ) { + switch ( $country ) { + case 'IE': + $postcode_validation_notice = __( 'Please enter a valid Eircode.', 'woocommerce' ); + break; + default: + $postcode_validation_notice = __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ); + } + wc_add_notice( $postcode_validation_notice, 'error' ); + } + break; + case 'phone': + if ( '' !== $value && ! WC_Validation::is_phone( $value ) ) { + /* translators: %s: Phone number. */ + wc_add_notice( sprintf( __( '%s is not a valid phone number.', 'woocommerce' ), '' . $field['label'] . '' ), 'error' ); + } + break; + case 'email': + $value = strtolower( $value ); + + if ( ! is_email( $value ) ) { + /* translators: %s: Email address. */ + wc_add_notice( sprintf( __( '%s is not a valid email address.', 'woocommerce' ), '' . $field['label'] . '' ), 'error' ); + } + break; + } + } + } + } + + try { + // Set prop in customer object. + if ( is_callable( array( $customer, "set_$key" ) ) ) { + $customer->{"set_$key"}( $value ); + } else { + $customer->update_meta_data( $key, $value ); + } + } catch ( WC_Data_Exception $e ) { + // Set notices. Ignore invalid billing email, since is already validated. + if ( 'customer_invalid_billing_email' !== $e->getErrorCode() ) { + wc_add_notice( $e->getMessage(), 'error' ); + } + } + } + + /** + * Hook: woocommerce_after_save_address_validation. + * + * Allow developers to add custom validation logic and throw an error to prevent save. + * + * @param int $user_id User ID being saved. + * @param string $load_address Type of address e.g. billing or shipping. + * @param array $address The address fields. + * @param WC_Customer $customer The customer object being saved. @since 3.6.0 + */ + do_action( 'woocommerce_after_save_address_validation', $user_id, $load_address, $address, $customer ); + + if ( 0 < wc_notice_count( 'error' ) ) { + return; + } + + $customer->save(); + + wc_add_notice( __( 'Address changed successfully.', 'woocommerce' ) ); + + do_action( 'woocommerce_customer_save_address', $user_id, $load_address ); + + wp_safe_redirect( wc_get_endpoint_url( 'edit-address', '', wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } + + /** + * Save the password/account details and redirect back to the my account page. + */ + public static function save_account_details() { + $nonce_value = wc_get_var( $_REQUEST['save-account-details-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'save_account_details' ) ) { + return; + } + + if ( empty( $_POST['action'] ) || 'save_account_details' !== $_POST['action'] ) { + return; + } + + wc_nocache_headers(); + + $user_id = get_current_user_id(); + + if ( $user_id <= 0 ) { + return; + } + + $account_first_name = ! empty( $_POST['account_first_name'] ) ? wc_clean( wp_unslash( $_POST['account_first_name'] ) ) : ''; + $account_last_name = ! empty( $_POST['account_last_name'] ) ? wc_clean( wp_unslash( $_POST['account_last_name'] ) ) : ''; + $account_display_name = ! empty( $_POST['account_display_name'] ) ? wc_clean( wp_unslash( $_POST['account_display_name'] ) ) : ''; + $account_email = ! empty( $_POST['account_email'] ) ? wc_clean( wp_unslash( $_POST['account_email'] ) ) : ''; + $pass_cur = ! empty( $_POST['password_current'] ) ? $_POST['password_current'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $pass1 = ! empty( $_POST['password_1'] ) ? $_POST['password_1'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $pass2 = ! empty( $_POST['password_2'] ) ? $_POST['password_2'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $save_pass = true; + + // Current user data. + $current_user = get_user_by( 'id', $user_id ); + $current_first_name = $current_user->first_name; + $current_last_name = $current_user->last_name; + $current_email = $current_user->user_email; + + // New user data. + $user = new stdClass(); + $user->ID = $user_id; + $user->first_name = $account_first_name; + $user->last_name = $account_last_name; + $user->display_name = $account_display_name; + + // Prevent display name to be changed to email. + if ( is_email( $account_display_name ) ) { + wc_add_notice( __( 'Display name cannot be changed to email address due to privacy concern.', 'woocommerce' ), 'error' ); + } + + // Handle required fields. + $required_fields = apply_filters( + 'woocommerce_save_account_details_required_fields', + array( + 'account_first_name' => __( 'First name', 'woocommerce' ), + 'account_last_name' => __( 'Last name', 'woocommerce' ), + 'account_display_name' => __( 'Display name', 'woocommerce' ), + 'account_email' => __( 'Email address', 'woocommerce' ), + ) + ); + + foreach ( $required_fields as $field_key => $field_name ) { + if ( empty( $_POST[ $field_key ] ) ) { + /* translators: %s: Field name. */ + wc_add_notice( sprintf( __( '%s is a required field.', 'woocommerce' ), '' . esc_html( $field_name ) . '' ), 'error', array( 'id' => $field_key ) ); + } + } + + if ( $account_email ) { + $account_email = sanitize_email( $account_email ); + if ( ! is_email( $account_email ) ) { + wc_add_notice( __( 'Please provide a valid email address.', 'woocommerce' ), 'error' ); + } elseif ( email_exists( $account_email ) && $account_email !== $current_user->user_email ) { + wc_add_notice( __( 'This email address is already registered.', 'woocommerce' ), 'error' ); + } + $user->user_email = $account_email; + } + + if ( ! empty( $pass_cur ) && empty( $pass1 ) && empty( $pass2 ) ) { + wc_add_notice( __( 'Please fill out all password fields.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ! empty( $pass1 ) && empty( $pass_cur ) ) { + wc_add_notice( __( 'Please enter your current password.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ! empty( $pass1 ) && empty( $pass2 ) ) { + wc_add_notice( __( 'Please re-enter your password.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ( ! empty( $pass1 ) || ! empty( $pass2 ) ) && $pass1 !== $pass2 ) { + wc_add_notice( __( 'New passwords do not match.', 'woocommerce' ), 'error' ); + $save_pass = false; + } elseif ( ! empty( $pass1 ) && ! wp_check_password( $pass_cur, $current_user->user_pass, $current_user->ID ) ) { + wc_add_notice( __( 'Your current password is incorrect.', 'woocommerce' ), 'error' ); + $save_pass = false; + } + + if ( $pass1 && $save_pass ) { + $user->user_pass = $pass1; + } + + // Allow plugins to return their own errors. + $errors = new WP_Error(); + do_action_ref_array( 'woocommerce_save_account_details_errors', array( &$errors, &$user ) ); + + if ( $errors->get_error_messages() ) { + foreach ( $errors->get_error_messages() as $error ) { + wc_add_notice( $error, 'error' ); + } + } + + if ( wc_notice_count( 'error' ) === 0 ) { + wp_update_user( $user ); + + // Update customer object to keep data in sync. + $customer = new WC_Customer( $user->ID ); + + if ( $customer ) { + // Keep billing data in sync if data changed. + if ( is_email( $user->user_email ) && $current_email !== $user->user_email ) { + $customer->set_billing_email( $user->user_email ); + } + + if ( $current_first_name !== $user->first_name ) { + $customer->set_billing_first_name( $user->first_name ); + } + + if ( $current_last_name !== $user->last_name ) { + $customer->set_billing_last_name( $user->last_name ); + } + + $customer->save(); + } + + wc_add_notice( __( 'Account details changed successfully.', 'woocommerce' ) ); + + do_action( 'woocommerce_save_account_details', $user->ID ); + + wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); + exit; + } + } + + /** + * Process the checkout form. + */ + public static function checkout_action() { + if ( isset( $_POST['woocommerce_checkout_place_order'] ) || isset( $_POST['woocommerce_checkout_update_totals'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + wc_nocache_headers(); + + if ( WC()->cart->is_empty() ) { + wp_safe_redirect( wc_get_cart_url() ); + exit; + } + + wc_maybe_define_constant( 'WOOCOMMERCE_CHECKOUT', true ); + + WC()->checkout()->process_checkout(); + } + } + + /** + * Process the pay form. + * + * @throws Exception On payment error. + */ + public static function pay_action() { + global $wp; + + if ( isset( $_POST['woocommerce_pay'], $_GET['key'] ) ) { + wc_nocache_headers(); + + $nonce_value = wc_get_var( $_REQUEST['woocommerce-pay-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-pay' ) ) { + return; + } + + ob_start(); + + // Pay for existing order. + $order_key = wp_unslash( $_GET['key'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $order_id = absint( $wp->query_vars['order-pay'] ); + $order = wc_get_order( $order_id ); + + if ( $order_id === $order->get_id() && hash_equals( $order->get_order_key(), $order_key ) && $order->needs_payment() ) { + + do_action( 'woocommerce_before_pay_action', $order ); + + WC()->customer->set_props( + array( + 'billing_country' => $order->get_billing_country() ? $order->get_billing_country() : null, + 'billing_state' => $order->get_billing_state() ? $order->get_billing_state() : null, + 'billing_postcode' => $order->get_billing_postcode() ? $order->get_billing_postcode() : null, + 'billing_city' => $order->get_billing_city() ? $order->get_billing_city() : null, + ) + ); + WC()->customer->save(); + + if ( ! empty( $_POST['terms-field'] ) && empty( $_POST['terms'] ) ) { + wc_add_notice( __( 'Please read and accept the terms and conditions to proceed with your order.', 'woocommerce' ), 'error' ); + return; + } + + // Update payment method. + if ( $order->needs_payment() ) { + try { + $payment_method_id = isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : false; + + if ( ! $payment_method_id ) { + throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) ); + } + + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + $payment_method = isset( $available_gateways[ $payment_method_id ] ) ? $available_gateways[ $payment_method_id ] : false; + + if ( ! $payment_method ) { + throw new Exception( __( 'Invalid payment method.', 'woocommerce' ) ); + } + + $order->set_payment_method( $payment_method ); + $order->save(); + + $payment_method->validate_fields(); + + if ( 0 === wc_notice_count( 'error' ) ) { + + $result = $payment_method->process_payment( $order_id ); + + // Redirect to success/confirmation/payment page. + if ( isset( $result['result'] ) && 'success' === $result['result'] ) { + $result['order_id'] = $order_id; + + $result = apply_filters( 'woocommerce_payment_successful_result', $result, $order_id ); + + wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + exit; + } + } + } catch ( Exception $e ) { + wc_add_notice( $e->getMessage(), 'error' ); + } + } else { + // No payment was required for order. + $order->payment_complete(); + wp_safe_redirect( $order->get_checkout_order_received_url() ); + exit; + } + + do_action( 'woocommerce_after_pay_action', $order ); + + } + } + } + + /** + * Process the add payment method form. + */ + public static function add_payment_method_action() { + if ( isset( $_POST['woocommerce_add_payment_method'], $_POST['payment_method'] ) ) { + wc_nocache_headers(); + + $nonce_value = wc_get_var( $_REQUEST['woocommerce-add-payment-method-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'woocommerce-add-payment-method' ) ) { + return; + } + + if ( ! apply_filters( 'woocommerce_add_payment_method_form_is_valid', true ) ) { + return; + } + + // Test rate limit. + $current_user_id = get_current_user_id(); + $rate_limit_id = 'add_payment_method_' . $current_user_id; + $delay = (int) apply_filters( 'woocommerce_payment_gateway_add_payment_method_delay', 20 ); + + if ( WC_Rate_Limiter::retried_too_soon( $rate_limit_id ) ) { + wc_add_notice( + sprintf( + /* translators: %d number of seconds */ + _n( + 'You cannot add a new payment method so soon after the previous one. Please wait for %d second.', + 'You cannot add a new payment method so soon after the previous one. Please wait for %d seconds.', + $delay, + 'woocommerce' + ), + $delay + ), + 'error' + ); + return; + } + + WC_Rate_Limiter::set_rate_limit( $rate_limit_id, $delay ); + + ob_start(); + + $payment_method_id = wc_clean( wp_unslash( $_POST['payment_method'] ) ); + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + + if ( isset( $available_gateways[ $payment_method_id ] ) ) { + $gateway = $available_gateways[ $payment_method_id ]; + + if ( ! $gateway->supports( 'add_payment_method' ) && ! $gateway->supports( 'tokenization' ) ) { + wc_add_notice( __( 'Invalid payment gateway.', 'woocommerce' ), 'error' ); + return; + } + + $gateway->validate_fields(); + + if ( wc_notice_count( 'error' ) > 0 ) { + return; + } + + $result = $gateway->add_payment_method(); + + if ( 'success' === $result['result'] ) { + wc_add_notice( __( 'Payment method successfully added.', 'woocommerce' ) ); + } + + if ( 'failure' === $result['result'] ) { + wc_add_notice( __( 'Unable to add payment method to your account.', 'woocommerce' ), 'error' ); + } + + if ( ! empty( $result['redirect'] ) ) { + wp_redirect( $result['redirect'] ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + exit(); + } + } + } + } + + /** + * Process the delete payment method form. + */ + public static function delete_payment_method_action() { + global $wp; + + if ( isset( $wp->query_vars['delete-payment-method'] ) ) { + wc_nocache_headers(); + + $token_id = absint( $wp->query_vars['delete-payment-method'] ); + $token = WC_Payment_Tokens::get( $token_id ); + + if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'delete-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); + } else { + WC_Payment_Tokens::delete( $token_id ); + wc_add_notice( __( 'Payment method deleted.', 'woocommerce' ) ); + } + + wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); + exit(); + } + + } + + /** + * Process the delete payment method form. + */ + public static function set_default_payment_method_action() { + global $wp; + + if ( isset( $wp->query_vars['set-default-payment-method'] ) ) { + wc_nocache_headers(); + + $token_id = absint( $wp->query_vars['set-default-payment-method'] ); + $token = WC_Payment_Tokens::get( $token_id ); + + if ( is_null( $token ) || get_current_user_id() !== $token->get_user_id() || ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( wp_unslash( $_REQUEST['_wpnonce'] ), 'set-default-payment-method-' . $token_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + wc_add_notice( __( 'Invalid payment method.', 'woocommerce' ), 'error' ); + } else { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), intval( $token_id ) ); + wc_add_notice( __( 'This payment method was successfully set as your default.', 'woocommerce' ) ); + } + + wp_safe_redirect( wc_get_account_endpoint_url( 'payment-methods' ) ); + exit(); + } + + } + + /** + * Remove from cart/update. + */ + public static function update_cart_action() { + if ( ! ( isset( $_REQUEST['apply_coupon'] ) || isset( $_REQUEST['remove_coupon'] ) || isset( $_REQUEST['remove_item'] ) || isset( $_REQUEST['undo_item'] ) || isset( $_REQUEST['update_cart'] ) || isset( $_REQUEST['proceed'] ) ) ) { + return; + } + + wc_nocache_headers(); + + $nonce_value = wc_get_var( $_REQUEST['woocommerce-cart-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! empty( $_POST['apply_coupon'] ) && ! empty( $_POST['coupon_code'] ) ) { + WC()->cart->add_discount( wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + } elseif ( isset( $_GET['remove_coupon'] ) ) { + WC()->cart->remove_coupon( wc_format_coupon_code( urldecode( wp_unslash( $_GET['remove_coupon'] ) ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + } elseif ( ! empty( $_GET['remove_item'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { + $cart_item_key = sanitize_text_field( wp_unslash( $_GET['remove_item'] ) ); + $cart_item = WC()->cart->get_cart_item( $cart_item_key ); + + if ( $cart_item ) { + WC()->cart->remove_cart_item( $cart_item_key ); + + $product = wc_get_product( $cart_item['product_id'] ); + + /* translators: %s: Item name. */ + $item_removed_title = apply_filters( 'woocommerce_cart_item_removed_title', $product ? sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), $product->get_name() ) : __( 'Item', 'woocommerce' ), $cart_item ); + + // Don't show undo link if removed item is out of stock. + if ( $product && $product->is_in_stock() && $product->has_enough_stock( $cart_item['quantity'] ) ) { + /* Translators: %s Product title. */ + $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); + $removed_notice .= ' ' . __( 'Undo?', 'woocommerce' ) . ''; + } else { + /* Translators: %s Product title. */ + $removed_notice = sprintf( __( '%s removed.', 'woocommerce' ), $item_removed_title ); + } + + wc_add_notice( $removed_notice, apply_filters( 'woocommerce_cart_item_removed_notice_type', 'success' ) ); + } + + $referer = wp_get_referer() ? remove_query_arg( array( 'remove_item', 'add-to-cart', 'added-to-cart', 'order_again', '_wpnonce' ), add_query_arg( 'removed_item', '1', wp_get_referer() ) ) : wc_get_cart_url(); + wp_safe_redirect( $referer ); + exit; + + } elseif ( ! empty( $_GET['undo_item'] ) && isset( $_GET['_wpnonce'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { + + // Undo Cart Item. + $cart_item_key = sanitize_text_field( wp_unslash( $_GET['undo_item'] ) ); + + WC()->cart->restore_cart_item( $cart_item_key ); + + $referer = wp_get_referer() ? remove_query_arg( array( 'undo_item', '_wpnonce' ), wp_get_referer() ) : wc_get_cart_url(); + wp_safe_redirect( $referer ); + exit; + + } + + // Update Cart - checks apply_coupon too because they are in the same form. + if ( ( ! empty( $_POST['apply_coupon'] ) || ! empty( $_POST['update_cart'] ) || ! empty( $_POST['proceed'] ) ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) { + + $cart_updated = false; + $cart_totals = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( ! WC()->cart->is_empty() && is_array( $cart_totals ) ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) { + + $_product = $values['data']; + + // Skip product if no updated quantity was posted. + if ( ! isset( $cart_totals[ $cart_item_key ] ) || ! isset( $cart_totals[ $cart_item_key ]['qty'] ) ) { + continue; + } + + // Sanitize. + $quantity = apply_filters( 'woocommerce_stock_amount_cart_item', wc_stock_amount( preg_replace( '/[^0-9\.]/', '', $cart_totals[ $cart_item_key ]['qty'] ) ), $cart_item_key ); + + if ( '' === $quantity || $quantity === $values['quantity'] ) { + continue; + } + + // Update cart validation. + $passed_validation = apply_filters( 'woocommerce_update_cart_validation', true, $cart_item_key, $values, $quantity ); + + // is_sold_individually. + if ( $_product->is_sold_individually() && $quantity > 1 ) { + /* Translators: %s Product title. */ + wc_add_notice( sprintf( __( 'You can only have 1 %s in your cart.', 'woocommerce' ), $_product->get_name() ), 'error' ); + $passed_validation = false; + } + + if ( $passed_validation ) { + WC()->cart->set_quantity( $cart_item_key, $quantity, false ); + $cart_updated = true; + } + } + } + + // Trigger action - let 3rd parties update the cart if they need to and update the $cart_updated variable. + $cart_updated = apply_filters( 'woocommerce_update_cart_action_cart_updated', $cart_updated ); + + if ( $cart_updated ) { + WC()->cart->calculate_totals(); + } + + if ( ! empty( $_POST['proceed'] ) ) { + wp_safe_redirect( wc_get_checkout_url() ); + exit; + } elseif ( $cart_updated ) { + wc_add_notice( __( 'Cart updated.', 'woocommerce' ), apply_filters( 'woocommerce_cart_updated_notice_type', 'success' ) ); + $referer = remove_query_arg( array( 'remove_coupon', 'add-to-cart' ), ( wp_get_referer() ? wp_get_referer() : wc_get_cart_url() ) ); + wp_safe_redirect( $referer ); + exit; + } + } + } + + /** + * Place a previous order again. + * + * @deprecated 3.5.0 Logic moved to cart session handling. + */ + public static function order_again() { + wc_deprecated_function( 'WC_Form_Handler::order_again', '3.5', 'This method should not be called manually.' ); + } + + /** + * Cancel a pending order. + */ + public static function cancel_order() { + if ( + isset( $_GET['cancel_order'] ) && + isset( $_GET['order'] ) && + isset( $_GET['order_id'] ) && + ( isset( $_GET['_wpnonce'] ) && wp_verify_nonce( wp_unslash( $_GET['_wpnonce'] ), 'woocommerce-cancel_order' ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + ) { + wc_nocache_headers(); + + $order_key = wp_unslash( $_GET['order'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $order_id = absint( $_GET['order_id'] ); + $order = wc_get_order( $order_id ); + $user_can_cancel = current_user_can( 'cancel_order', $order_id ); + $order_can_cancel = $order->has_status( apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ) ); + $redirect = isset( $_GET['redirect'] ) ? wp_unslash( $_GET['redirect'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( $user_can_cancel && $order_can_cancel && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) { + + // Cancel the order + restore stock. + WC()->session->set( 'order_awaiting_payment', false ); + $order->update_status( 'cancelled', __( 'Order cancelled by customer.', 'woocommerce' ) ); + + wc_add_notice( apply_filters( 'woocommerce_order_cancelled_notice', __( 'Your order was cancelled.', 'woocommerce' ) ), apply_filters( 'woocommerce_order_cancelled_notice_type', 'notice' ) ); + + do_action( 'woocommerce_cancelled_order', $order->get_id() ); + + } elseif ( $user_can_cancel && ! $order_can_cancel ) { + wc_add_notice( __( 'Your order can no longer be cancelled. Please contact us if you need assistance.', 'woocommerce' ), 'error' ); + } else { + wc_add_notice( __( 'Invalid order.', 'woocommerce' ), 'error' ); + } + + if ( $redirect ) { + wp_safe_redirect( $redirect ); + exit; + } + } + } + + /** + * Add to cart action. + * + * Checks for a valid request, does validation (via hooks) and then redirects if valid. + * + * @param bool $url (default: false) URL to redirect to. + */ + public static function add_to_cart_action( $url = false ) { + if ( ! isset( $_REQUEST['add-to-cart'] ) || ! is_numeric( wp_unslash( $_REQUEST['add-to-cart'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + return; + } + + wc_nocache_headers(); + + $product_id = apply_filters( 'woocommerce_add_to_cart_product_id', absint( wp_unslash( $_REQUEST['add-to-cart'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $was_added_to_cart = false; + $adding_to_cart = wc_get_product( $product_id ); + + if ( ! $adding_to_cart ) { + return; + } + + $add_to_cart_handler = apply_filters( 'woocommerce_add_to_cart_handler', $adding_to_cart->get_type(), $adding_to_cart ); + + if ( 'variable' === $add_to_cart_handler || 'variation' === $add_to_cart_handler ) { + $was_added_to_cart = self::add_to_cart_handler_variable( $product_id ); + } elseif ( 'grouped' === $add_to_cart_handler ) { + $was_added_to_cart = self::add_to_cart_handler_grouped( $product_id ); + } elseif ( has_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler ) ) { + do_action( 'woocommerce_add_to_cart_handler_' . $add_to_cart_handler, $url ); // Custom handler. + } else { + $was_added_to_cart = self::add_to_cart_handler_simple( $product_id ); + } + + // If we added the product to the cart we can now optionally do a redirect. + if ( $was_added_to_cart && 0 === wc_notice_count( 'error' ) ) { + $url = apply_filters( 'woocommerce_add_to_cart_redirect', $url, $adding_to_cart ); + + if ( $url ) { + wp_safe_redirect( $url ); + exit; + } elseif ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { + wp_safe_redirect( wc_get_cart_url() ); + exit; + } + } + } + + /** + * Handle adding simple products to the cart. + * + * @since 2.4.6 Split from add_to_cart_action. + * @param int $product_id Product ID to add to the cart. + * @return bool success or not + */ + private static function add_to_cart_handler_simple( $product_id ) { + $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); + + if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity ) ) { + wc_add_to_cart_message( array( $product_id => $quantity ), true ); + return true; + } + return false; + } + + /** + * Handle adding grouped products to the cart. + * + * @since 2.4.6 Split from add_to_cart_action. + * @param int $product_id Product ID to add to the cart. + * @return bool success or not + */ + private static function add_to_cart_handler_grouped( $product_id ) { + $was_added_to_cart = false; + $added_to_cart = array(); + $items = isset( $_REQUEST['quantity'] ) && is_array( $_REQUEST['quantity'] ) ? wp_unslash( $_REQUEST['quantity'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( ! empty( $items ) ) { + $quantity_set = false; + + foreach ( $items as $item => $quantity ) { + $quantity = wc_stock_amount( $quantity ); + if ( $quantity <= 0 ) { + continue; + } + $quantity_set = true; + + // Add to cart validation. + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $item, $quantity ); + + // Suppress total recalculation until finished. + remove_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 ); + + if ( $passed_validation && false !== WC()->cart->add_to_cart( $item, $quantity ) ) { + $was_added_to_cart = true; + $added_to_cart[ $item ] = $quantity; + } + + add_action( 'woocommerce_add_to_cart', array( WC()->cart, 'calculate_totals' ), 20, 0 ); + } + + if ( ! $was_added_to_cart && ! $quantity_set ) { + wc_add_notice( __( 'Please choose the quantity of items you wish to add to your cart…', 'woocommerce' ), 'error' ); + } elseif ( $was_added_to_cart ) { + wc_add_to_cart_message( $added_to_cart ); + WC()->cart->calculate_totals(); + return true; + } + } elseif ( $product_id ) { + /* Link on product archives */ + wc_add_notice( __( 'Please choose a product to add to your cart…', 'woocommerce' ), 'error' ); + } + return false; + } + + /** + * Handle adding variable products to the cart. + * + * @since 2.4.6 Split from add_to_cart_action. + * @throws Exception If add to cart fails. + * @param int $product_id Product ID to add to the cart. + * @return bool success or not + */ + private static function add_to_cart_handler_variable( $product_id ) { + $variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $variations = array(); + + $product = wc_get_product( $product_id ); + + foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( 'attribute_' !== substr( $key, 0, 10 ) ) { + continue; + } + + $variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value ); + } + + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations ); + + if ( ! $passed_validation ) { + return false; + } + + // Prevent parent variable product from being added to cart. + if ( empty( $variation_id ) && $product && $product->is_type( 'variable' ) ) { + /* translators: 1: product link, 2: product name */ + wc_add_notice( sprintf( __( 'Please choose product options by visiting %2$s.', 'woocommerce' ), esc_url( get_permalink( $product_id ) ), esc_html( $product->get_name() ) ), 'error' ); + + return false; + } + + if ( false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) ) { + wc_add_to_cart_message( array( $product_id => $quantity ), true ); + return true; + } + + return false; + } + + /** + * Process the login form. + * + * @throws Exception On login error. + */ + public static function process_login() { + // The global form-login.php template used `_wpnonce` in template versions < 3.3.0. + $nonce_value = wc_get_var( $_REQUEST['woocommerce-login-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( isset( $_POST['login'], $_POST['username'], $_POST['password'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-login' ) ) { + + try { + $creds = array( + 'user_login' => trim( wp_unslash( $_POST['username'] ) ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + 'user_password' => $_POST['password'], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + 'remember' => isset( $_POST['rememberme'] ), // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + ); + + $validation_error = new WP_Error(); + $validation_error = apply_filters( 'woocommerce_process_login_errors', $validation_error, $creds['user_login'], $creds['user_password'] ); + + if ( $validation_error->get_error_code() ) { + throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . $validation_error->get_error_message() ); + } + + if ( empty( $creds['user_login'] ) ) { + throw new Exception( '' . __( 'Error:', 'woocommerce' ) . ' ' . __( 'Username is required.', 'woocommerce' ) ); + } + + // On multisite, ensure user exists on current site, if not add them before allowing login. + if ( is_multisite() ) { + $user_data = get_user_by( is_email( $creds['user_login'] ) ? 'email' : 'login', $creds['user_login'] ); + + if ( $user_data && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) { + add_user_to_blog( get_current_blog_id(), $user_data->ID, 'customer' ); + } + } + + // Perform the login. + $user = wp_signon( apply_filters( 'woocommerce_login_credentials', $creds ), is_ssl() ); + + if ( is_wp_error( $user ) ) { + throw new Exception( $user->get_error_message() ); + } else { + + if ( ! empty( $_POST['redirect'] ) ) { + $redirect = wp_unslash( $_POST['redirect'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } elseif ( wc_get_raw_referer() ) { + $redirect = wc_get_raw_referer(); + } else { + $redirect = wc_get_page_permalink( 'myaccount' ); + } + + wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_login_redirect', remove_query_arg( 'wc_error', $redirect ), $user ), wc_get_page_permalink( 'myaccount' ) ) ); // phpcs:ignore + exit; + } + } catch ( Exception $e ) { + wc_add_notice( apply_filters( 'login_errors', $e->getMessage() ), 'error' ); + do_action( 'woocommerce_login_failed' ); + } + } + } + + /** + * Handle lost password form. + */ + public static function process_lost_password() { + if ( isset( $_POST['wc_reset_password'], $_POST['user_login'] ) ) { + $nonce_value = wc_get_var( $_REQUEST['woocommerce-lost-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'lost_password' ) ) { + return; + } + + $success = WC_Shortcode_My_Account::retrieve_password(); + + // If successful, redirect to my account with query arg set. + if ( $success ) { + wp_safe_redirect( add_query_arg( 'reset-link-sent', 'true', wc_get_account_endpoint_url( 'lost-password' ) ) ); + exit; + } + } + } + + /** + * Handle reset password form. + */ + public static function process_reset_password() { + $nonce_value = wc_get_var( $_REQUEST['woocommerce-reset-password-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( ! wp_verify_nonce( $nonce_value, 'reset_password' ) ) { + return; + } + + $posted_fields = array( 'wc_reset_password', 'password_1', 'password_2', 'reset_key', 'reset_login' ); + + foreach ( $posted_fields as $field ) { + if ( ! isset( $_POST[ $field ] ) ) { + return; + } + + if ( in_array( $field, array( 'password_1', 'password_2' ), true ) ) { + // Don't unslash password fields + // @see https://github.com/woocommerce/woocommerce/issues/23922. + $posted_fields[ $field ] = $_POST[ $field ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + } else { + $posted_fields[ $field ] = wp_unslash( $_POST[ $field ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } + } + + $user = WC_Shortcode_My_Account::check_password_reset_key( $posted_fields['reset_key'], $posted_fields['reset_login'] ); + + if ( $user instanceof WP_User ) { + if ( empty( $posted_fields['password_1'] ) ) { + wc_add_notice( __( 'Please enter your password.', 'woocommerce' ), 'error' ); + } + + if ( $posted_fields['password_1'] !== $posted_fields['password_2'] ) { + wc_add_notice( __( 'Passwords do not match.', 'woocommerce' ), 'error' ); + } + + $errors = new WP_Error(); + + do_action( 'validate_password_reset', $errors, $user ); + + wc_add_wp_error_notices( $errors ); + + if ( 0 === wc_notice_count( 'error' ) ) { + WC_Shortcode_My_Account::reset_password( $user, $posted_fields['password_1'] ); + + do_action( 'woocommerce_customer_reset_password', $user ); + + wp_safe_redirect( add_query_arg( 'password-reset', 'true', wc_get_page_permalink( 'myaccount' ) ) ); + exit; + } + } + } + + /** + * Process the registration form. + * + * @throws Exception On registration error. + */ + public static function process_registration() { + $nonce_value = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $nonce_value = isset( $_POST['woocommerce-register-nonce'] ) ? wp_unslash( $_POST['woocommerce-register-nonce'] ) : $nonce_value; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + if ( isset( $_POST['register'], $_POST['email'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-register' ) ) { + $username = 'no' === get_option( 'woocommerce_registration_generate_username' ) && isset( $_POST['username'] ) ? wp_unslash( $_POST['username'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $password = 'no' === get_option( 'woocommerce_registration_generate_password' ) && isset( $_POST['password'] ) ? $_POST['password'] : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash + $email = wp_unslash( $_POST['email'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + try { + $validation_error = new WP_Error(); + $validation_error = apply_filters( 'woocommerce_process_registration_errors', $validation_error, $username, $password, $email ); + $validation_errors = $validation_error->get_error_messages(); + + if ( 1 === count( $validation_errors ) ) { + throw new Exception( $validation_error->get_error_message() ); + } elseif ( $validation_errors ) { + foreach ( $validation_errors as $message ) { + wc_add_notice( '' . __( 'Error:', 'woocommerce' ) . ' ' . $message, 'error' ); + } + throw new Exception(); + } + + $new_customer = wc_create_new_customer( sanitize_email( $email ), wc_clean( $username ), $password ); + + if ( is_wp_error( $new_customer ) ) { + throw new Exception( $new_customer->get_error_message() ); + } + + if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) ) { + wc_add_notice( __( 'Your account was created successfully and a password has been sent to your email address.', 'woocommerce' ) ); + } else { + wc_add_notice( __( 'Your account was created successfully. Your login details have been sent to your email address.', 'woocommerce' ) ); + } + + // Only redirect after a forced login - otherwise output a success notice. + if ( apply_filters( 'woocommerce_registration_auth_new_customer', true, $new_customer ) ) { + wc_set_customer_auth_cookie( $new_customer ); + + if ( ! empty( $_POST['redirect'] ) ) { + $redirect = wp_sanitize_redirect( wp_unslash( $_POST['redirect'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } elseif ( wc_get_raw_referer() ) { + $redirect = wc_get_raw_referer(); + } else { + $redirect = wc_get_page_permalink( 'myaccount' ); + } + + wp_redirect( wp_validate_redirect( apply_filters( 'woocommerce_registration_redirect', $redirect ), wc_get_page_permalink( 'myaccount' ) ) ); //phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + exit; + } + } catch ( Exception $e ) { + if ( $e->getMessage() ) { + wc_add_notice( '' . __( 'Error:', 'woocommerce' ) . ' ' . $e->getMessage(), 'error' ); + } + } + } + } +} + +WC_Form_Handler::init(); diff --git a/includes/class-wc-frontend-scripts.php b/includes/class-wc-frontend-scripts.php new file mode 100644 index 0000000..0b23f6f --- /dev/null +++ b/includes/class-wc-frontend-scripts.php @@ -0,0 +1,621 @@ + array( + 'src' => self::get_asset_url( 'assets/css/woocommerce-layout.css' ), + 'deps' => '', + 'version' => $version, + 'media' => 'all', + 'has_rtl' => true, + ), + 'woocommerce-smallscreen' => array( + 'src' => self::get_asset_url( 'assets/css/woocommerce-smallscreen.css' ), + 'deps' => 'woocommerce-layout', + 'version' => $version, + 'media' => 'only screen and (max-width: ' . apply_filters( 'woocommerce_style_smallscreen_breakpoint', '768px' ) . ')', + 'has_rtl' => true, + ), + 'woocommerce-general' => array( + 'src' => self::get_asset_url( 'assets/css/woocommerce.css' ), + 'deps' => '', + 'version' => $version, + 'media' => 'all', + 'has_rtl' => true, + ), + ) + ); + } + + /** + * Return asset URL. + * + * @param string $path Assets path. + * @return string + */ + private static function get_asset_url( $path ) { + return apply_filters( 'woocommerce_get_asset_url', plugins_url( $path, WC_PLUGIN_FILE ), $path ); + } + + /** + * Register a script for use. + * + * @uses wp_register_script() + * @param string $handle Name of the script. Should be unique. + * @param string $path Full URL of the script, or path of the script relative to the WordPress root directory. + * @param string[] $deps An array of registered script handles this script depends on. + * @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param boolean $in_footer Whether to enqueue the script before instead of in the . Default 'false'. + */ + private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { + self::$scripts[] = $handle; + wp_register_script( $handle, $path, $deps, $version, $in_footer ); + } + + /** + * Register and enqueue a script for use. + * + * @uses wp_enqueue_script() + * @param string $handle Name of the script. Should be unique. + * @param string $path Full URL of the script, or path of the script relative to the WordPress root directory. + * @param string[] $deps An array of registered script handles this script depends on. + * @param string $version String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param boolean $in_footer Whether to enqueue the script before instead of in the . Default 'false'. + */ + private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = true ) { + if ( ! in_array( $handle, self::$scripts, true ) && $path ) { + self::register_script( $handle, $path, $deps, $version, $in_footer ); + } + wp_enqueue_script( $handle ); + } + + /** + * Register a style for use. + * + * @uses wp_register_style() + * @param string $handle Name of the stylesheet. Should be unique. + * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. + * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. + * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. + * @param boolean $has_rtl If has RTL version to load too. + */ + private static function register_style( $handle, $path, $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { + self::$styles[] = $handle; + wp_register_style( $handle, $path, $deps, $version, $media ); + + if ( $has_rtl ) { + wp_style_add_data( $handle, 'rtl', 'replace' ); + } + } + + /** + * Register and enqueue a styles for use. + * + * @uses wp_enqueue_style() + * @param string $handle Name of the stylesheet. Should be unique. + * @param string $path Full URL of the stylesheet, or path of the stylesheet relative to the WordPress root directory. + * @param string[] $deps An array of registered stylesheet handles this stylesheet depends on. + * @param string $version String specifying stylesheet version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param string $media The media for which this stylesheet has been defined. Accepts media types like 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'. + * @param boolean $has_rtl If has RTL version to load too. + */ + private static function enqueue_style( $handle, $path = '', $deps = array(), $version = WC_VERSION, $media = 'all', $has_rtl = false ) { + if ( ! in_array( $handle, self::$styles, true ) && $path ) { + self::register_style( $handle, $path, $deps, $version, $media, $has_rtl ); + } + wp_enqueue_style( $handle ); + } + + /** + * Register all WC scripts. + */ + private static function register_scripts() { + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + $register_scripts = array( + 'flexslider' => array( + 'src' => self::get_asset_url( 'assets/js/flexslider/jquery.flexslider' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '2.7.2-wc.' . $version, + ), + 'js-cookie' => array( + 'src' => self::get_asset_url( 'assets/js/js-cookie/js.cookie' . $suffix . '.js' ), + 'deps' => array(), + 'version' => '2.1.4-wc.' . $version, + ), + 'jquery-blockui' => array( + 'src' => self::get_asset_url( 'assets/js/jquery-blockui/jquery.blockUI' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '2.7.0-wc.' . $version, + ), + 'jquery-cookie' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/jquery-cookie/jquery.cookie' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '1.4.1-wc.' . $version, + ), + 'jquery-payment' => array( + 'src' => self::get_asset_url( 'assets/js/jquery-payment/jquery.payment' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '3.0.0-wc.' . $version, + ), + 'photoswipe' => array( + 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe' . $suffix . '.js' ), + 'deps' => array(), + 'version' => '4.1.1-wc.' . $version, + ), + 'photoswipe-ui-default' => array( + 'src' => self::get_asset_url( 'assets/js/photoswipe/photoswipe-ui-default' . $suffix . '.js' ), + 'deps' => array( 'photoswipe' ), + 'version' => '4.1.1-wc.' . $version, + ), + 'prettyPhoto' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '3.1.6-wc.' . $version, + ), + 'prettyPhoto-init' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/js/prettyPhoto/jquery.prettyPhoto.init' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'prettyPhoto' ), + 'version' => $version, + ), + 'select2' => array( + 'src' => self::get_asset_url( 'assets/js/select2/select2.full' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '4.0.3-wc.' . $version, + ), + 'selectWoo' => array( + 'src' => self::get_asset_url( 'assets/js/selectWoo/selectWoo.full' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '1.0.9-wc.' . $version, + ), + 'wc-address-i18n' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/address-i18n' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'wc-country-select' ), + 'version' => $version, + ), + 'wc-add-payment-method' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-payment-method' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce' ), + 'version' => $version, + ), + 'wc-cart' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/cart' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), + 'version' => $version, + ), + 'wc-cart-fragments' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/cart-fragments' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'js-cookie' ), + 'version' => $version, + ), + 'wc-checkout' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/checkout' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce', 'wc-country-select', 'wc-address-i18n' ), + 'version' => $version, + ), + 'wc-country-select' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/country-select' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => $version, + ), + 'wc-credit-card-form' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/credit-card-form' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'jquery-payment' ), + 'version' => $version, + ), + 'wc-add-to-cart' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'jquery-blockui' ), + 'version' => $version, + ), + 'wc-add-to-cart-variation' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/add-to-cart-variation' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'wp-util', 'jquery-blockui' ), + 'version' => $version, + ), + 'wc-geolocation' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/geolocation' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => $version, + ), + 'wc-lost-password' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/lost-password' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'woocommerce' ), + 'version' => $version, + ), + 'wc-password-strength-meter' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/password-strength-meter' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'password-strength-meter' ), + 'version' => $version, + ), + 'wc-single-product' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/single-product' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => $version, + ), + 'woocommerce' => array( + 'src' => self::get_asset_url( 'assets/js/frontend/woocommerce' . $suffix . '.js' ), + 'deps' => array( 'jquery', 'jquery-blockui', 'js-cookie' ), + 'version' => $version, + ), + 'zoom' => array( + 'src' => self::get_asset_url( 'assets/js/zoom/jquery.zoom' . $suffix . '.js' ), + 'deps' => array( 'jquery' ), + 'version' => '1.7.21-wc.' . $version, + ), + ); + foreach ( $register_scripts as $name => $props ) { + self::register_script( $name, $props['src'], $props['deps'], $props['version'] ); + } + } + + /** + * Register all WC styles. + */ + private static function register_styles() { + $version = Constants::get_constant( 'WC_VERSION' ); + + $register_styles = array( + 'photoswipe' => array( + 'src' => self::get_asset_url( 'assets/css/photoswipe/photoswipe.min.css' ), + 'deps' => array(), + 'version' => $version, + 'has_rtl' => false, + ), + 'photoswipe-default-skin' => array( + 'src' => self::get_asset_url( 'assets/css/photoswipe/default-skin/default-skin.min.css' ), + 'deps' => array( 'photoswipe' ), + 'version' => $version, + 'has_rtl' => false, + ), + 'select2' => array( + 'src' => self::get_asset_url( 'assets/css/select2.css' ), + 'deps' => array(), + 'version' => $version, + 'has_rtl' => false, + ), + 'woocommerce_prettyPhoto_css' => array( // deprecated. + 'src' => self::get_asset_url( 'assets/css/prettyPhoto.css' ), + 'deps' => array(), + 'version' => $version, + 'has_rtl' => true, + ), + ); + foreach ( $register_styles as $name => $props ) { + self::register_style( $name, $props['src'], $props['deps'], $props['version'], 'all', $props['has_rtl'] ); + } + } + + /** + * Register/queue frontend scripts. + */ + public static function load_scripts() { + global $post; + + if ( ! did_action( 'before_woocommerce_init' ) ) { + return; + } + + self::register_scripts(); + self::register_styles(); + + if ( 'yes' === get_option( 'woocommerce_enable_ajax_add_to_cart' ) ) { + self::enqueue_script( 'wc-add-to-cart' ); + } + if ( is_cart() ) { + self::enqueue_script( 'wc-cart' ); + } + if ( is_cart() || is_checkout() || is_account_page() ) { + self::enqueue_script( 'selectWoo' ); + self::enqueue_style( 'select2' ); + + // Password strength meter. Load in checkout, account login and edit account page. + if ( ( 'no' === get_option( 'woocommerce_registration_generate_password' ) && ! is_user_logged_in() ) || is_edit_account_page() || is_lost_password_page() ) { + self::enqueue_script( 'wc-password-strength-meter' ); + } + } + if ( is_checkout() ) { + self::enqueue_script( 'wc-checkout' ); + } + if ( is_add_payment_method_page() ) { + self::enqueue_script( 'wc-add-payment-method' ); + } + if ( is_lost_password_page() ) { + self::enqueue_script( 'wc-lost-password' ); + } + + // Load gallery scripts on product pages only if supported. + if ( is_product() || ( ! empty( $post->post_content ) && strstr( $post->post_content, '[product_page' ) ) ) { + if ( current_theme_supports( 'wc-product-gallery-zoom' ) ) { + self::enqueue_script( 'zoom' ); + } + if ( current_theme_supports( 'wc-product-gallery-slider' ) ) { + self::enqueue_script( 'flexslider' ); + } + if ( current_theme_supports( 'wc-product-gallery-lightbox' ) ) { + self::enqueue_script( 'photoswipe-ui-default' ); + self::enqueue_style( 'photoswipe-default-skin' ); + add_action( 'wp_footer', 'woocommerce_photoswipe' ); + } + self::enqueue_script( 'wc-single-product' ); + } + + // Only enqueue the geolocation script if the Default Current Address is set to "Geolocate + // (with Page Caching Support) and outside of the cart, checkout, account and customizer preview. + if ( + 'geolocation_ajax' === get_option( 'woocommerce_default_customer_address' ) + && ! ( is_cart() || is_account_page() || is_checkout() || is_customize_preview() ) + ) { + $ua = strtolower( wc_get_user_agent() ); // Exclude common bots from geolocation by user agent. + + if ( ! strstr( $ua, 'bot' ) && ! strstr( $ua, 'spider' ) && ! strstr( $ua, 'crawl' ) ) { + self::enqueue_script( 'wc-geolocation' ); + } + } + + // Global frontend scripts. + self::enqueue_script( 'woocommerce' ); + self::enqueue_script( 'wc-cart-fragments' ); + + // CSS Styles. + $enqueue_styles = self::get_styles(); + if ( $enqueue_styles ) { + foreach ( $enqueue_styles as $handle => $args ) { + if ( ! isset( $args['has_rtl'] ) ) { + $args['has_rtl'] = false; + } + + self::enqueue_style( $handle, $args['src'], $args['deps'], $args['version'], $args['media'], $args['has_rtl'] ); + } + } + + // Placeholder style. + wp_register_style( 'woocommerce-inline', false ); // phpcs:ignore + wp_enqueue_style( 'woocommerce-inline' ); + + if ( true === wc_string_to_bool( get_option( 'woocommerce_checkout_highlight_required_fields', 'yes' ) ) ) { + wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: visible; }' ); + } else { + wp_add_inline_style( 'woocommerce-inline', '.woocommerce form .form-row .required { visibility: hidden; }' ); + } + } + + /** + * Localize a WC script once. + * + * @since 2.3.0 this needs less wp_script_is() calls due to https://core.trac.wordpress.org/ticket/28404 being added in WP 4.0. + * @param string $handle Script handle the data will be attached to. + */ + private static function localize_script( $handle ) { + if ( ! in_array( $handle, self::$wp_localize_scripts, true ) && wp_script_is( $handle ) ) { + $data = self::get_script_data( $handle ); + + if ( ! $data ) { + return; + } + + $name = str_replace( '-', '_', $handle ) . '_params'; + self::$wp_localize_scripts[] = $handle; + wp_localize_script( $handle, $name, apply_filters( $name, $data ) ); + } + } + + /** + * Return data for script handles. + * + * @param string $handle Script handle the data will be attached to. + * @return array|bool + */ + private static function get_script_data( $handle ) { + global $wp; + + switch ( $handle ) { + case 'woocommerce': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + ); + break; + case 'wc-geolocation': + $params = array( + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'home_url' => remove_query_arg( 'lang', home_url() ), // FIX for WPML compatibility. + ); + break; + case 'wc-single-product': + $params = array( + 'i18n_required_rating_text' => esc_attr__( 'Please select a rating', 'woocommerce' ), + 'review_rating_required' => wc_review_ratings_required() ? 'yes' : 'no', + 'flexslider' => apply_filters( + 'woocommerce_single_product_carousel_options', + array( + 'rtl' => is_rtl(), + 'animation' => 'slide', + 'smoothHeight' => true, + 'directionNav' => false, + 'controlNav' => 'thumbnails', + 'slideshow' => false, + 'animationSpeed' => 500, + 'animationLoop' => false, // Breaks photoswipe pagination if true. + 'allowOneSlide' => false, + ) + ), + 'zoom_enabled' => apply_filters( 'woocommerce_single_product_zoom_enabled', get_theme_support( 'wc-product-gallery-zoom' ) ), + 'zoom_options' => apply_filters( 'woocommerce_single_product_zoom_options', array() ), + 'photoswipe_enabled' => apply_filters( 'woocommerce_single_product_photoswipe_enabled', get_theme_support( 'wc-product-gallery-lightbox' ) ), + 'photoswipe_options' => apply_filters( + 'woocommerce_single_product_photoswipe_options', + array( + 'shareEl' => false, + 'closeOnScroll' => false, + 'history' => false, + 'hideAnimationDuration' => 0, + 'showAnimationDuration' => 0, + ) + ), + 'flexslider_enabled' => apply_filters( 'woocommerce_single_product_flexslider_enabled', get_theme_support( 'wc-product-gallery-slider' ) ), + ); + break; + case 'wc-checkout': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'update_order_review_nonce' => wp_create_nonce( 'update-order-review' ), + 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), + 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), + 'option_guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), + 'checkout_url' => WC_AJAX::get_endpoint( 'checkout' ), + 'is_checkout' => is_checkout() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) ? 1 : 0, + 'debug_mode' => Constants::is_true( 'WP_DEBUG' ), + 'i18n_checkout_error' => esc_attr__( 'Error processing checkout. Please try again.', 'woocommerce' ), + ); + break; + case 'wc-address-i18n': + $params = array( + 'locale' => wp_json_encode( WC()->countries->get_country_locale() ), + 'locale_fields' => wp_json_encode( WC()->countries->get_country_locale_field_selectors() ), + 'i18n_required_text' => esc_attr__( 'required', 'woocommerce' ), + 'i18n_optional_text' => esc_html__( 'optional', 'woocommerce' ), + ); + break; + case 'wc-cart': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'update_shipping_method_nonce' => wp_create_nonce( 'update-shipping-method' ), + 'apply_coupon_nonce' => wp_create_nonce( 'apply-coupon' ), + 'remove_coupon_nonce' => wp_create_nonce( 'remove-coupon' ), + ); + break; + case 'wc-cart-fragments': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'cart_hash_key' => apply_filters( 'woocommerce_cart_hash_key', 'wc_cart_hash_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ), + 'fragment_name' => apply_filters( 'woocommerce_cart_fragment_name', 'wc_fragments_' . md5( get_current_blog_id() . '_' . get_site_url( get_current_blog_id(), '/' ) . get_template() ) ), + 'request_timeout' => 5000, + ); + break; + case 'wc-add-to-cart': + $params = array( + 'ajax_url' => WC()->ajax_url(), + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'i18n_view_cart' => esc_attr__( 'View cart', 'woocommerce' ), + 'cart_url' => apply_filters( 'woocommerce_add_to_cart_redirect', wc_get_cart_url(), null ), + 'is_cart' => is_cart(), + 'cart_redirect_after_add' => get_option( 'woocommerce_cart_redirect_after_add' ), + ); + break; + case 'wc-add-to-cart-variation': + // We also need the wp.template for this script :). + wc_get_template( 'single-product/add-to-cart/variation.php' ); + + $params = array( + 'wc_ajax_url' => WC_AJAX::get_endpoint( '%%endpoint%%' ), + 'i18n_no_matching_variations_text' => esc_attr__( 'Sorry, no products matched your selection. Please choose a different combination.', 'woocommerce' ), + 'i18n_make_a_selection_text' => esc_attr__( 'Please select some product options before adding this product to your cart.', 'woocommerce' ), + 'i18n_unavailable_text' => esc_attr__( 'Sorry, this product is unavailable. Please choose a different combination.', 'woocommerce' ), + ); + break; + case 'wc-country-select': + $params = array( + 'countries' => wp_json_encode( array_merge( WC()->countries->get_allowed_country_states(), WC()->countries->get_shipping_country_states() ) ), + 'i18n_select_state_text' => esc_attr__( 'Select an option…', 'woocommerce' ), + 'i18n_no_matches' => _x( 'No matches found', 'enhanced select', 'woocommerce' ), + 'i18n_ajax_error' => _x( 'Loading failed', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_1' => _x( 'Please enter 1 or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_short_n' => _x( 'Please enter %qty% or more characters', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_1' => _x( 'Please delete 1 character', 'enhanced select', 'woocommerce' ), + 'i18n_input_too_long_n' => _x( 'Please delete %qty% characters', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_1' => _x( 'You can only select 1 item', 'enhanced select', 'woocommerce' ), + 'i18n_selection_too_long_n' => _x( 'You can only select %qty% items', 'enhanced select', 'woocommerce' ), + 'i18n_load_more' => _x( 'Loading more results…', 'enhanced select', 'woocommerce' ), + 'i18n_searching' => _x( 'Searching…', 'enhanced select', 'woocommerce' ), + ); + break; + case 'wc-password-strength-meter': + $params = array( + 'min_password_strength' => apply_filters( 'woocommerce_min_password_strength', 3 ), + 'stop_checkout' => apply_filters( 'woocommerce_enforce_password_strength_meter_on_checkout', false ), + 'i18n_password_error' => esc_attr__( 'Please enter a stronger password.', 'woocommerce' ), + 'i18n_password_hint' => esc_attr( wp_get_password_hint() ), + ); + break; + default: + $params = false; + } + + $params = apply_filters_deprecated( $handle . '_params', array( $params ), '3.0.0', 'woocommerce_get_script_data' ); + + return apply_filters( 'woocommerce_get_script_data', $params, $handle ); + } + + /** + * Localize scripts only when enqueued. + */ + public static function localize_printed_scripts() { + foreach ( self::$scripts as $handle ) { + self::localize_script( $handle ); + } + } +} + +WC_Frontend_Scripts::init(); diff --git a/includes/class-wc-geo-ip.php b/includes/class-wc-geo-ip.php new file mode 100644 index 0000000..c79be25 --- /dev/null +++ b/includes/class-wc-geo-ip.php @@ -0,0 +1,1814 @@ +log( $level, $message, array( 'source' => 'geoip' ) ); + } + + /** + * Open geoip file. + * + * @param string $filename + * @param int $flags + */ + public function geoip_open( $filename, $flags ) { + $this->flags = $flags; + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $this->shmid = @shmop_open( self::GEOIP_SHM_KEY, 'a', 0, 0 ); + } else { + if ( $this->filehandle = fopen( $filename, 'rb' ) ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $s_array = fstat( $this->filehandle ); + $this->memory_buffer = fread( $this->filehandle, $s_array['size'] ); + } + } else { + $this->log( 'GeoIP API: Can not open ' . $filename, 'error' ); + } + } + + $this->_setup_segments(); + } + + /** + * Setup segments. + * + * @return WC_Geo_IP instance + */ + private function _setup_segments() { + $this->databaseType = self::GEOIP_COUNTRY_EDITION; + $this->record_length = self::STANDARD_RECORD_LENGTH; + + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $offset = @shmop_size( $this->shmid ) - 3; + + for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { + $delim = @shmop_read( $this->shmid, $offset, 3 ); + $offset += 3; + + if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { + $this->databaseType = ord( @shmop_read( $this->shmid, $offset, 1 ) ); + + if ( $this->databaseType >= 106 ) { + $this->databaseType -= 105; + } + + $offset++; + + if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; + } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; + } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) + || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) + ) { + $this->databaseSegments = 0; + $buf = @shmop_read( $this->shmid, $offset, self::SEGMENT_RECORD_LENGTH ); + + for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { + $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); + } + + if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + ) { + $this->record_length = self::ORG_RECORD_LENGTH; + } + } + + break; + } else { + $offset -= 4; + } + } + if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) + || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) + ) { + $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; + } + } else { + $filepos = ftell( $this->filehandle ); + fseek( $this->filehandle, -3, SEEK_END ); + + for ( $i = 0; $i < self::STRUCTURE_INFO_MAX_SIZE; $i++ ) { + + $delim = fread( $this->filehandle, 3 ); + if ( ( chr( 255 ) . chr( 255 ) . chr( 255 ) ) == $delim ) { + + $this->databaseType = ord( fread( $this->filehandle, 1 ) ); + if ( $this->databaseType >= 106 ) { + $this->databaseType -= 105; + } + + if ( self::GEOIP_REGION_EDITION_REV0 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV0; + } elseif ( self::GEOIP_REGION_EDITION_REV1 == $this->databaseType ) { + $this->databaseSegments = self::GEOIP_STATE_BEGIN_REV1; + } elseif ( ( self::GEOIP_CITY_EDITION_REV0 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV0_V6 == $this->databaseType ) + || ( self::GEOIP_CITY_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_LOCATIONA_EDITION == $this->databaseType ) + || ( self::GEOIP_ACCURACYRADIUS_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1 == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION_REV1_V6 == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION == $this->databaseType ) + || ( self::GEOIP_USERTYPE_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION == $this->databaseType ) + || ( self::GEOIP_ASNUM_EDITION_V6 == $this->databaseType ) + ) { + $this->databaseSegments = 0; + $buf = fread( $this->filehandle, self::SEGMENT_RECORD_LENGTH ); + + for ( $j = 0; $j < self::SEGMENT_RECORD_LENGTH; $j++ ) { + $this->databaseSegments += ( ord( $buf[ $j ] ) << ( $j * 8 ) ); + } + + if ( ( self::GEOIP_ORG_EDITION == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION == $this->databaseType ) + || ( self::GEOIP_ORG_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_DOMAIN_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_ISP_EDITION_V6 == $this->databaseType ) + ) { + $this->record_length = self::ORG_RECORD_LENGTH; + } + } + + break; + } else { + fseek( $this->filehandle, -4, SEEK_CUR ); + } + } + + if ( ( self::GEOIP_COUNTRY_EDITION == $this->databaseType ) + || ( self::GEOIP_COUNTRY_EDITION_V6 == $this->databaseType ) + || ( self::GEOIP_PROXY_EDITION == $this->databaseType ) + || ( self::GEOIP_NETSPEED_EDITION == $this->databaseType ) + ) { + $this->databaseSegments = self::GEOIP_COUNTRY_BEGIN; + } + + fseek( $this->filehandle, $filepos, SEEK_SET ); + } + + return $this; + } + + /** + * Close geoip file. + * + * @return bool + */ + public function geoip_close() { + if ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + return true; + } + + return fclose( $this->filehandle ); + } + + /** + * Common get record. + * + * @param string $seek_country + * @return WC_Geo_IP_Record instance + */ + private function _common_get_record( $seek_country ) { + // workaround php's broken substr, strpos, etc handling with + // mbstring.func_overload and mbstring.internal_encoding + $mbExists = extension_loaded( 'mbstring' ); + if ( $mbExists ) { + $enc = mb_internal_encoding(); + mb_internal_encoding( 'ISO-8859-1' ); + } + + $record_pointer = $seek_country + ( 2 * $this->record_length - 1 ) * $this->databaseSegments; + + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $record_buf = substr( $this->memory_buffer, $record_pointer, FULL_RECORD_LENGTH ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $record_buf = @shmop_read( $this->shmid, $record_pointer, FULL_RECORD_LENGTH ); + } else { + fseek( $this->filehandle, $record_pointer, SEEK_SET ); + $record_buf = fread( $this->filehandle, FULL_RECORD_LENGTH ); + } + + $record = new WC_Geo_IP_Record(); + $record_buf_pos = 0; + $char = ord( substr( $record_buf, $record_buf_pos, 1 ) ); + $record->country_code = $this->GEOIP_COUNTRY_CODES[ $char ]; + $record->country_code3 = $this->GEOIP_COUNTRY_CODES3[ $char ]; + $record->country_name = $this->GEOIP_COUNTRY_NAMES[ $char ]; + $record->continent_code = $this->GEOIP_CONTINENT_CODES[ $char ]; + $str_length = 0; + + $record_buf_pos++; + + // Get region + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->region = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + $str_length = 0; + + // Get city + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->city = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + $str_length = 0; + + // Get postal code + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + while ( 0 != $char ) { + $str_length++; + $char = ord( substr( $record_buf, $record_buf_pos + $str_length, 1 ) ); + } + + if ( $str_length > 0 ) { + $record->postal_code = substr( $record_buf, $record_buf_pos, $str_length ); + } + + $record_buf_pos += $str_length + 1; + + // Get latitude and longitude + $latitude = 0; + $longitude = 0; + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $latitude += ( $char << ( $j * 8 ) ); + } + + $record->latitude = ( $latitude / 10000 ) - 180; + + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $longitude += ( $char << ( $j * 8 ) ); + } + + $record->longitude = ( $longitude / 10000 ) - 180; + + if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { + $metroarea_combo = 0; + if ( 'US' === $record->country_code ) { + for ( $j = 0; $j < 3; ++$j ) { + $char = ord( substr( $record_buf, $record_buf_pos++, 1 ) ); + $metroarea_combo += ( $char << ( $j * 8 ) ); + } + + $record->metro_code = $record->dma_code = floor( $metroarea_combo / 1000 ); + $record->area_code = $metroarea_combo % 1000; + } + } + + if ( $mbExists ) { + mb_internal_encoding( $enc ); + } + + return $record; + } + + /** + * Get record. + * + * @param int $ipnum + * @return WC_Geo_IP_Record instance + */ + private function _get_record( $ipnum ) { + $seek_country = $this->_geoip_seek_country( $ipnum ); + if ( $seek_country == $this->databaseSegments ) { + return null; + } + + return $this->_common_get_record( $seek_country ); + } + + /** + * Seek country IPv6. + * + * @param int $ipnum + * @return string + */ + public function _geoip_seek_country_v6( $ipnum ) { + // arrays from unpack start with offset 1 + // yet another php mystery. array_merge work around + // this broken behaviour + $v6vec = array_merge( unpack( 'C16', $ipnum ) ); + + $offset = 0; + for ( $depth = 127; $depth >= 0; --$depth ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $buf = $this->_safe_substr( + $this->memory_buffer, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $buf = @shmop_read( + $this->shmid, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } else { + if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { + break; + } + + $buf = fread( $this->filehandle, 2 * $this->record_length ); + } + $x = array( 0, 0 ); + for ( $i = 0; $i < 2; ++$i ) { + for ( $j = 0; $j < $this->record_length; ++$j ) { + $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); + } + } + + $bnum = 127 - $depth; + $idx = $bnum >> 3; + $b_mask = 1 << ( $bnum & 7 ^ 7 ); + if ( ( $v6vec[ $idx ] & $b_mask ) > 0 ) { + if ( $x[1] >= $this->databaseSegments ) { + return $x[1]; + } + $offset = $x[1]; + } else { + if ( $x[0] >= $this->databaseSegments ) { + return $x[0]; + } + $offset = $x[0]; + } + } + + $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); + + return false; + } + + /** + * Seek country. + * + * @param int $ipnum + * @return string + */ + private function _geoip_seek_country( $ipnum ) { + $offset = 0; + for ( $depth = 31; $depth >= 0; --$depth ) { + if ( $this->flags & self::GEOIP_MEMORY_CACHE ) { + $buf = $this->_safe_substr( + $this->memory_buffer, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } elseif ( $this->flags & self::GEOIP_SHARED_MEMORY ) { + $buf = @shmop_read( + $this->shmid, + 2 * $this->record_length * $offset, + 2 * $this->record_length + ); + } else { + if ( 0 != fseek( $this->filehandle, 2 * $this->record_length * $offset, SEEK_SET ) ) { + break; + } + + $buf = fread( $this->filehandle, 2 * $this->record_length ); + } + + $x = array( 0, 0 ); + for ( $i = 0; $i < 2; ++$i ) { + for ( $j = 0; $j < $this->record_length; ++$j ) { + $x[ $i ] += ord( $buf[ $this->record_length * $i + $j ] ) << ( $j * 8 ); + } + } + if ( $ipnum & ( 1 << $depth ) ) { + if ( $x[1] >= $this->databaseSegments ) { + return $x[1]; + } + + $offset = $x[1]; + } else { + if ( $x[0] >= $this->databaseSegments ) { + return $x[0]; + } + + $offset = $x[0]; + } + } + + $this->log( 'GeoIP API: Error traversing database - perhaps it is corrupt?', 'error' ); + + return false; + } + + /** + * Record by addr. + * + * @param string $addr + * + * @return WC_Geo_IP_Record + */ + public function geoip_record_by_addr( $addr ) { + if ( null == $addr ) { + return 0; + } + + $ipnum = ip2long( $addr ); + return $this->_get_record( $ipnum ); + } + + /** + * Country ID by addr IPv6. + * + * @param string $addr + * @return int|bool + */ + public function geoip_country_id_by_addr_v6( $addr ) { + if ( ! defined( 'AF_INET6' ) ) { + $this->log( 'GEOIP (geoip_country_id_by_addr_v6): PHP was compiled with --disable-ipv6 option' ); + return false; + } + $ipnum = inet_pton( $addr ); + return $this->_geoip_seek_country_v6( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; + } + + /** + * Country ID by addr. + * + * @param string $addr + * @return int + */ + public function geoip_country_id_by_addr( $addr ) { + $ipnum = ip2long( $addr ); + return $this->_geoip_seek_country( $ipnum ) - self::GEOIP_COUNTRY_BEGIN; + } + + /** + * Country code by addr IPv6. + * + * @param string $addr + * @return string + */ + public function geoip_country_code_by_addr_v6( $addr ) { + $country_id = $this->geoip_country_id_by_addr_v6( $addr ); + if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { + return $this->GEOIP_COUNTRY_CODES[ $country_id ]; + } + + return false; + } + + /** + * Country code by addr. + * + * @param string $addr + * @return string + */ + public function geoip_country_code_by_addr( $addr ) { + if ( self::GEOIP_CITY_EDITION_REV1 == $this->databaseType ) { + $record = $this->geoip_record_by_addr( $addr ); + if ( false !== $record ) { + return $record->country_code; + } + } else { + $country_id = $this->geoip_country_id_by_addr( $addr ); + if ( false !== $country_id && isset( $this->GEOIP_COUNTRY_CODES[ $country_id ] ) ) { + return $this->GEOIP_COUNTRY_CODES[ $country_id ]; + } + } + + return false; + } + + /** + * Encode string. + * + * @param string $string + * @param int $start + * @param int $length + * @return string + */ + private function _safe_substr( $string, $start, $length ) { + // workaround php's broken substr, strpos, etc handling with + // mbstring.func_overload and mbstring.internal_encoding + $mb_exists = extension_loaded( 'mbstring' ); + + if ( $mb_exists ) { + $enc = mb_internal_encoding(); + mb_internal_encoding( 'ISO-8859-1' ); + } + + $buf = substr( $string, $start, $length ); + + if ( $mb_exists ) { + mb_internal_encoding( $enc ); + } + + return $buf; + } +} + +/** + * Geo IP Record class. + */ +class WC_Geo_IP_Record { + + /** + * Country code. + * + * @var string + */ + public $country_code; + + /** + * 3 letters country code. + * + * @var string + */ + public $country_code3; + + /** + * Country name. + * + * @var string + */ + public $country_name; + + /** + * Region. + * + * @var string + */ + public $region; + + /** + * City. + * + * @var string + */ + public $city; + + /** + * Postal code. + * + * @var string + */ + public $postal_code; + + /** + * Latitude + * + * @var int + */ + public $latitude; + + /** + * Longitude. + * + * @var int + */ + public $longitude; + + /** + * Area code. + * + * @var int + */ + public $area_code; + + /** + * DMA Code. + * + * Metro and DMA code are the same. + * Use metro code instead. + * + * @var float + */ + public $dma_code; + + /** + * Metro code. + * + * @var float + */ + public $metro_code; + + /** + * Continent code. + * + * @var string + */ + public $continent_code; +} diff --git a/includes/class-wc-geolite-integration.php b/includes/class-wc-geolite-integration.php new file mode 100644 index 0000000..c661aa0 --- /dev/null +++ b/includes/class-wc-geolite-integration.php @@ -0,0 +1,92 @@ +database = $database; + } + + /** + * Get country 2-letters ISO by IP address. + * Returns empty string when not able to find any ISO code. + * + * @param string $ip_address User IP address. + * @return string + * @deprecated 3.9.0 + */ + public function get_country_iso( $ip_address ) { + wc_deprecated_function( 'get_country_iso', '3.9.0' ); + + $iso_code = ''; + + try { + $reader = new MaxMind\Db\Reader( $this->database ); // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound + $data = $reader->get( $ip_address ); + + if ( isset( $data['country']['iso_code'] ) ) { + $iso_code = $data['country']['iso_code']; + } + + $reader->close(); + } catch ( Exception $e ) { + $this->log( $e->getMessage(), 'warning' ); + } + + return sanitize_text_field( strtoupper( $iso_code ) ); + } + + /** + * Logging method. + * + * @param string $message Log message. + * @param string $level Log level. + * Available options: 'emergency', 'alert', + * 'critical', 'error', 'warning', 'notice', + * 'info' and 'debug'. + * Defaults to 'info'. + */ + private function log( $message, $level = 'info' ) { + if ( is_null( $this->log ) ) { + $this->log = wc_get_logger(); + } + + $this->log->log( $level, $message, array( 'source' => 'geoip' ) ); + } +} diff --git a/includes/class-wc-geolocation.php b/includes/class-wc-geolocation.php new file mode 100644 index 0000000..09c76e9 --- /dev/null +++ b/includes/class-wc-geolocation.php @@ -0,0 +1,356 @@ + 'http://api.ipify.org/', + 'ipecho' => 'http://ipecho.net/plain', + 'ident' => 'http://ident.me', + 'whatismyipaddress' => 'http://bot.whatismyipaddress.com', + ); + + /** + * API endpoints for geolocating an IP address + * + * @var array + */ + private static $geoip_apis = array( + 'ipinfo.io' => 'https://ipinfo.io/%s/json', + 'ip-api.com' => 'http://ip-api.com/json/%s', + ); + + /** + * Check if geolocation is enabled. + * + * @since 3.4.0 + * @param string $current_settings Current geolocation settings. + * @return bool + */ + private static function is_geolocation_enabled( $current_settings ) { + return in_array( $current_settings, array( 'geolocation', 'geolocation_ajax' ), true ); + } + + /** + * Get current user IP Address. + * + * @return string + */ + public static function get_ip_address() { + if ( isset( $_SERVER['HTTP_X_REAL_IP'] ) ) { + return sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REAL_IP'] ) ); + } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { + // Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2 + // Make sure we always only send through the first IP in the list which should always be the client IP. + return (string) rest_is_ip_address( trim( current( preg_split( '/,/', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) ); + } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) { + return sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); + } + return ''; + } + + /** + * Get user IP Address using an external service. + * This can be used as a fallback for users on localhost where + * get_ip_address() will be a local IP and non-geolocatable. + * + * @return string + */ + public static function get_external_ip_address() { + $external_ip_address = '0.0.0.0'; + + if ( '' !== self::get_ip_address() ) { + $transient_name = 'external_ip_address_' . self::get_ip_address(); + $external_ip_address = get_transient( $transient_name ); + } + + if ( false === $external_ip_address ) { + $external_ip_address = '0.0.0.0'; + $ip_lookup_services = apply_filters( 'woocommerce_geolocation_ip_lookup_apis', self::$ip_lookup_apis ); + $ip_lookup_services_keys = array_keys( $ip_lookup_services ); + shuffle( $ip_lookup_services_keys ); + + foreach ( $ip_lookup_services_keys as $service_name ) { + $service_endpoint = $ip_lookup_services[ $service_name ]; + $response = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) ); + + if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) { + $external_ip_address = apply_filters( 'woocommerce_geolocation_ip_lookup_api_response', wc_clean( $response['body'] ), $service_name ); + break; + } + } + + set_transient( $transient_name, $external_ip_address, DAY_IN_SECONDS ); + } + + return $external_ip_address; + } + + /** + * Geolocate an IP address. + * + * @param string $ip_address IP Address. + * @param bool $fallback If true, fallbacks to alternative IP detection (can be slower). + * @param bool $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower). + * @return array + */ + public static function geolocate_ip( $ip_address = '', $fallback = false, $api_fallback = true ) { + // Filter to allow custom geolocation of the IP address. + $country_code = apply_filters( 'woocommerce_geolocate_ip', false, $ip_address, $fallback, $api_fallback ); + + if ( false !== $country_code ) { + return array( + 'country' => $country_code, + 'state' => '', + 'city' => '', + 'postcode' => '', + ); + } + + if ( empty( $ip_address ) ) { + $ip_address = self::get_ip_address(); + } + + $country_code = self::get_country_code_from_headers(); + + /** + * Get geolocation filter. + * + * @since 3.9.0 + * @param array $geolocation Geolocation data, including country, state, city, and postcode. + * @param string $ip_address IP Address. + */ + $geolocation = apply_filters( + 'woocommerce_get_geolocation', + array( + 'country' => $country_code, + 'state' => '', + 'city' => '', + 'postcode' => '', + ), + $ip_address + ); + + // If we still haven't found a country code, let's consider doing an API lookup. + if ( '' === $geolocation['country'] && $api_fallback ) { + $geolocation['country'] = self::geolocate_via_api( $ip_address ); + } + + // It's possible that we're in a local environment, in which case the geolocation needs to be done from the + // external address. + if ( '' === $geolocation['country'] && $fallback ) { + $external_ip_address = self::get_external_ip_address(); + + // Only bother with this if the external IP differs. + if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) { + return self::geolocate_ip( $external_ip_address, false, $api_fallback ); + } + } + + return array( + 'country' => $geolocation['country'], + 'state' => $geolocation['state'], + 'city' => $geolocation['city'], + 'postcode' => $geolocation['postcode'], + ); + } + + /** + * Path to our local db. + * + * @deprecated 3.9.0 + * @param string $deprecated Deprecated since 3.4.0. + * @return string + */ + public static function get_local_database_path( $deprecated = '2' ) { + wc_deprecated_function( 'WC_Geolocation::get_local_database_path', '3.9.0' ); + $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); + return $integration->get_database_service()->get_database_path(); + } + + /** + * Update geoip database. + * + * @deprecated 3.9.0 + * Extract files with PharData. Tool built into PHP since 5.3. + */ + public static function update_database() { + wc_deprecated_function( 'WC_Geolocation::update_database', '3.9.0' ); + $integration = wc()->integrations->get_integration( 'maxmind_geolocation' ); + $integration->update_database(); + } + + /** + * Fetches the country code from the request headers, if one is available. + * + * @since 3.9.0 + * @return string The country code pulled from the headers, or empty string if one was not found. + */ + private static function get_country_code_from_headers() { + $country_code = ''; + + $headers = array( + 'MM_COUNTRY_CODE', + 'GEOIP_COUNTRY_CODE', + 'HTTP_CF_IPCOUNTRY', + 'HTTP_X_COUNTRY_CODE', + ); + + foreach ( $headers as $header ) { + if ( empty( $_SERVER[ $header ] ) ) { + continue; + } + + $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) ); + break; + } + + return $country_code; + } + + /** + * Use APIs to Geolocate the user. + * + * Geolocation APIs can be added through the use of the woocommerce_geolocation_geoip_apis filter. + * Provide a name=>value pair for service-slug=>endpoint. + * + * If APIs are defined, one will be chosen at random to fulfil the request. After completing, the result + * will be cached in a transient. + * + * @param string $ip_address IP address. + * @return string + */ + private static function geolocate_via_api( $ip_address ) { + $country_code = get_transient( 'geoip_' . $ip_address ); + + if ( false === $country_code ) { + $geoip_services = apply_filters( 'woocommerce_geolocation_geoip_apis', self::$geoip_apis ); + + if ( empty( $geoip_services ) ) { + return ''; + } + + $geoip_services_keys = array_keys( $geoip_services ); + + shuffle( $geoip_services_keys ); + + foreach ( $geoip_services_keys as $service_name ) { + $service_endpoint = $geoip_services[ $service_name ]; + $response = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) ); + + if ( ! is_wp_error( $response ) && $response['body'] ) { + switch ( $service_name ) { + case 'ipinfo.io': + $data = json_decode( $response['body'] ); + $country_code = isset( $data->country ) ? $data->country : ''; + break; + case 'ip-api.com': + $data = json_decode( $response['body'] ); + $country_code = isset( $data->countryCode ) ? $data->countryCode : ''; // @codingStandardsIgnoreLine + break; + default: + $country_code = apply_filters( 'woocommerce_geolocation_geoip_response_' . $service_name, '', $response['body'] ); + break; + } + + $country_code = sanitize_text_field( strtoupper( $country_code ) ); + + if ( $country_code ) { + break; + } + } + } + + set_transient( 'geoip_' . $ip_address, $country_code, DAY_IN_SECONDS ); + } + + return $country_code; + } + + /** + * Hook in geolocation functionality. + * + * @deprecated 3.9.0 + * @return null + */ + public static function init() { + wc_deprecated_function( 'WC_Geolocation::init', '3.9.0' ); + return null; + } + + /** + * Prevent geolocation via MaxMind when using legacy versions of php. + * + * @deprecated 3.9.0 + * @since 3.4.0 + * @param string $default_customer_address current value. + * @return string + */ + public static function disable_geolocation_on_legacy_php( $default_customer_address ) { + wc_deprecated_function( 'WC_Geolocation::disable_geolocation_on_legacy_php', '3.9.0' ); + + if ( self::is_geolocation_enabled( $default_customer_address ) ) { + $default_customer_address = 'base'; + } + + return $default_customer_address; + } + + /** + * Maybe trigger a DB update for the first time. + * + * @deprecated 3.9.0 + * @param string $new_value New value. + * @param string $old_value Old value. + * @return string + */ + public static function maybe_update_database( $new_value, $old_value ) { + wc_deprecated_function( 'WC_Geolocation::maybe_update_database', '3.9.0' ); + if ( $new_value !== $old_value && self::is_geolocation_enabled( $new_value ) ) { + self::update_database(); + } + + return $new_value; + } +} diff --git a/includes/class-wc-https.php b/includes/class-wc-https.php new file mode 100644 index 0000000..84e3aed --- /dev/null +++ b/includes/class-wc-https.php @@ -0,0 +1,138 @@ + array( + 'wc_update_200_file_paths', + 'wc_update_200_permalinks', + 'wc_update_200_subcat_display', + 'wc_update_200_taxrates', + 'wc_update_200_line_items', + 'wc_update_200_images', + 'wc_update_200_db_version', + ), + '2.0.9' => array( + 'wc_update_209_brazillian_state', + 'wc_update_209_db_version', + ), + '2.1.0' => array( + 'wc_update_210_remove_pages', + 'wc_update_210_file_paths', + 'wc_update_210_db_version', + ), + '2.2.0' => array( + 'wc_update_220_shipping', + 'wc_update_220_order_status', + 'wc_update_220_variations', + 'wc_update_220_attributes', + 'wc_update_220_db_version', + ), + '2.3.0' => array( + 'wc_update_230_options', + 'wc_update_230_db_version', + ), + '2.4.0' => array( + 'wc_update_240_options', + 'wc_update_240_shipping_methods', + 'wc_update_240_api_keys', + 'wc_update_240_refunds', + 'wc_update_240_db_version', + ), + '2.4.1' => array( + 'wc_update_241_variations', + 'wc_update_241_db_version', + ), + '2.5.0' => array( + 'wc_update_250_currency', + 'wc_update_250_db_version', + ), + '2.6.0' => array( + 'wc_update_260_options', + 'wc_update_260_termmeta', + 'wc_update_260_zones', + 'wc_update_260_zone_methods', + 'wc_update_260_refunds', + 'wc_update_260_db_version', + ), + '3.0.0' => array( + 'wc_update_300_grouped_products', + 'wc_update_300_settings', + 'wc_update_300_product_visibility', + 'wc_update_300_db_version', + ), + '3.1.0' => array( + 'wc_update_310_downloadable_products', + 'wc_update_310_old_comments', + 'wc_update_310_db_version', + ), + '3.1.2' => array( + 'wc_update_312_shop_manager_capabilities', + 'wc_update_312_db_version', + ), + '3.2.0' => array( + 'wc_update_320_mexican_states', + 'wc_update_320_db_version', + ), + '3.3.0' => array( + 'wc_update_330_image_options', + 'wc_update_330_webhooks', + 'wc_update_330_product_stock_status', + 'wc_update_330_set_default_product_cat', + 'wc_update_330_clear_transients', + 'wc_update_330_set_paypal_sandbox_credentials', + 'wc_update_330_db_version', + ), + '3.4.0' => array( + 'wc_update_340_states', + 'wc_update_340_state', + 'wc_update_340_last_active', + 'wc_update_340_db_version', + ), + '3.4.3' => array( + 'wc_update_343_cleanup_foreign_keys', + 'wc_update_343_db_version', + ), + '3.4.4' => array( + 'wc_update_344_recreate_roles', + 'wc_update_344_db_version', + ), + '3.5.0' => array( + 'wc_update_350_reviews_comment_type', + 'wc_update_350_db_version', + ), + '3.5.2' => array( + 'wc_update_352_drop_download_log_fk', + ), + '3.5.4' => array( + 'wc_update_354_modify_shop_manager_caps', + 'wc_update_354_db_version', + ), + '3.6.0' => array( + 'wc_update_360_product_lookup_tables', + 'wc_update_360_term_meta', + 'wc_update_360_downloadable_product_permissions_index', + 'wc_update_360_db_version', + ), + '3.7.0' => array( + 'wc_update_370_tax_rate_classes', + 'wc_update_370_mro_std_currency', + 'wc_update_370_db_version', + ), + '3.9.0' => array( + 'wc_update_390_move_maxmind_database', + 'wc_update_390_change_geolocation_database_update_cron', + 'wc_update_390_db_version', + ), + '4.0.0' => array( + 'wc_update_product_lookup_tables', + 'wc_update_400_increase_size_of_column', + 'wc_update_400_reset_action_scheduler_migration_status', + 'wc_update_400_db_version', + ), + '4.4.0' => array( + 'wc_update_440_insert_attribute_terms_for_variable_products', + 'wc_update_440_db_version', + ), + '4.5.0' => array( + 'wc_update_450_sanitize_coupons_code', + 'wc_update_450_db_version', + ), + '5.0.0' => array( + 'wc_update_500_fix_product_review_count', + 'wc_update_500_db_version', + ), + '5.6.0' => array( + 'wc_update_560_create_refund_returns_page', + 'wc_update_560_db_version', + ), + ); + + /** + * Hook in tabs. + */ + public static function init() { + add_action( 'init', array( __CLASS__, 'check_version' ), 5 ); + add_action( 'init', array( __CLASS__, 'manual_database_update' ), 20 ); + add_action( 'admin_init', array( __CLASS__, 'wc_admin_db_update_notice' ) ); + add_action( 'admin_init', array( __CLASS__, 'add_admin_note_after_page_created' ) ); + add_action( 'woocommerce_run_update_callback', array( __CLASS__, 'run_update_callback' ) ); + add_action( 'admin_init', array( __CLASS__, 'install_actions' ) ); + add_action( 'woocommerce_page_created', array( __CLASS__, 'page_created' ), 10, 2 ); + add_filter( 'plugin_action_links_' . WC_PLUGIN_BASENAME, array( __CLASS__, 'plugin_action_links' ) ); + add_filter( 'plugin_row_meta', array( __CLASS__, 'plugin_row_meta' ), 10, 2 ); + add_filter( 'wpmu_drop_tables', array( __CLASS__, 'wpmu_drop_tables' ) ); + add_filter( 'cron_schedules', array( __CLASS__, 'cron_schedules' ) ); + } + + /** + * Check WooCommerce version and run the updater is required. + * + * This check is done on all requests and runs if the versions do not match. + */ + public static function check_version() { + if ( ! Constants::is_defined( 'IFRAME_REQUEST' ) && version_compare( get_option( 'woocommerce_version' ), WC()->version, '<' ) ) { + self::install(); + do_action( 'woocommerce_updated' ); + } + } + + /** + * Performan manual database update when triggered by WooCommerce System Tools. + * + * @since 3.6.5 + */ + public static function manual_database_update() { + $blog_id = get_current_blog_id(); + + add_action( 'wp_' . $blog_id . '_wc_updater_cron', array( __CLASS__, 'run_manual_database_update' ) ); + } + + /** + * Add WC Admin based db update notice. + * + * @since 4.0.0 + */ + public static function wc_admin_db_update_notice() { + if ( + WC()->is_wc_admin_active() && + false !== get_option( 'woocommerce_admin_install_timestamp' ) + ) { + new WC_Notes_Run_Db_Update(); + } + } + + /** + * Run manual database update. + */ + public static function run_manual_database_update() { + self::update(); + } + + /** + * Run an update callback when triggered by ActionScheduler. + * + * @param string $update_callback Callback name. + * + * @since 3.6.0 + */ + public static function run_update_callback( $update_callback ) { + include_once dirname( __FILE__ ) . '/wc-update-functions.php'; + + if ( is_callable( $update_callback ) ) { + self::run_update_callback_start( $update_callback ); + $result = (bool) call_user_func( $update_callback ); + self::run_update_callback_end( $update_callback, $result ); + } + } + + /** + * Triggered when a callback will run. + * + * @since 3.6.0 + * @param string $callback Callback name. + */ + protected static function run_update_callback_start( $callback ) { + wc_maybe_define_constant( 'WC_UPDATING', true ); + } + + /** + * Triggered when a callback has ran. + * + * @since 3.6.0 + * @param string $callback Callback name. + * @param bool $result Return value from callback. Non-false need to run again. + */ + protected static function run_update_callback_end( $callback, $result ) { + if ( $result ) { + WC()->queue()->add( + 'woocommerce_run_update_callback', + array( + 'update_callback' => $callback, + ), + 'woocommerce-db-updates' + ); + } + } + + /** + * Install actions when a update button is clicked within the admin area. + * + * This function is hooked into admin_init to affect admin only. + */ + public static function install_actions() { + if ( ! empty( $_GET['do_update_woocommerce'] ) ) { // WPCS: input var ok. + check_admin_referer( 'wc_db_update', 'wc_db_update_nonce' ); + self::update(); + WC_Admin_Notices::add_notice( 'update', true ); + } + } + + /** + * Install WC. + */ + public static function install() { + if ( ! is_blog_installed() ) { + return; + } + + // Check if we are not already running this routine. + if ( 'yes' === get_transient( 'wc_installing' ) ) { + return; + } + + // If we made it till here nothing is running yet, lets set the transient now. + set_transient( 'wc_installing', 'yes', MINUTE_IN_SECONDS * 10 ); + wc_maybe_define_constant( 'WC_INSTALLING', true ); + + WC()->wpdb_table_fix(); + self::remove_admin_notices(); + self::create_tables(); + self::verify_base_tables(); + self::create_options(); + self::create_roles(); + self::setup_environment(); + self::create_terms(); + self::create_cron_jobs(); + self::create_files(); + self::maybe_create_pages(); + self::maybe_set_activation_transients(); + self::set_paypal_standard_load_eligibility(); + self::update_wc_version(); + self::maybe_update_db_version(); + + delete_transient( 'wc_installing' ); + + do_action( 'woocommerce_flush_rewrite_rules' ); + do_action( 'woocommerce_installed' ); + } + + /** + * Check if all the base tables are present. + * + * @param bool $modify_notice Whether to modify notice based on if all tables are present. + * @param bool $execute Whether to execute get_schema queries as well. + * + * @return array List of querues. + */ + public static function verify_base_tables( $modify_notice = true, $execute = false ) { + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + if ( $execute ) { + self::create_tables(); + } + $queries = dbDelta( self::get_schema(), false ); + $missing_tables = array(); + foreach ( $queries as $table_name => $result ) { + if ( "Created table $table_name" === $result ) { + $missing_tables[] = $table_name; + } + } + + if ( 0 < count( $missing_tables ) ) { + if ( $modify_notice ) { + WC_Admin_Notices::add_notice( 'base_tables_missing' ); + } + update_option( 'woocommerce_schema_missing_tables', $missing_tables ); + } else { + if ( $modify_notice ) { + WC_Admin_Notices::remove_notice( 'base_tables_missing' ); + } + update_option( 'woocommerce_schema_version', WC()->db_version ); + delete_option( 'woocommerce_schema_missing_tables' ); + } + return $missing_tables; + } + + /** + * Reset any notices added to admin. + * + * @since 3.2.0 + */ + private static function remove_admin_notices() { + include_once dirname( __FILE__ ) . '/admin/class-wc-admin-notices.php'; + WC_Admin_Notices::remove_all_notices(); + } + + /** + * Setup WC environment - post types, taxonomies, endpoints. + * + * @since 3.2.0 + */ + private static function setup_environment() { + WC_Post_types::register_post_types(); + WC_Post_types::register_taxonomies(); + WC()->query->init_query_vars(); + WC()->query->add_endpoints(); + WC_API::add_endpoint(); + WC_Auth::add_endpoint(); + } + + /** + * Is this a brand new WC install? + * + * A brand new install has no version yet. Also treat empty installs as 'new'. + * + * @since 3.2.0 + * @return boolean + */ + public static function is_new_install() { + $product_count = array_sum( (array) wp_count_posts( 'product' ) ); + + return is_null( get_option( 'woocommerce_version', null ) ) || ( 0 === $product_count && -1 === wc_get_page_id( 'shop' ) ); + } + + /** + * Is a DB update needed? + * + * @since 3.2.0 + * @return boolean + */ + public static function needs_db_update() { + $current_db_version = get_option( 'woocommerce_db_version', null ); + $updates = self::get_db_update_callbacks(); + $update_versions = array_keys( $updates ); + usort( $update_versions, 'version_compare' ); + + return ! is_null( $current_db_version ) && version_compare( $current_db_version, end( $update_versions ), '<' ); + } + + /** + * See if we need to set redirect transients for activation or not. + * + * @since 4.6.0 + */ + private static function maybe_set_activation_transients() { + if ( self::is_new_install() ) { + set_transient( '_wc_activation_redirect', 1, 30 ); + } + } + + /** + * See if we need to show or run database updates during install. + * + * @since 3.2.0 + */ + private static function maybe_update_db_version() { + if ( self::needs_db_update() ) { + if ( apply_filters( 'woocommerce_enable_auto_update_db', false ) ) { + self::update(); + } else { + WC_Admin_Notices::add_notice( 'update', true ); + } + } else { + self::update_db_version(); + } + } + + /** + * Update WC version to current. + */ + private static function update_wc_version() { + update_option( 'woocommerce_version', WC()->version ); + } + + /** + * Get list of DB update callbacks. + * + * @since 3.0.0 + * @return array + */ + public static function get_db_update_callbacks() { + return self::$db_updates; + } + + /** + * Push all needed DB updates to the queue for processing. + */ + private static function update() { + $current_db_version = get_option( 'woocommerce_db_version' ); + $loop = 0; + + foreach ( self::get_db_update_callbacks() as $version => $update_callbacks ) { + if ( version_compare( $current_db_version, $version, '<' ) ) { + foreach ( $update_callbacks as $update_callback ) { + WC()->queue()->schedule_single( + time() + $loop, + 'woocommerce_run_update_callback', + array( + 'update_callback' => $update_callback, + ), + 'woocommerce-db-updates' + ); + $loop++; + } + } + } + } + + /** + * Update DB version to current. + * + * @param string|null $version New WooCommerce DB version or null. + */ + public static function update_db_version( $version = null ) { + update_option( 'woocommerce_db_version', is_null( $version ) ? WC()->version : $version ); + } + + /** + * Add more cron schedules. + * + * @param array $schedules List of WP scheduled cron jobs. + * + * @return array + */ + public static function cron_schedules( $schedules ) { + $schedules['monthly'] = array( + 'interval' => 2635200, + 'display' => __( 'Monthly', 'woocommerce' ), + ); + $schedules['fifteendays'] = array( + 'interval' => 1296000, + 'display' => __( 'Every 15 Days', 'woocommerce' ), + ); + return $schedules; + } + + /** + * Create cron jobs (clear them first). + */ + private static function create_cron_jobs() { + wp_clear_scheduled_hook( 'woocommerce_scheduled_sales' ); + wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); + wp_clear_scheduled_hook( 'woocommerce_cleanup_sessions' ); + wp_clear_scheduled_hook( 'woocommerce_cleanup_personal_data' ); + wp_clear_scheduled_hook( 'woocommerce_cleanup_logs' ); + wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); + wp_clear_scheduled_hook( 'woocommerce_tracker_send_event' ); + + $ve = get_option( 'gmt_offset' ) > 0 ? '-' : '+'; + + wp_schedule_event( strtotime( '00:00 tomorrow ' . $ve . absint( get_option( 'gmt_offset' ) ) . ' HOURS' ), 'daily', 'woocommerce_scheduled_sales' ); + + $held_duration = get_option( 'woocommerce_hold_stock_minutes', '60' ); + + if ( '' !== $held_duration ) { + $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); + wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); + } + + // Delay the first run of `woocommerce_cleanup_personal_data` by 10 seconds + // so it doesn't occur in the same request. WooCommerce Admin also schedules + // a daily cron that gets lost due to a race condition. WC_Privacy's background + // processing instance updates the cron schedule from within a cron job. + wp_schedule_event( time() + 10, 'daily', 'woocommerce_cleanup_personal_data' ); + wp_schedule_event( time() + ( 3 * HOUR_IN_SECONDS ), 'daily', 'woocommerce_cleanup_logs' ); + wp_schedule_event( time() + ( 6 * HOUR_IN_SECONDS ), 'twicedaily', 'woocommerce_cleanup_sessions' ); + wp_schedule_event( time() + MINUTE_IN_SECONDS, 'fifteendays', 'woocommerce_geoip_updater' ); + wp_schedule_event( time() + 10, apply_filters( 'woocommerce_tracker_event_recurrence', 'daily' ), 'woocommerce_tracker_send_event' ); + } + + /** + * Create pages on installation. + */ + public static function maybe_create_pages() { + if ( empty( get_option( 'woocommerce_db_version' ) ) ) { + self::create_pages(); + } + } + + /** + * Create pages that the plugin relies on, storing page IDs in variables. + */ + public static function create_pages() { + include_once dirname( __FILE__ ) . '/admin/wc-admin-functions.php'; + + $pages = apply_filters( + 'woocommerce_create_pages', + array( + 'shop' => array( + 'name' => _x( 'shop', 'Page slug', 'woocommerce' ), + 'title' => _x( 'Shop', 'Page title', 'woocommerce' ), + 'content' => '', + ), + 'cart' => array( + 'name' => _x( 'cart', 'Page slug', 'woocommerce' ), + 'title' => _x( 'Cart', 'Page title', 'woocommerce' ), + 'content' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', + ), + 'checkout' => array( + 'name' => _x( 'checkout', 'Page slug', 'woocommerce' ), + 'title' => _x( 'Checkout', 'Page title', 'woocommerce' ), + 'content' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', + ), + 'myaccount' => array( + 'name' => _x( 'my-account', 'Page slug', 'woocommerce' ), + 'title' => _x( 'My account', 'Page title', 'woocommerce' ), + 'content' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']', + ), + 'refund_returns' => array( + 'name' => _x( 'refund_returns', 'Page slug', 'woocommerce' ), + 'title' => _x( 'Refund and Returns Policy', 'Page title', 'woocommerce' ), + 'content' => self::get_refunds_return_policy_page_content(), + 'post_status' => 'draft', + ), + ) + ); + + foreach ( $pages as $key => $page ) { + wc_create_page( + esc_sql( $page['name'] ), + 'woocommerce_' . $key . '_page_id', + $page['title'], + $page['content'], + ! empty( $page['parent'] ) ? wc_get_page_id( $page['parent'] ) : '', + ! empty( $page['post_status'] ) ? $page['post_status'] : 'publish' + ); + } + } + + /** + * Default options. + * + * Sets up the default options used on the settings page. + */ + private static function create_options() { + // Include settings so that we can run through defaults. + include_once dirname( __FILE__ ) . '/admin/class-wc-admin-settings.php'; + + $settings = WC_Admin_Settings::get_settings_pages(); + + foreach ( $settings as $section ) { + if ( ! method_exists( $section, 'get_settings' ) ) { + continue; + } + $subsections = array_unique( array_merge( array( '' ), array_keys( $section->get_sections() ) ) ); + + /** + * We are using 'WC_Settings_Page::get_settings' on purpose even thought it's deprecated. + * See the method documentation for an explanation. + */ + + foreach ( $subsections as $subsection ) { + foreach ( $section->get_settings( $subsection ) as $value ) { + if ( isset( $value['default'] ) && isset( $value['id'] ) ) { + $autoload = isset( $value['autoload'] ) ? (bool) $value['autoload'] : true; + add_option( $value['id'], $value['default'], '', ( $autoload ? 'yes' : 'no' ) ); + } + } + } + } + + // Define other defaults if not in setting screens. + add_option( 'woocommerce_single_image_width', '600', '', 'yes' ); + add_option( 'woocommerce_thumbnail_image_width', '300', '', 'yes' ); + add_option( 'woocommerce_checkout_highlight_required_fields', 'yes', '', 'yes' ); + add_option( 'woocommerce_demo_store', 'no', '', 'no' ); + + if ( self::is_new_install() ) { + // Define initial tax classes. + WC_Tax::create_tax_class( __( 'Reduced rate', 'woocommerce' ) ); + WC_Tax::create_tax_class( __( 'Zero rate', 'woocommerce' ) ); + } + } + + /** + * Add the default terms for WC taxonomies - product types and order statuses. Modify this at your own risk. + */ + public static function create_terms() { + $taxonomies = array( + 'product_type' => array( + 'simple', + 'grouped', + 'variable', + 'external', + ), + 'product_visibility' => array( + 'exclude-from-search', + 'exclude-from-catalog', + 'featured', + 'outofstock', + 'rated-1', + 'rated-2', + 'rated-3', + 'rated-4', + 'rated-5', + ), + ); + + foreach ( $taxonomies as $taxonomy => $terms ) { + foreach ( $terms as $term ) { + if ( ! get_term_by( 'name', $term, $taxonomy ) ) { // @codingStandardsIgnoreLine. + wp_insert_term( $term, $taxonomy ); + } + } + } + + $woocommerce_default_category = (int) get_option( 'default_product_cat', 0 ); + + if ( ! $woocommerce_default_category || ! term_exists( $woocommerce_default_category, 'product_cat' ) ) { + $default_product_cat_id = 0; + $default_product_cat_slug = sanitize_title( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ) ); + $default_product_cat = get_term_by( 'slug', $default_product_cat_slug, 'product_cat' ); // @codingStandardsIgnoreLine. + + if ( $default_product_cat ) { + $default_product_cat_id = absint( $default_product_cat->term_taxonomy_id ); + } else { + $result = wp_insert_term( _x( 'Uncategorized', 'Default category slug', 'woocommerce' ), 'product_cat', array( 'slug' => $default_product_cat_slug ) ); + + if ( ! is_wp_error( $result ) && ! empty( $result['term_taxonomy_id'] ) ) { + $default_product_cat_id = absint( $result['term_taxonomy_id'] ); + } + } + + if ( $default_product_cat_id ) { + update_option( 'default_product_cat', $default_product_cat_id ); + } + } + } + + /** + * Set up the database tables which the plugin needs to function. + * WARNING: If you are modifying this method, make sure that its safe to call regardless of the state of database. + * + * This is called from `install` method and is executed in-sync when WC is installed or updated. This can also be called optionally from `verify_base_tables`. + * + * TODO: Add all crucial tables that we have created from workers in the past. + * + * Tables: + * woocommerce_attribute_taxonomies - Table for storing attribute taxonomies - these are user defined + * woocommerce_downloadable_product_permissions - Table for storing user and guest download permissions. + * KEY(order_id, product_id, download_id) used for organizing downloads on the My Account page + * woocommerce_order_items - Order line items are stored in a table to make them easily queryable for reports + * woocommerce_order_itemmeta - Order line item meta is stored in a table for storing extra data. + * woocommerce_tax_rates - Tax Rates are stored inside 2 tables making tax queries simple and efficient. + * woocommerce_tax_rate_locations - Each rate can be applied to more than one postcode/city hence the second table. + */ + private static function create_tables() { + global $wpdb; + + $wpdb->hide_errors(); + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + + /** + * Before updating with DBDELTA, remove any primary keys which could be + * modified due to schema updates. + */ + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_downloadable_product_permissions';" ) ) { + if ( ! $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_downloadable_product_permissions` LIKE 'permission_id';" ) ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions DROP PRIMARY KEY, ADD `permission_id` BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT;" ); + } + } + + /** + * Change wp_woocommerce_sessions schema to use a bigint auto increment field instead of char(32) field as + * the primary key as it is not a good practice to use a char(32) field as the primary key of a table and as + * there were reports of issues with this table (see https://github.com/woocommerce/woocommerce/issues/20912). + * + * This query needs to run before dbDelta() as this WP function is not able to handle primary key changes + * (see https://github.com/woocommerce/woocommerce/issues/21534 and https://core.trac.wordpress.org/ticket/40357). + */ + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_sessions'" ) ) { + if ( ! $wpdb->get_var( "SHOW KEYS FROM {$wpdb->prefix}woocommerce_sessions WHERE Key_name = 'PRIMARY' AND Column_name = 'session_id'" ) ) { + $wpdb->query( + "ALTER TABLE `{$wpdb->prefix}woocommerce_sessions` DROP PRIMARY KEY, DROP KEY `session_id`, ADD PRIMARY KEY(`session_id`), ADD UNIQUE KEY(`session_key`)" + ); + } + } + + dbDelta( self::get_schema() ); + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); + + if ( is_null( $index_exists ) ) { + // Add an index to the field comment_type to improve the response time of the query + // used by WC_Comments::wp_count_comments() to get the number of comments by type. + $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); + } + + // Get tables data types and check it matches before adding constraint. + $download_log_columns = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}wc_download_log WHERE Field = 'permission_id'", ARRAY_A ); + $download_log_column_type = ''; + if ( isset( $download_log_columns[0]['Type'] ) ) { + $download_log_column_type = $download_log_columns[0]['Type']; + } + + $download_permissions_columns = $wpdb->get_results( "SHOW COLUMNS FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE Field = 'permission_id'", ARRAY_A ); + $download_permissions_column_type = ''; + if ( isset( $download_permissions_columns[0]['Type'] ) ) { + $download_permissions_column_type = $download_permissions_columns[0]['Type']; + } + + // Add constraint to download logs if the columns matches. + if ( ! empty( $download_permissions_column_type ) && ! empty( $download_log_column_type ) && $download_permissions_column_type === $download_log_column_type ) { + $fk_result = $wpdb->get_row( "SHOW CREATE TABLE {$wpdb->prefix}wc_download_log" ); + if ( false === strpos( $fk_result->{'Create Table'}, "fk_{$wpdb->prefix}wc_download_log_permission_id" ) ) { + $wpdb->query( + "ALTER TABLE `{$wpdb->prefix}wc_download_log` + ADD CONSTRAINT `fk_{$wpdb->prefix}wc_download_log_permission_id` + FOREIGN KEY (`permission_id`) + REFERENCES `{$wpdb->prefix}woocommerce_downloadable_product_permissions` (`permission_id`) ON DELETE CASCADE;" + ); + } + } + + // Clear table caches. + delete_transient( 'wc_attribute_taxonomies' ); + } + + /** + * Get Table schema. + * + * See https://github.com/woocommerce/woocommerce/wiki/Database-Description/ + * + * A note on indexes; Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. + * As of WordPress 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which + * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. + * + * Changing indexes may cause duplicate index notices in logs due to https://core.trac.wordpress.org/ticket/34870 but dropping + * indexes first causes too much load on some servers/larger DB. + * + * When adding or removing a table, make sure to update the list of tables in WC_Install::get_tables(). + * + * @return string + */ + private static function get_schema() { + global $wpdb; + + $collate = ''; + + if ( $wpdb->has_cap( 'collation' ) ) { + $collate = $wpdb->get_charset_collate(); + } + + /* + * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that. + * As of WP 4.2, however, they moved to utf8mb4, which uses 4 bytes per character. This means that an index which + * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters. + */ + $max_index_length = 191; + + $tables = " +CREATE TABLE {$wpdb->prefix}woocommerce_sessions ( + session_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + session_key char(32) NOT NULL, + session_value longtext NOT NULL, + session_expiry BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (session_id), + UNIQUE KEY session_key (session_key) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_api_keys ( + key_id BIGINT UNSIGNED NOT NULL auto_increment, + user_id BIGINT UNSIGNED NOT NULL, + description varchar(200) NULL, + permissions varchar(10) NOT NULL, + consumer_key char(64) NOT NULL, + consumer_secret char(43) NOT NULL, + nonces longtext NULL, + truncated_key char(7) NOT NULL, + last_access datetime NULL default null, + PRIMARY KEY (key_id), + KEY consumer_key (consumer_key), + KEY consumer_secret (consumer_secret) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_attribute_taxonomies ( + attribute_id BIGINT UNSIGNED NOT NULL auto_increment, + attribute_name varchar(200) NOT NULL, + attribute_label varchar(200) NULL, + attribute_type varchar(20) NOT NULL, + attribute_orderby varchar(20) NOT NULL, + attribute_public int(1) NOT NULL DEFAULT 1, + PRIMARY KEY (attribute_id), + KEY attribute_name (attribute_name(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ( + permission_id BIGINT UNSIGNED NOT NULL auto_increment, + download_id varchar(36) NOT NULL, + product_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NOT NULL DEFAULT 0, + order_key varchar(200) NOT NULL, + user_email varchar(200) NOT NULL, + user_id BIGINT UNSIGNED NULL, + downloads_remaining varchar(9) NULL, + access_granted datetime NOT NULL default '0000-00-00 00:00:00', + access_expires datetime NULL default null, + download_count BIGINT UNSIGNED NOT NULL DEFAULT 0, + PRIMARY KEY (permission_id), + KEY download_order_key_product (product_id,order_id,order_key(16),download_id), + KEY download_order_product (download_id,order_id,product_id), + KEY order_id (order_id), + KEY user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_order_items ( + order_item_id BIGINT UNSIGNED NOT NULL auto_increment, + order_item_name TEXT NOT NULL, + order_item_type varchar(200) NOT NULL DEFAULT '', + order_id BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (order_item_id), + KEY order_id (order_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_order_itemmeta ( + meta_id BIGINT UNSIGNED NOT NULL auto_increment, + order_item_id BIGINT UNSIGNED NOT NULL, + meta_key varchar(255) default NULL, + meta_value longtext NULL, + PRIMARY KEY (meta_id), + KEY order_item_id (order_item_id), + KEY meta_key (meta_key(32)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_tax_rates ( + tax_rate_id BIGINT UNSIGNED NOT NULL auto_increment, + tax_rate_country varchar(2) NOT NULL DEFAULT '', + tax_rate_state varchar(200) NOT NULL DEFAULT '', + tax_rate varchar(8) NOT NULL DEFAULT '', + tax_rate_name varchar(200) NOT NULL DEFAULT '', + tax_rate_priority BIGINT UNSIGNED NOT NULL, + tax_rate_compound int(1) NOT NULL DEFAULT 0, + tax_rate_shipping int(1) NOT NULL DEFAULT 1, + tax_rate_order BIGINT UNSIGNED NOT NULL, + tax_rate_class varchar(200) NOT NULL DEFAULT '', + PRIMARY KEY (tax_rate_id), + KEY tax_rate_country (tax_rate_country), + KEY tax_rate_state (tax_rate_state(2)), + KEY tax_rate_class (tax_rate_class(10)), + KEY tax_rate_priority (tax_rate_priority) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations ( + location_id BIGINT UNSIGNED NOT NULL auto_increment, + location_code varchar(200) NOT NULL, + tax_rate_id BIGINT UNSIGNED NOT NULL, + location_type varchar(40) NOT NULL, + PRIMARY KEY (location_id), + KEY tax_rate_id (tax_rate_id), + KEY location_type_code (location_type(10),location_code(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zones ( + zone_id BIGINT UNSIGNED NOT NULL auto_increment, + zone_name varchar(200) NOT NULL, + zone_order BIGINT UNSIGNED NOT NULL, + PRIMARY KEY (zone_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_locations ( + location_id BIGINT UNSIGNED NOT NULL auto_increment, + zone_id BIGINT UNSIGNED NOT NULL, + location_code varchar(200) NOT NULL, + location_type varchar(40) NOT NULL, + PRIMARY KEY (location_id), + KEY location_id (location_id), + KEY location_type_code (location_type(10),location_code(20)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods ( + zone_id BIGINT UNSIGNED NOT NULL, + instance_id BIGINT UNSIGNED NOT NULL auto_increment, + method_id varchar(200) NOT NULL, + method_order BIGINT UNSIGNED NOT NULL, + is_enabled tinyint(1) NOT NULL DEFAULT '1', + PRIMARY KEY (instance_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokens ( + token_id BIGINT UNSIGNED NOT NULL auto_increment, + gateway_id varchar(200) NOT NULL, + token text NOT NULL, + user_id BIGINT UNSIGNED NOT NULL DEFAULT '0', + type varchar(200) NOT NULL, + is_default tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (token_id), + KEY user_id (user_id) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_payment_tokenmeta ( + meta_id BIGINT UNSIGNED NOT NULL auto_increment, + payment_token_id BIGINT UNSIGNED NOT NULL, + meta_key varchar(255) NULL, + meta_value longtext NULL, + PRIMARY KEY (meta_id), + KEY payment_token_id (payment_token_id), + KEY meta_key (meta_key(32)) +) $collate; +CREATE TABLE {$wpdb->prefix}woocommerce_log ( + log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp datetime NOT NULL, + level smallint(4) NOT NULL, + source varchar(200) NOT NULL, + message longtext NOT NULL, + context longtext NULL, + PRIMARY KEY (log_id), + KEY level (level) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_webhooks ( + webhook_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + status varchar(200) NOT NULL, + name text NOT NULL, + user_id BIGINT UNSIGNED NOT NULL, + delivery_url text NOT NULL, + secret text NOT NULL, + topic varchar(200) NOT NULL, + date_created datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + date_created_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + date_modified datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + date_modified_gmt datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + api_version smallint(4) NOT NULL, + failure_count smallint(10) NOT NULL DEFAULT '0', + pending_delivery tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (webhook_id), + KEY user_id (user_id) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_download_log ( + download_log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp datetime NOT NULL, + permission_id BIGINT UNSIGNED NOT NULL, + user_id BIGINT UNSIGNED NULL, + user_ip_address VARCHAR(100) NULL DEFAULT '', + PRIMARY KEY (download_log_id), + KEY permission_id (permission_id), + KEY timestamp (timestamp) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_product_meta_lookup ( + `product_id` bigint(20) NOT NULL, + `sku` varchar(100) NULL default '', + `virtual` tinyint(1) NULL default 0, + `downloadable` tinyint(1) NULL default 0, + `min_price` decimal(19,4) NULL default NULL, + `max_price` decimal(19,4) NULL default NULL, + `onsale` tinyint(1) NULL default 0, + `stock_quantity` double NULL default NULL, + `stock_status` varchar(100) NULL default 'instock', + `rating_count` bigint(20) NULL default 0, + `average_rating` decimal(3,2) NULL default 0.00, + `total_sales` bigint(20) NULL default 0, + `tax_status` varchar(100) NULL default 'taxable', + `tax_class` varchar(100) NULL default '', + PRIMARY KEY (`product_id`), + KEY `virtual` (`virtual`), + KEY `downloadable` (`downloadable`), + KEY `stock_status` (`stock_status`), + KEY `stock_quantity` (`stock_quantity`), + KEY `onsale` (`onsale`), + KEY min_max_price (`min_price`, `max_price`) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_tax_rate_classes ( + tax_rate_class_id BIGINT UNSIGNED NOT NULL auto_increment, + name varchar(200) NOT NULL DEFAULT '', + slug varchar(200) NOT NULL DEFAULT '', + PRIMARY KEY (tax_rate_class_id), + UNIQUE KEY slug (slug($max_index_length)) +) $collate; +CREATE TABLE {$wpdb->prefix}wc_reserved_stock ( + `order_id` bigint(20) NOT NULL, + `product_id` bigint(20) NOT NULL, + `stock_quantity` double NOT NULL DEFAULT 0, + `timestamp` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY (`order_id`, `product_id`) +) $collate; + "; + + return $tables; + } + + /** + * Return a list of WooCommerce tables. Used to make sure all WC tables are dropped when uninstalling the plugin + * in a single site or multi site environment. + * + * @return array WC tables. + */ + public static function get_tables() { + global $wpdb; + + $tables = array( + "{$wpdb->prefix}wc_download_log", + "{$wpdb->prefix}wc_product_meta_lookup", + "{$wpdb->prefix}wc_tax_rate_classes", + "{$wpdb->prefix}wc_webhooks", + "{$wpdb->prefix}woocommerce_api_keys", + "{$wpdb->prefix}woocommerce_attribute_taxonomies", + "{$wpdb->prefix}woocommerce_downloadable_product_permissions", + "{$wpdb->prefix}woocommerce_log", + "{$wpdb->prefix}woocommerce_order_itemmeta", + "{$wpdb->prefix}woocommerce_order_items", + "{$wpdb->prefix}woocommerce_payment_tokenmeta", + "{$wpdb->prefix}woocommerce_payment_tokens", + "{$wpdb->prefix}woocommerce_sessions", + "{$wpdb->prefix}woocommerce_shipping_zone_locations", + "{$wpdb->prefix}woocommerce_shipping_zone_methods", + "{$wpdb->prefix}woocommerce_shipping_zones", + "{$wpdb->prefix}woocommerce_tax_rate_locations", + "{$wpdb->prefix}woocommerce_tax_rates", + "{$wpdb->prefix}wc_reserved_stock", + ); + + /** + * Filter the list of known WooCommerce tables. + * + * If WooCommerce plugins need to add new tables, they can inject them here. + * + * @param array $tables An array of WooCommerce-specific database table names. + */ + $tables = apply_filters( 'woocommerce_install_get_tables', $tables ); + + return $tables; + } + + /** + * Drop WooCommerce tables. + * + * @return void + */ + public static function drop_tables() { + global $wpdb; + + $tables = self::get_tables(); + + foreach ( $tables as $table ) { + $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + } + + /** + * Uninstall tables when MU blog is deleted. + * + * @param array $tables List of tables that will be deleted by WP. + * + * @return string[] + */ + public static function wpmu_drop_tables( $tables ) { + return array_merge( $tables, self::get_tables() ); + } + + /** + * Create roles and capabilities. + */ + public static function create_roles() { + global $wp_roles; + + if ( ! class_exists( 'WP_Roles' ) ) { + return; + } + + if ( ! isset( $wp_roles ) ) { + $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine + } + + // Dummy gettext calls to get strings in the catalog. + /* translators: user role */ + _x( 'Customer', 'User role', 'woocommerce' ); + /* translators: user role */ + _x( 'Shop manager', 'User role', 'woocommerce' ); + + // Customer role. + add_role( + 'customer', + 'Customer', + array( + 'read' => true, + ) + ); + + // Shop manager role. + add_role( + 'shop_manager', + 'Shop manager', + array( + 'level_9' => true, + 'level_8' => true, + 'level_7' => true, + 'level_6' => true, + 'level_5' => true, + 'level_4' => true, + 'level_3' => true, + 'level_2' => true, + 'level_1' => true, + 'level_0' => true, + 'read' => true, + 'read_private_pages' => true, + 'read_private_posts' => true, + 'edit_posts' => true, + 'edit_pages' => true, + 'edit_published_posts' => true, + 'edit_published_pages' => true, + 'edit_private_pages' => true, + 'edit_private_posts' => true, + 'edit_others_posts' => true, + 'edit_others_pages' => true, + 'publish_posts' => true, + 'publish_pages' => true, + 'delete_posts' => true, + 'delete_pages' => true, + 'delete_private_pages' => true, + 'delete_private_posts' => true, + 'delete_published_pages' => true, + 'delete_published_posts' => true, + 'delete_others_posts' => true, + 'delete_others_pages' => true, + 'manage_categories' => true, + 'manage_links' => true, + 'moderate_comments' => true, + 'upload_files' => true, + 'export' => true, + 'import' => true, + 'list_users' => true, + 'edit_theme_options' => true, + ) + ); + + $capabilities = self::get_core_capabilities(); + + foreach ( $capabilities as $cap_group ) { + foreach ( $cap_group as $cap ) { + $wp_roles->add_cap( 'shop_manager', $cap ); + $wp_roles->add_cap( 'administrator', $cap ); + } + } + } + + /** + * Get capabilities for WooCommerce - these are assigned to admin/shop manager during installation or reset. + * + * @return array + */ + public static function get_core_capabilities() { + $capabilities = array(); + + $capabilities['core'] = array( + 'manage_woocommerce', + 'view_woocommerce_reports', + ); + + $capability_types = array( 'product', 'shop_order', 'shop_coupon' ); + + foreach ( $capability_types as $capability_type ) { + + $capabilities[ $capability_type ] = array( + // Post type. + "edit_{$capability_type}", + "read_{$capability_type}", + "delete_{$capability_type}", + "edit_{$capability_type}s", + "edit_others_{$capability_type}s", + "publish_{$capability_type}s", + "read_private_{$capability_type}s", + "delete_{$capability_type}s", + "delete_private_{$capability_type}s", + "delete_published_{$capability_type}s", + "delete_others_{$capability_type}s", + "edit_private_{$capability_type}s", + "edit_published_{$capability_type}s", + + // Terms. + "manage_{$capability_type}_terms", + "edit_{$capability_type}_terms", + "delete_{$capability_type}_terms", + "assign_{$capability_type}_terms", + ); + } + + return $capabilities; + } + + /** + * Remove WooCommerce roles. + */ + public static function remove_roles() { + global $wp_roles; + + if ( ! class_exists( 'WP_Roles' ) ) { + return; + } + + if ( ! isset( $wp_roles ) ) { + $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine + } + + $capabilities = self::get_core_capabilities(); + + foreach ( $capabilities as $cap_group ) { + foreach ( $cap_group as $cap ) { + $wp_roles->remove_cap( 'shop_manager', $cap ); + $wp_roles->remove_cap( 'administrator', $cap ); + } + } + + remove_role( 'customer' ); + remove_role( 'shop_manager' ); + } + + /** + * Create files/directories. + */ + private static function create_files() { + // Bypass if filesystem is read-only and/or non-standard upload system is used. + if ( apply_filters( 'woocommerce_install_skip_create_files', false ) ) { + return; + } + + // Install files and folders for uploading files and prevent hotlinking. + $upload_dir = wp_get_upload_dir(); + $download_method = get_option( 'woocommerce_file_download_method', 'force' ); + + $files = array( + array( + 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', + 'file' => 'index.html', + 'content' => '', + ), + array( + 'base' => WC_LOG_DIR, + 'file' => '.htaccess', + 'content' => 'deny from all', + ), + array( + 'base' => WC_LOG_DIR, + 'file' => 'index.html', + 'content' => '', + ), + array( + 'base' => $upload_dir['basedir'] . '/woocommerce_uploads', + 'file' => '.htaccess', + 'content' => 'redirect' === $download_method ? 'Options -Indexes' : 'deny from all', + ), + ); + + foreach ( $files as $file ) { + if ( wp_mkdir_p( $file['base'] ) && ! file_exists( trailingslashit( $file['base'] ) . $file['file'] ) ) { + $file_handle = @fopen( trailingslashit( $file['base'] ) . $file['file'], 'wb' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen + if ( $file_handle ) { + fwrite( $file_handle, $file['content'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite + fclose( $file_handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose + } + } + } + + // Create attachment for placeholders. + self::create_placeholder_image(); + } + + /** + * Create a placeholder image in the media library. + * + * @since 3.5.0 + */ + private static function create_placeholder_image() { + $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); + + // Validate current setting if set. If set, return. + if ( ! empty( $placeholder_image ) ) { + if ( ! is_numeric( $placeholder_image ) ) { + return; + } elseif ( $placeholder_image && wp_attachment_is_image( $placeholder_image ) ) { + return; + } + } + + $upload_dir = wp_upload_dir(); + $source = WC()->plugin_path() . '/assets/images/placeholder-attachment.png'; + $filename = $upload_dir['basedir'] . '/woocommerce-placeholder.png'; + + if ( ! file_exists( $filename ) ) { + copy( $source, $filename ); // @codingStandardsIgnoreLine. + } + + if ( ! file_exists( $filename ) ) { + update_option( 'woocommerce_placeholder_image', 0 ); + return; + } + + $filetype = wp_check_filetype( basename( $filename ), null ); + $attachment = array( + 'guid' => $upload_dir['url'] . '/' . basename( $filename ), + 'post_mime_type' => $filetype['type'], + 'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ), + 'post_content' => '', + 'post_status' => 'inherit', + ); + + $attach_id = wp_insert_attachment( $attachment, $filename ); + if ( is_wp_error( $attach_id ) ) { + update_option( 'woocommerce_placeholder_image', 0 ); + return; + } + + update_option( 'woocommerce_placeholder_image', $attach_id ); + + // Make sure that this file is included, as wp_generate_attachment_metadata() depends on it. + require_once ABSPATH . 'wp-admin/includes/image.php'; + + // Generate the metadata for the attachment, and update the database record. + $attach_data = wp_generate_attachment_metadata( $attach_id, $filename ); + wp_update_attachment_metadata( $attach_id, $attach_data ); + } + + /** + * Show action links on the plugin screen. + * + * @param mixed $links Plugin Action links. + * + * @return array + */ + public static function plugin_action_links( $links ) { + $action_links = array( + 'settings' => '' . esc_html__( 'Settings', 'woocommerce' ) . '', + ); + + return array_merge( $action_links, $links ); + } + + /** + * Show row meta on the plugin screen. + * + * @param mixed $links Plugin Row Meta. + * @param mixed $file Plugin Base file. + * + * @return array + */ + public static function plugin_row_meta( $links, $file ) { + if ( WC_PLUGIN_BASENAME !== $file ) { + return $links; + } + + $row_meta = array( + 'docs' => '' . esc_html__( 'Docs', 'woocommerce' ) . '', + 'apidocs' => '' . esc_html__( 'API docs', 'woocommerce' ) . '', + 'support' => '' . esc_html__( 'Community support', 'woocommerce' ) . '', + ); + + if ( WCConnectionHelper::is_connected() ) { + $row_meta['premium_support'] = '' . esc_html__( 'Premium support', 'woocommerce' ) . ''; + } + + return array_merge( $links, $row_meta ); + } + + /** + * Get slug from path and associate it with the path. + * + * @param array $plugins Associative array of plugin files to paths. + * @param string $key Plugin relative path. Example: woocommerce/woocommerce.php. + */ + private static function associate_plugin_file( $plugins, $key ) { + $path = explode( '/', $key ); + $filename = end( $path ); + $plugins[ $filename ] = $key; + return $plugins; + } + + /** + * Install a plugin from .org in the background via a cron job (used by + * installer - opt in). + * + * @param string $plugin_to_install_id Plugin ID. + * @param array $plugin_to_install Plugin information. + * + * @throws Exception If unable to proceed with plugin installation. + * @since 2.6.0 + */ + public static function background_installer( $plugin_to_install_id, $plugin_to_install ) { + // Explicitly clear the event. + $args = func_get_args(); + + if ( ! empty( $plugin_to_install['repo-slug'] ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin(); + $upgrader = new WP_Upgrader( $skin ); + $installed_plugins = array_reduce( array_keys( get_plugins() ), array( __CLASS__, 'associate_plugin_file' ) ); + if ( empty( $installed_plugins ) ) { + $installed_plugins = array(); + } + $plugin_slug = $plugin_to_install['repo-slug']; + $plugin_file = isset( $plugin_to_install['file'] ) ? $plugin_to_install['file'] : $plugin_slug . '.php'; + $installed = false; + $activate = false; + + // See if the plugin is installed already. + if ( isset( $installed_plugins[ $plugin_file ] ) ) { + $installed = true; + $activate = ! is_plugin_active( $installed_plugins[ $plugin_file ] ); + } + + // Install this thing! + if ( ! $installed ) { + // Suppress feedback. + ob_start(); + + try { + $plugin_information = plugins_api( + 'plugin_information', + array( + 'slug' => $plugin_slug, + 'fields' => array( + 'short_description' => false, + 'sections' => false, + 'requires' => false, + 'rating' => false, + 'ratings' => false, + 'downloaded' => false, + 'last_updated' => false, + 'added' => false, + 'tags' => false, + 'homepage' => false, + 'donate_link' => false, + 'author_profile' => false, + 'author' => false, + ), + ) + ); + + if ( is_wp_error( $plugin_information ) ) { + throw new Exception( $plugin_information->get_error_message() ); + } + + $package = $plugin_information->download_link; + $download = $upgrader->download_package( $package ); + + if ( is_wp_error( $download ) ) { + throw new Exception( $download->get_error_message() ); + } + + $working_dir = $upgrader->unpack_package( $download, true ); + + if ( is_wp_error( $working_dir ) ) { + throw new Exception( $working_dir->get_error_message() ); + } + + $result = $upgrader->install_package( + array( + 'source' => $working_dir, + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => false, + 'abort_if_destination_exists' => false, + 'clear_working' => true, + 'hook_extra' => array( + 'type' => 'plugin', + 'action' => 'install', + ), + ) + ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + $activate = true; + + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + // translators: 1: plugin name, 2: error message, 3: URL to install plugin manually. + __( '%1$s could not be installed (%2$s). Please install it manually by clicking here.', 'woocommerce' ), + $plugin_to_install['name'], + $e->getMessage(), + esc_url( admin_url( 'index.php?wc-install-plugin-redirect=' . $plugin_slug ) ) + ) + ); + } + + // Discard feedback. + ob_end_clean(); + } + + wp_clean_plugins_cache(); + + // Activate this thing. + if ( $activate ) { + try { + add_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ), 10, 2 ); + $result = activate_plugin( $installed ? $installed_plugins[ $plugin_file ] : $plugin_slug . '/' . $plugin_file ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $plugin_to_install_id . '_install_error', + sprintf( + // translators: 1: plugin name, 2: URL to WP plugin page. + __( '%1$s was installed but could not be activated. Please activate it manually by clicking here.', 'woocommerce' ), + $plugin_to_install['name'], + admin_url( 'plugins.php' ) + ) + ); + } + } + } + } + + /** + * Removes redirect added during MailChimp plugin's activation. + * + * @param string $option Option name. + * @param string $value Option value. + */ + public static function remove_mailchimps_redirect( $option, $value ) { + // Remove this action to prevent infinite looping. + remove_action( 'add_option_mailchimp_woocommerce_plugin_do_activation_redirect', array( __CLASS__, 'remove_mailchimps_redirect' ) ); + + // Update redirect back to false. + update_option( 'mailchimp_woocommerce_plugin_do_activation_redirect', false ); + } + + /** + * Install a theme from .org in the background via a cron job (used by installer - opt in). + * + * @param string $theme_slug Theme slug. + * + * @throws Exception If unable to proceed with theme installation. + * @since 3.1.0 + */ + public static function theme_background_installer( $theme_slug ) { + // Explicitly clear the event. + $args = func_get_args(); + + if ( ! empty( $theme_slug ) ) { + // Suppress feedback. + ob_start(); + + try { + $theme = wp_get_theme( $theme_slug ); + + if ( ! $theme->exists() ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + include_once ABSPATH . 'wp-admin/includes/theme.php'; + + WP_Filesystem(); + + $skin = new Automatic_Upgrader_Skin(); + $upgrader = new Theme_Upgrader( $skin ); + $api = themes_api( + 'theme_information', + array( + 'slug' => $theme_slug, + 'fields' => array( 'sections' => false ), + ) + ); + $result = $upgrader->install( $api->download_link ); + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } elseif ( is_wp_error( $skin->result ) ) { + throw new Exception( $skin->result->get_error_message() ); + } elseif ( is_null( $result ) ) { + throw new Exception( 'Unable to connect to the filesystem. Please confirm your credentials.' ); + } + } + + switch_theme( $theme_slug ); + } catch ( Exception $e ) { + WC_Admin_Notices::add_custom_notice( + $theme_slug . '_install_error', + sprintf( + // translators: 1: theme slug, 2: error message, 3: URL to install theme manually. + __( '%1$s could not be installed (%2$s). Please install it manually by clicking here.', 'woocommerce' ), + $theme_slug, + $e->getMessage(), + esc_url( admin_url( 'update.php?action=install-theme&theme=' . $theme_slug . '&_wpnonce=' . wp_create_nonce( 'install-theme_' . $theme_slug ) ) ) + ) + ); + } + + // Discard feedback. + ob_end_clean(); + } + } + + /** + * Sets whether PayPal Standard will be loaded on install. + * + * @since 5.5.0 + */ + private static function set_paypal_standard_load_eligibility() { + // Initiating the payment gateways sets the flag. + if ( class_exists( 'WC_Gateway_Paypal' ) ) { + ( new WC_Gateway_Paypal() )->should_load(); + } + } + + /** + * Gets the content of the sample refunds and return policy page. + * + * @since 5.6.0 + * @return HTML The content for the page + */ + private static function get_refunds_return_policy_page_content() { + return << +

    This is a sample page.

    + + + +

    Overview

    + + + +

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

    + + + +

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

    + + + +

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

    + + + +

    Additional non-returnable items:

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

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

    + + + +

    Please do not send your purchase back to the manufacturer.

    + + + +

    There are certain situations where only partial refunds are granted:

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

    Refunds

    + + + +

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

    + + + +

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

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

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

    + + + +

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

    + + + +

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

    + + + +

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

    + + + +Sale items + + + +

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

    + + + +

    Exchanges

    + + + +

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

    + + + +

    Gifts

    + + + +

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

    + + + +

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

    + + + +

    Shipping returns

    + + + +

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

    + + + +

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

    + + + +

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

    + + + +

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

    + + + +

    Need help?

    + + + +

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

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

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

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

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

    ' . + '

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

    ' . + '

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

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

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

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

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '

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

    ' . + '
    '; + + return apply_filters( 'wc_privacy_policy_content', $content ); + } + + /** + * Spawn events for order cleanup. + */ + public function queue_cleanup_personal_data() { + self::$background_process->push_to_queue( array( 'task' => 'trash_pending_orders' ) ); + self::$background_process->push_to_queue( array( 'task' => 'trash_failed_orders' ) ); + self::$background_process->push_to_queue( array( 'task' => 'trash_cancelled_orders' ) ); + self::$background_process->push_to_queue( array( 'task' => 'anonymize_completed_orders' ) ); + self::$background_process->push_to_queue( array( 'task' => 'delete_inactive_accounts' ) ); + self::$background_process->save()->dispatch(); + } + + /** + * Handle some custom types of data and anonymize them. + * + * @param string $anonymous Anonymized string. + * @param string $type Type of data. + * @param string $data The data being anonymized. + * @return string Anonymized string. + */ + public function anonymize_custom_data_types( $anonymous, $type, $data ) { + switch ( $type ) { + case 'address_state': + case 'address_country': + $anonymous = ''; // Empty string - we don't want to store anything after removal. + break; + case 'phone': + $anonymous = preg_replace( '/\d/u', '0', $data ); + break; + case 'numeric_id': + $anonymous = 0; + break; + } + return $anonymous; + } + + /** + * Find and trash old orders. + * + * @since 3.4.0 + * @param int $limit Limit orders to process per batch. + * @return int Number of orders processed. + */ + public static function trash_pending_orders( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_pending_orders' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::trash_orders_query( + apply_filters( + 'woocommerce_trash_pending_orders_query_args', + array( + 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), + 'limit' => $limit, // Batches of 20. + 'status' => 'wc-pending', + 'type' => 'shop_order', + ) + ) + ); + } + + /** + * Find and trash old orders. + * + * @since 3.4.0 + * @param int $limit Limit orders to process per batch. + * @return int Number of orders processed. + */ + public static function trash_failed_orders( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_failed_orders' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::trash_orders_query( + apply_filters( + 'woocommerce_trash_failed_orders_query_args', + array( + 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), + 'limit' => $limit, // Batches of 20. + 'status' => 'wc-failed', + 'type' => 'shop_order', + ) + ) + ); + } + + /** + * Find and trash old orders. + * + * @since 3.4.0 + * @param int $limit Limit orders to process per batch. + * @return int Number of orders processed. + */ + public static function trash_cancelled_orders( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_trash_cancelled_orders' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::trash_orders_query( + apply_filters( + 'woocommerce_trash_cancelled_orders_query_args', + array( + 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), + 'limit' => $limit, // Batches of 20. + 'status' => 'wc-cancelled', + 'type' => 'shop_order', + ) + ) + ); + } + + /** + * For a given query trash all matches. + * + * @since 3.4.0 + * @param array $query Query array to pass to wc_get_orders(). + * @return int Count of orders that were trashed. + */ + protected static function trash_orders_query( $query ) { + $orders = wc_get_orders( $query ); + $count = 0; + + if ( $orders ) { + foreach ( $orders as $order ) { + $order->delete( false ); + $count ++; + } + } + + return $count; + } + + /** + * Anonymize old completed orders. + * + * @since 3.4.0 + * @param int $limit Limit orders to process per batch. + * @return int Number of orders processed. + */ + public static function anonymize_completed_orders( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_anonymize_completed_orders' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::anonymize_orders_query( + apply_filters( + 'woocommerce_anonymize_completed_orders_query_args', + array( + 'date_created' => '<' . strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), + 'limit' => $limit, // Batches of 20. + 'status' => 'wc-completed', + 'anonymized' => false, + 'type' => 'shop_order', + ) + ) + ); + } + + /** + * For a given query, anonymize all matches. + * + * @since 3.4.0 + * @param array $query Query array to pass to wc_get_orders(). + * @return int Count of orders that were anonymized. + */ + protected static function anonymize_orders_query( $query ) { + $orders = wc_get_orders( $query ); + $count = 0; + + if ( $orders ) { + foreach ( $orders as $order ) { + WC_Privacy_Erasers::remove_order_personal_data( $order ); + $count ++; + } + } + + return $count; + } + + /** + * Delete inactive accounts. + * + * @since 3.4.0 + * @param int $limit Limit users to process per batch. + * @return int Number of users processed. + */ + public static function delete_inactive_accounts( $limit = 20 ) { + $option = wc_parse_relative_date_option( get_option( 'woocommerce_delete_inactive_accounts' ) ); + + if ( empty( $option['number'] ) ) { + return 0; + } + + return self::delete_inactive_accounts_query( strtotime( '-' . $option['number'] . ' ' . $option['unit'] ), $limit ); + } + + /** + * Delete inactive accounts. + * + * @since 3.4.0 + * @param int $timestamp Timestamp to delete customers before. + * @param int $limit Limit number of users to delete per run. + * @return int Count of customers that were deleted. + */ + protected static function delete_inactive_accounts_query( $timestamp, $limit = 20 ) { + $count = 0; + $user_query = new WP_User_Query( + array( + 'fields' => 'ID', + 'number' => $limit, + 'role__in' => apply_filters( + 'woocommerce_delete_inactive_account_roles', + array( + 'Customer', + 'Subscriber', + ) + ), + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'relation' => 'AND', + array( + 'key' => 'wc_last_active', + 'value' => (string) $timestamp, + 'compare' => '<', + 'type' => 'NUMERIC', + ), + array( + 'key' => 'wc_last_active', + 'value' => '0', + 'compare' => '>', + 'type' => 'NUMERIC', + ), + ), + ) + ); + + $user_ids = $user_query->get_results(); + + if ( $user_ids ) { + if ( ! function_exists( 'wp_delete_user' ) ) { + require_once ABSPATH . 'wp-admin/includes/user.php'; + } + + foreach ( $user_ids as $user_id ) { + wp_delete_user( $user_id ); + $count ++; + } + } + + return $count; + } +} + +new WC_Privacy(); diff --git a/includes/class-wc-product-attribute.php b/includes/class-wc-product-attribute.php new file mode 100644 index 0000000..14f1901 --- /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 Attribute ID. + */ + public function set_id( $value ) { + $this->data['id'] = absint( $value ); + } + + /** + * Set name (this is the attribute name or taxonomy). + * + * @param int $value Attribute name. + */ + public function set_name( $value ) { + $this->data['name'] = $value; + } + + /** + * Set options. + * + * @param array $value Attribute options. + */ + public function set_options( $value ) { + $this->data['options'] = $value; + } + + /** + * Set position. + * + * @param int $value Attribute position. + */ + public function set_position( $value ) { + $this->data['position'] = absint( $value ); + } + + /** + * Set if visible. + * + * @param bool $value If is visible on Product's additional info tab. + */ + public function set_visible( $value ) { + $this->data['visible'] = wc_string_to_bool( $value ); + } + + /** + * Set if variation. + * + * @param bool $value If is used for variations. + */ + public function set_variation( $value ) { + $this->data['variation'] = wc_string_to_bool( $value ); + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get the ID. + * + * @return int + */ + public function get_id() { + return $this->data['id']; + } + + /** + * Get name. + * + * @return string + */ + public function get_name() { + return $this->data['name']; + } + + /** + * Get options. + * + * @return array + */ + public function get_options() { + return $this->data['options']; + } + + /** + * Get position. + * + * @return int + */ + public function get_position() { + return $this->data['position']; + } + + /** + * Get if visible. + * + * @return bool + */ + public function get_visible() { + return $this->data['visible']; + } + + /** + * Get if variation. + * + * @return bool + */ + public function get_variation() { + return $this->data['variation']; + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * OffsetGet. + * + * @param string $offset Offset. + * @return mixed + */ + public function offsetGet( $offset ) { + switch ( $offset ) { + case 'is_variation': + return $this->get_variation() ? 1 : 0; + case 'is_visible': + return $this->get_visible() ? 1 : 0; + case 'is_taxonomy': + return $this->is_taxonomy() ? 1 : 0; + case 'value': + return $this->is_taxonomy() ? '' : wc_implode_text_attributes( $this->get_options() ); + default: + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + break; + } + return ''; + } + + /** + * OffsetSet. + * + * @param string $offset Offset. + * @param mixed $value Value. + */ + public function offsetSet( $offset, $value ) { + switch ( $offset ) { + case 'is_variation': + $this->set_variation( $value ); + break; + case 'is_visible': + $this->set_visible( $value ); + break; + case 'value': + $this->set_options( $value ); + break; + default: + if ( is_callable( array( $this, "set_$offset" ) ) ) { + return $this->{"set_$offset"}( $value ); + } + break; + } + } + + /** + * OffsetUnset. + * + * @param string $offset Offset. + */ + public function offsetUnset( $offset ) {} + + /** + * OffsetExists. + * + * @param string $offset Offset. + * @return bool + */ + public function offsetExists( $offset ) { + return in_array( $offset, array_merge( array( 'is_variation', 'is_visible', 'is_taxonomy', 'value' ), array_keys( $this->data ) ), true ); + } +} diff --git a/includes/class-wc-product-download.php b/includes/class-wc-product-download.php new file mode 100644 index 0000000..8ab4108 --- /dev/null +++ b/includes/class-wc-product-download.php @@ -0,0 +1,301 @@ + '', + 'name' => '', + 'file' => '', + ); + + /** + * Returns all data for this object. + * + * @return array + */ + public function get_data() { + return $this->data; + } + + /** + * Get allowed mime types. + * + * @return array + */ + public function get_allowed_mime_types() { + return apply_filters( 'woocommerce_downloadable_file_allowed_mime_types', get_allowed_mime_types() ); + } + + /** + * Get type of file path set. + * + * @param string $file_path optional. + * @return string absolute, relative, or shortcode. + */ + public function get_type_of_file_path( $file_path = '' ) { + $file_path = $file_path ? $file_path : $this->get_file(); + $parsed_url = parse_url( $file_path ); + if ( + $parsed_url && + isset( $parsed_url['host'] ) && // Absolute url means that it has a host. + ( // Theoretically we could permit any scheme (like ftp as well), but that has not been the case before. So we allow none or http(s). + ! isset( $parsed_url['scheme'] ) || + in_array( $parsed_url['scheme'], array( 'http', 'https' ) ) + ) + ) { + return 'absolute'; + } elseif ( '[' === substr( $file_path, 0, 1 ) && ']' === substr( $file_path, -1 ) ) { + return 'shortcode'; + } else { + return 'relative'; + } + } + + /** + * Get file type. + * + * @return string + */ + public function get_file_type() { + $type = wp_check_filetype( strtok( $this->get_file(), '?' ), $this->get_allowed_mime_types() ); + return $type['type']; + } + + /** + * Get file extension. + * + * @return string + */ + public function get_file_extension() { + $parsed_url = wp_parse_url( $this->get_file(), PHP_URL_PATH ); + return pathinfo( $parsed_url, PATHINFO_EXTENSION ); + } + + /** + * Check if file is allowed. + * + * @return boolean + */ + public function is_allowed_filetype() { + $file_path = $this->get_file(); + + // File types for URL-based files located on the server should get validated. + $parsed_file_path = WC_Download_Handler::parse_file_path( $file_path ); + $is_file_on_server = ! $parsed_file_path['remote_file']; + $file_path_type = $this->get_type_of_file_path( $file_path ); + + // Shortcodes are allowed, validations should be done by the shortcode provider in this case. + if ( 'shortcode' === $file_path_type ) { + return true; + } + + // Remote paths are allowed. + if ( ! $is_file_on_server && 'relative' !== $file_path_type ) { + return true; + } + + // On windows system, local files ending with `.` are not allowed. + // @link https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions. + if ( $is_file_on_server && ! $this->get_file_extension() && 'WIN' === strtoupper( substr( Constants::get_constant( 'PHP_OS' ), 0, 3 ) ) ) { + if ( '.' === substr( $file_path, -1 ) ) { + return false; + } + } + + return ! $this->get_file_extension() || in_array( $this->get_file_type(), $this->get_allowed_mime_types(), true ); + } + + /** + * Validate file exists. + * + * @return boolean + */ + public function file_exists() { + if ( 'relative' !== $this->get_type_of_file_path() ) { + return true; + } + $file_url = $this->get_file(); + if ( '..' === substr( $file_url, 0, 2 ) || '/' !== substr( $file_url, 0, 1 ) ) { + $file_url = realpath( ABSPATH . $file_url ); + } elseif ( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) === substr( $file_url, 0, strlen( substr( WP_CONTENT_DIR, strlen( untrailingslashit( ABSPATH ) ) ) ) ) ) { + $file_url = realpath( WP_CONTENT_DIR . substr( $file_url, 11 ) ); + } + return apply_filters( 'woocommerce_downloadable_file_exists', file_exists( $file_url ), $this->get_file() ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set ID. + * + * @param string $value Download ID. + */ + public function set_id( $value ) { + $this->data['id'] = wc_clean( $value ); + } + + /** + * Set name. + * + * @param string $value Download name. + */ + public function set_name( $value ) { + $this->data['name'] = wc_clean( $value ); + } + + /** + * Set previous_hash. + * + * @deprecated 3.3.0 No longer using filename based hashing to keep track of files. + * @param string $value Previous hash. + */ + public function set_previous_hash( $value ) { + wc_deprecated_function( __FUNCTION__, '3.3' ); + $this->data['previous_hash'] = wc_clean( $value ); + } + + /** + * Set file. + * + * @param string $value File URL/Path. + */ + public function set_file( $value ) { + // A `///` is recognized as an "absolute", but on the filesystem, so it bypasses the mime check in `self::is_allowed_filetype`. + // This will strip extra prepending / to the maximum of 2. + if ( preg_match( '#^//+(/[^/].+)$#i', $value, $matches ) ) { + $value = $matches[1]; + } + switch ( $this->get_type_of_file_path( $value ) ) { + case 'absolute': + $this->data['file'] = esc_url_raw( $value ); + break; + default: + $this->data['file'] = wc_clean( $value ); + break; + } + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get id. + * + * @return string + */ + public function get_id() { + return $this->data['id']; + } + + /** + * Get name. + * + * @return string + */ + public function get_name() { + return $this->data['name']; + } + + /** + * Get previous_hash. + * + * @deprecated 3.3.0 No longer using filename based hashing to keep track of files. + * @return string + */ + public function get_previous_hash() { + wc_deprecated_function( __FUNCTION__, '3.3' ); + return $this->data['previous_hash']; + } + + /** + * Get file. + * + * @return string + */ + public function get_file() { + return $this->data['file']; + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess/Backwards compatibility. + |-------------------------------------------------------------------------- + */ + + /** + * OffsetGet. + * + * @param string $offset Offset. + * @return mixed + */ + public function offsetGet( $offset ) { + switch ( $offset ) { + default: + if ( is_callable( array( $this, "get_$offset" ) ) ) { + return $this->{"get_$offset"}(); + } + break; + } + return ''; + } + + /** + * OffsetSet. + * + * @param string $offset Offset. + * @param mixed $value Offset value. + */ + public function offsetSet( $offset, $value ) { + switch ( $offset ) { + default: + if ( is_callable( array( $this, "set_$offset" ) ) ) { + return $this->{"set_$offset"}( $value ); + } + break; + } + } + + /** + * OffsetUnset. + * + * @param string $offset Offset. + */ + public function offsetUnset( $offset ) {} + + /** + * OffsetExists. + * + * @param string $offset Offset. + * @return bool + */ + public function offsetExists( $offset ) { + return in_array( $offset, array_keys( $this->data ), true ); + } +} diff --git a/includes/class-wc-product-external.php b/includes/class-wc-product-external.php new file mode 100644 index 0000000..f88d45f --- /dev/null +++ b/includes/class-wc-product-external.php @@ -0,0 +1,194 @@ + '', + '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 What the value is for. Valid values are 'view' and 'edit'. + * @return string + */ + public function get_product_url( $context = 'view' ) { + return esc_url_raw( $this->get_prop( 'product_url', $context ) ); + } + + /** + * Get button text. + * + * @param string $context What the value is for. Valid values are 'view' and 'edit'. + * @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', htmlspecialchars_decode( $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 $manage_stock If manage stock. + */ + 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 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' ) ); + } + } + + /** + * External 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. + * + * @access public + * @return bool + */ + public function is_purchasable() { + return apply_filters( 'woocommerce_is_purchasable', false, $this ); + } + + /** + * Get the add to url used mainly in loops. + * + * @access public + * @return string + */ + public function add_to_cart_url() { + return apply_filters( 'woocommerce_product_add_to_cart_url', $this->get_product_url(), $this ); + } + + /** + * 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->get_button_text() : _x( 'Buy product', 'placeholder', 'woocommerce' ), $this ); + } + + /** + * 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', $this->get_button_text() ? $this->get_button_text() : _x( 'Buy product', 'placeholder', 'woocommerce' ), $this ); + } + + /** + * Get the add to cart button text description - used in aria tags. + * + * @since 3.3.0 + * @return string + */ + public function add_to_cart_description() { + /* translators: %s: Product title */ + return apply_filters( 'woocommerce_product_add_to_cart_description', $this->get_button_text() ? $this->get_button_text() : sprintf( __( 'Buy “%s”', 'woocommerce' ), $this->get_name() ), $this ); + } +} diff --git a/includes/class-wc-product-factory.php b/includes/class-wc-product-factory.php new file mode 100644 index 0000000..9344f67 --- /dev/null +++ b/includes/class-wc-product-factory.php @@ -0,0 +1,119 @@ +get_product_id( $product_id ); + + if ( ! $product_id ) { + return false; + } + + $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'] ); + } + } + + $classname = $this->get_product_classname( $product_id, $product_type ); + + 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 Product ID. + * @param string $product_type 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 $classname; + } + + /** + * Get the product type for a product. + * + * @since 3.0.0 + * @param int $product_id 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 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 WC_Product|WP_Post|int|bool $product Product instance, post instance, numeric or false to use global $post. + * @return int|bool false on failure + */ + private function get_product_id( $product ) { + global $post; + + if ( false === $product && isset( $post, $post->ID ) && 'product' === get_post_type( $post->ID ) ) { + return absint( $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 new file mode 100644 index 0000000..e2b19d0 --- /dev/null +++ b/includes/class-wc-product-grouped.php @@ -0,0 +1,193 @@ + array(), + ); + + /** + * Get internal type. + * + * @return string + */ + public function get_type() { + return 'grouped'; + } + + /** + * Get the add to cart button text. + * + * @return string + */ + public function add_to_cart_text() { + return apply_filters( 'woocommerce_product_add_to_cart_text', __( 'View products', 'woocommerce' ), $this ); + } + + /** + * Get the add to cart button text description - used in aria tags. + * + * @since 3.3.0 + * @return string + */ + public function add_to_cart_description() { + /* translators: %s: Product title */ + return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'View products in the “%s” group', 'woocommerce' ), $this->get_name() ), $this ); + } + + /** + * Returns whether or not the product is on sale. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function is_on_sale( $context = 'view' ) { + $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_purchasable() && ! $child->has_child() && $child->is_on_sale() ) { + $on_sale = true; + break; + } + } + + return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; + } + + /** + * Returns false if the product cannot be bought. + * + * @return bool + */ + public function is_purchasable() { + return apply_filters( 'woocommerce_is_purchasable', false, $this ); + } + + /** + * Returns the price in html format. + * + * @param string $price (default: ''). + * @return string + */ + 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 ( $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 ); + $max_price = max( $child_prices ); + } else { + $min_price = ''; + $max_price = ''; + } + + if ( '' !== $min_price ) { + if ( $min_price !== $max_price ) { + $price = wc_format_price_range( $min_price, $max_price ); + } else { + $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 { + $price = apply_filters( 'woocommerce_grouped_price_html', $price . $this->get_price_suffix(), $this, $child_prices ); + } + } 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 What the value is for. Valid values are view and edit. + * @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 List of product 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 product object will be saved to the DB before returning it. + * @return WC_Product Synced product object. + */ + public static function sync( $product, $save = true ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + if ( is_a( $product, 'WC_Product_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-query.php b/includes/class-wc-product-query.php new file mode 100644 index 0000000..8562a1f --- /dev/null +++ b/includes/class-wc-product-query.php @@ -0,0 +1,79 @@ + array( 'draft', 'pending', 'private', 'publish' ), + 'type' => array_merge( array_keys( wc_get_product_types() ) ), + 'limit' => get_option( 'posts_per_page' ), + 'include' => array(), + 'date_created' => '', + 'date_modified' => '', + 'featured' => '', + 'visibility' => '', + 'sku' => '', + 'price' => '', + 'regular_price' => '', + 'sale_price' => '', + 'date_on_sale_from' => '', + 'date_on_sale_to' => '', + 'total_sales' => '', + 'tax_status' => '', + 'tax_class' => '', + 'manage_stock' => '', + 'stock_quantity' => '', + 'stock_status' => '', + 'backorders' => '', + 'low_stock_amount' => '', + 'sold_individually' => '', + 'weight' => '', + 'length' => '', + 'width' => '', + 'height' => '', + 'reviews_allowed' => '', + 'virtual' => '', + 'downloadable' => '', + 'category' => array(), + 'tag' => array(), + 'shipping_class' => array(), + 'download_limit' => '', + 'download_expiry' => '', + 'average_rating' => '', + 'review_count' => '', + ) + ); + } + + /** + * Get products matching the current query vars. + * + * @return array|object of WC_Product objects + */ + public function get_products() { + $args = apply_filters( 'woocommerce_product_object_query_args', $this->get_query_vars() ); + $results = WC_Data_Store::load( 'product' )->query( $args ); + return apply_filters( 'woocommerce_product_object_query', $results, $args ); + } +} diff --git a/includes/class-wc-product-simple.php b/includes/class-wc-product-simple.php new file mode 100644 index 0000000..6dfebfc --- /dev/null +++ b/includes/class-wc-product-simple.php @@ -0,0 +1,77 @@ +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. + * + * @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( + 'add-to-cart' => $this->get_id(), + ), + ( function_exists( 'is_feed' ) && is_feed() ) || ( function_exists( 'is_404' ) && is_404() ) ? $this->get_permalink() : '' + ) + ) : $this->get_permalink(); + return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this ); + } + + /** + * Get the add to cart button text. + * + * @return string + */ + public function add_to_cart_text() { + $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 add to cart button text description - used in aria tags. + * + * @since 3.3.0 + * @return string + */ + public function add_to_cart_description() { + /* translators: %s: Product title */ + $text = $this->is_purchasable() && $this->is_in_stock() ? __( 'Add “%s” to your cart', 'woocommerce' ) : __( 'Read more about “%s”', 'woocommerce' ); + + return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( $text, $this->get_name() ), $this ); + } +} diff --git a/includes/class-wc-product-variable.php b/includes/class-wc-product-variable.php new file mode 100644 index 0000000..a240c40 --- /dev/null +++ b/includes/class-wc-product-variable.php @@ -0,0 +1,659 @@ +is_purchasable() ? __( 'Select options', 'woocommerce' ) : __( 'Read more', 'woocommerce' ), $this ); + } + + /** + * Get the add to cart button text description - used in aria tags. + * + * @since 3.3.0 + * @return string + */ + public function add_to_cart_description() { + /* translators: %s: Product title */ + return apply_filters( 'woocommerce_product_add_to_cart_description', sprintf( __( 'Select options for “%s”', 'woocommerce' ), $this->get_name() ), $this ); + } + + /** + * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. + * + * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * @return array Array of RAW prices, regular prices, and sale prices with keys set to variation ID. + */ + public function get_variation_prices( $for_display = false ) { + $prices = $this->data_store->read_price_data( $this, $for_display ); + + foreach ( $prices as $price_key => $variation_prices ) { + $prices[ $price_key ] = $this->sort_variation_prices( $variation_prices ); + } + + return $prices; + } + + /** + * Get the min or max variation regular price. + * + * @param string $min_or_max Min or max price. + * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * @return string + */ + public function get_variation_regular_price( $min_or_max = 'min', $for_display = false ) { + $prices = $this->get_variation_prices( $for_display ); + $price = 'min' === $min_or_max ? current( $prices['regular_price'] ) : end( $prices['regular_price'] ); + + return apply_filters( 'woocommerce_get_variation_regular_price', $price, $this, $min_or_max, $for_display ); + } + + /** + * Get the min or max variation sale price. + * + * @param string $min_or_max Min or max price. + * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * @return string + */ + public function get_variation_sale_price( $min_or_max = 'min', $for_display = false ) { + $prices = $this->get_variation_prices( $for_display ); + $price = 'min' === $min_or_max ? current( $prices['sale_price'] ) : end( $prices['sale_price'] ); + + return apply_filters( 'woocommerce_get_variation_sale_price', $price, $this, $min_or_max, $for_display ); + } + + /** + * Get the min or max variation (active) price. + * + * @param string $min_or_max Min or max price. + * @param boolean $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * @return string + */ + public function get_variation_price( $min_or_max = 'min', $for_display = false ) { + $prices = $this->get_variation_prices( $for_display ); + $price = 'min' === $min_or_max ? current( $prices['price'] ) : end( $prices['price'] ); + + return apply_filters( 'woocommerce_get_variation_price', $price, $this, $min_or_max, $for_display ); + } + + /** + * Returns the price in html format. + * + * Note: Variable prices do not show suffixes like other product types. This + * is due to some things like tax classes being set at variation level which + * could differ from the parent price. The only way to show accurate prices + * would be to load the variation and get it's price, which adds extra + * overhead and still has edge cases where the values would be inaccurate. + * + * Additionally, ranges of prices no longer show 'striked out' sale prices + * due to the strings being very long and unclear/confusing. A single range + * is shown instead. + * + * @param string $price Price (default: ''). + * @return string + */ + public function get_price_html( $price = '' ) { + $prices = $this->get_variation_prices( true ); + + if ( empty( $prices['price'] ) ) { + $price = apply_filters( 'woocommerce_variable_empty_price_html', '', $this ); + } else { + $min_price = current( $prices['price'] ); + $max_price = end( $prices['price'] ); + $min_reg_price = current( $prices['regular_price'] ); + $max_reg_price = end( $prices['regular_price'] ); + + if ( $min_price !== $max_price ) { + $price = wc_format_price_range( $min_price, $max_price ); + } elseif ( $this->is_on_sale() && $min_reg_price === $max_reg_price ) { + $price = wc_format_sale_price( wc_price( $max_reg_price ), wc_price( $min_price ) ); + } else { + $price = wc_price( $min_price ); + } + + $price = apply_filters( 'woocommerce_variable_price_html', $price . $this->get_price_suffix(), $this ); + } + + return apply_filters( 'woocommerce_get_price_html', $price, $this ); + } + + /** + * Get the suffix to display after prices > 0. + * + * This is skipped if the suffix + * has dynamic values such as {price_excluding_tax} for variable products. + * + * @see get_price_html for an explanation as to why. + * @param string $price Price to calculate, left blank to just use get_price(). + * @param integer $qty Quantity passed on to get_price_including_tax() or get_price_excluding_tax(). + * @return string + */ + public function get_price_suffix( $price = '', $qty = 1 ) { + $suffix = get_option( 'woocommerce_price_display_suffix' ); + + if ( strstr( $suffix, '{' ) ) { + return apply_filters( 'woocommerce_get_price_suffix', '', $this, $price, $qty ); + } else { + return parent::get_price_suffix( $price, $qty ); + } + } + + /** + * Return a products child ids. + * + * This is lazy loaded as it's not used often and does require several queries. + * + * @param bool|string $visible_only Visible only. + * @return array Children ids + */ + public function get_children( $visible_only = '' ) { + if ( is_bool( $visible_only ) ) { + wc_deprecated_argument( 'visible_only', '3.0', 'WC_Product_Variable::get_visible_children' ); + + return $visible_only ? $this->get_visible_children() : $this->get_children(); + } + + if ( null === $this->children ) { + $children = $this->data_store->read_children( $this ); + $this->set_children( $children['all'] ); + $this->set_visible_children( $children['visible'] ); + } + + return apply_filters( 'woocommerce_get_children', $this->children, $this, false ); + } + + /** + * Return a products child ids - visible only. + * + * This is lazy loaded as it's not used often and does require several queries. + * + * @since 3.0.0 + * @return array Children ids + */ + public function get_visible_children() { + if ( null === $this->visible_children ) { + $children = $this->data_store->read_children( $this ); + $this->set_children( $children['all'] ); + $this->set_visible_children( $children['visible'] ); + } + return apply_filters( 'woocommerce_get_children', $this->visible_children, $this, true ); + } + + /** + * Return an array of attributes used for variations, as well as their possible values. + * + * This is lazy loaded as it's not used often and does require several queries. + * + * @return array Attributes and their available values + */ + public function get_variation_attributes() { + if ( null === $this->variation_attributes ) { + $this->variation_attributes = $this->data_store->read_variation_attributes( $this ); + } + return $this->variation_attributes; + } + + /** + * If set, get the default attributes for a variable product. + * + * @param string $attribute_name Attribute name. + * @return string + */ + public function get_variation_default_attribute( $attribute_name ) { + $defaults = $this->get_default_attributes(); + $attribute_name = sanitize_title( $attribute_name ); + + return isset( $defaults[ $attribute_name ] ) ? $defaults[ $attribute_name ] : ''; + } + + /** + * Variable products themselves cannot be downloadable. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function get_downloadable( $context = 'view' ) { + return false; + } + + /** + * Variable products themselves cannot be virtual. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return bool + */ + public function get_virtual( $context = 'view' ) { + return false; + } + + /** + * Get an array of available variations for the current product. + * + * @param string $return Optional. The format to return the results in. Can be 'array' to return an array of variation data or 'objects' for the product objects. Default 'array'. + * + * @return array[]|WC_Product_Variation[] + */ + public function get_available_variations( $return = 'array' ) { + $variation_ids = $this->get_children(); + $available_variations = array(); + + if ( is_callable( '_prime_post_caches' ) ) { + _prime_post_caches( $variation_ids ); + } + + foreach ( $variation_ids as $variation_id ) { + + $variation = wc_get_product( $variation_id ); + + // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. + if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { + continue; + } + + // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price). + if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { + continue; + } + + if ( 'array' === $return ) { + $available_variations[] = $this->get_available_variation( $variation ); + } else { + $available_variations[] = $variation; + } + } + + if ( 'array' === $return ) { + $available_variations = array_values( array_filter( $available_variations ) ); + } + + return $available_variations; + } + + /** + * Check if a given variation is currently available. + * + * @param WC_Product_Variation $variation Variation to check. + * + * @return bool True if the variation is available, false otherwise. + */ + private function variation_is_available( WC_Product_Variation $variation ) { + // Hide out of stock variations if 'Hide out of stock items from the catalog' is checked. + if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) { + return false; + } + + // Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price). + if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) { + return false; + } + + return true; + } + + /** + * Returns an array of data for a variation. Used in the add to cart form. + * + * @since 2.4.0 + * @param WC_Product $variation Variation product object or ID. + * @return array|bool + */ + public function get_available_variation( $variation ) { + if ( is_numeric( $variation ) ) { + $variation = wc_get_product( $variation ); + } + if ( ! $variation instanceof WC_Product_Variation ) { + return false; + } + // See if prices should be shown for each variation after selection. + $show_variation_price = apply_filters( 'woocommerce_show_variation_price', $variation->get_price() === '' || $this->get_variation_sale_price( 'min' ) !== $this->get_variation_sale_price( 'max' ) || $this->get_variation_regular_price( 'min' ) !== $this->get_variation_regular_price( 'max' ), $this, $variation ); + + return apply_filters( + 'woocommerce_available_variation', + array( + 'attributes' => $variation->get_variation_attributes(), + 'availability_html' => wc_get_stock_html( $variation ), + 'backorders_allowed' => $variation->backorders_allowed(), + 'dimensions' => $variation->get_dimensions( false ), + 'dimensions_html' => wc_format_dimensions( $variation->get_dimensions( false ) ), + 'display_price' => wc_get_price_to_display( $variation ), + 'display_regular_price' => wc_get_price_to_display( $variation, array( 'price' => $variation->get_regular_price() ) ), + 'image' => wc_get_product_attachment_props( $variation->get_image_id() ), + 'image_id' => $variation->get_image_id(), + 'is_downloadable' => $variation->is_downloadable(), + 'is_in_stock' => $variation->is_in_stock(), + 'is_purchasable' => $variation->is_purchasable(), + 'is_sold_individually' => $variation->is_sold_individually() ? 'yes' : 'no', + 'is_virtual' => $variation->is_virtual(), + 'max_qty' => 0 < $variation->get_max_purchase_quantity() ? $variation->get_max_purchase_quantity() : '', + 'min_qty' => $variation->get_min_purchase_quantity(), + 'price_html' => $show_variation_price ? '' . $variation->get_price_html() . '' : '', + 'sku' => $variation->get_sku(), + 'variation_description' => wc_format_content( $variation->get_description() ), + 'variation_id' => $variation->get_id(), + 'variation_is_active' => $variation->variation_is_active(), + 'variation_is_visible' => $variation->variation_is_visible(), + 'weight' => $variation->get_weight(), + 'weight_html' => wc_format_weight( $variation->get_weight() ), + ), + $this, + $variation + ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Sets an array of variation attributes. + * + * @since 3.0.0 + * @param array $variation_attributes Attributes list. + */ + public function set_variation_attributes( $variation_attributes ) { + $this->variation_attributes = $variation_attributes; + } + + /** + * Sets an array of children for the product. + * + * @since 3.0.0 + * @param array $children Children products. + */ + public function set_children( $children ) { + $this->children = array_filter( wp_parse_id_list( (array) $children ) ); + } + + /** + * Sets an array of visible children only. + * + * @since 3.0.0 + * @param array $visible_children List of visible children products. + */ + public function set_visible_children( $visible_children ) { + $this->visible_children = array_filter( wp_parse_id_list( (array) $visible_children ) ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + */ + + /** + * Ensure properties are set correctly before save. + * + * @since 3.0.0 + */ + public function validate_props() { + parent::validate_props(); + + if ( ! $this->get_manage_stock() ) { + $this->data_store->sync_stock_status( $this ); + } + } + + /** + * Do any extra processing needed before the actual product save + * (but after triggering the 'woocommerce_before_..._object_save' action) + * + * @return mixed A state value that will be passed to after_data_store_save_or_update. + */ + protected function before_data_store_save_or_update() { + // Get names before save. + $previous_name = $this->data['name']; + $new_name = $this->get_name( 'edit' ); + + return array( + 'previous_name' => $previous_name, + 'new_name' => $new_name, + ); + } + + /** + * Do any extra processing needed after the actual product save + * (but before triggering the 'woocommerce_after_..._object_save' action) + * + * @param mixed $state The state object that was returned by before_data_store_save_or_update. + */ + protected function after_data_store_save_or_update( $state ) { + $this->data_store->sync_variation_names( $this, $state['previous_name'], $state['new_name'] ); + $this->data_store->sync_managed_variation_stock_status( $this ); + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + */ + + /** + * Returns whether or not the product is on sale. + * + * @param string $context What the value is for. Valid values are view and edit. What the value is for. Valid values are view and edit. + * @return bool + */ + public function is_on_sale( $context = 'view' ) { + $prices = $this->get_variation_prices(); + $on_sale = $prices['regular_price'] !== $prices['sale_price'] && $prices['sale_price'] === $prices['price']; + + return 'view' === $context ? apply_filters( 'woocommerce_product_is_on_sale', $on_sale, $this ) : $on_sale; + } + + /** + * Is a child in stock? + * + * @return boolean + */ + public function child_is_in_stock() { + return $this->data_store->child_is_in_stock( $this ); + } + + /** + * Is a child on backorder? + * + * @since 3.3.0 + * @return boolean + */ + public function child_is_on_backorder() { + return $this->data_store->child_has_stock_status( $this, 'onbackorder' ); + } + + /** + * Does a child have a weight set? + * + * @return boolean + */ + public function child_has_weight() { + $transient_name = 'wc_child_has_weight_' . $this->get_id(); + $has_weight = get_transient( $transient_name ); + + if ( false === $has_weight ) { + $has_weight = $this->data_store->child_has_weight( $this ); + set_transient( $transient_name, (int) $has_weight, DAY_IN_SECONDS * 30 ); + } + + return (bool) $has_weight; + } + + /** + * Does a child have dimensions set? + * + * @return boolean + */ + public function child_has_dimensions() { + $transient_name = 'wc_child_has_dimensions_' . $this->get_id(); + $has_dimension = get_transient( $transient_name ); + + if ( false === $has_dimension ) { + $has_dimension = $this->data_store->child_has_dimensions( $this ); + set_transient( $transient_name, (int) $has_dimension, DAY_IN_SECONDS * 30 ); + } + + return (bool) $has_dimension; + } + + /** + * Returns whether or not the product has dimensions set. + * + * @return bool + */ + public function has_dimensions() { + return parent::has_dimensions() || $this->child_has_dimensions(); + } + + /** + * Returns whether or not the product has weight set. + * + * @return bool + */ + public function has_weight() { + return parent::has_weight() || $this->child_has_weight(); + } + + /** + * Returns whether or not the product has additional options that need + * selecting before adding to cart. + * + * @since 3.0.0 + * @return boolean + */ + public function has_options() { + return apply_filters( 'woocommerce_product_has_options', true, $this ); + } + + + /* + |-------------------------------------------------------------------------- + | Sync with child variations. + |-------------------------------------------------------------------------- + */ + + /** + * Sync a variable product with it's children. These sync functions sync + * upwards (from child to parent) when the variation is saved. + * + * @param WC_Product|int $product Product object or ID for which you wish to sync. + * @param bool $save If true, the product object will be saved to the DB before returning it. + * @return WC_Product Synced product object. + */ + public static function sync( $product, $save = true ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + if ( is_a( $product, 'WC_Product_Variable' ) ) { + $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); + $data_store->sync_price( $product ); + $data_store->sync_stock_status( $product ); + self::sync_attributes( $product ); // Legacy update of attributes. + + do_action( 'woocommerce_variable_product_sync_data', $product ); + + if ( $save ) { + $product->save(); + } + + wc_do_deprecated_action( + 'woocommerce_variable_product_sync', + array( + $product->get_id(), + $product->get_visible_children(), + ), + '3.0', + 'woocommerce_variable_product_sync_data, woocommerce_new_product or woocommerce_update_product' + ); + } + + return $product; + } + + /** + * Sync parent stock status with the status of all children and save. + * + * @param WC_Product|int $product Product object or ID for which you wish to sync. + * @param bool $save If true, the product object will be saved to the DB before returning it. + * @return WC_Product Synced product object. + */ + public static function sync_stock_status( $product, $save = true ) { + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = wc_get_product( $product ); + } + if ( is_a( $product, 'WC_Product_Variable' ) ) { + $data_store = WC_Data_Store::load( 'product-' . $product->get_type() ); + $data_store->sync_stock_status( $product ); + + if ( $save ) { + $product->save(); + } + } + + return $product; + } + + /** + * Sort an associative array of $variation_id => $price pairs in order of min and max prices. + * + * @param array $prices associative array of $variation_id => $price pairs. + * @return array + */ + protected function sort_variation_prices( $prices ) { + asort( $prices ); + + return $prices; + } +} diff --git a/includes/class-wc-product-variation.php b/includes/class-wc-product-variation.php new file mode 100644 index 0000000..83b80fc --- /dev/null +++ b/includes/class-wc-product-variation.php @@ -0,0 +1,586 @@ + '', + 'sku' => '', + 'manage_stock' => '', + 'backorders' => '', + 'stock_quantity' => '', + 'weight' => '', + 'length' => '', + 'width' => '', + 'height' => '', + 'tax_class' => '', + 'shipping_class_id' => '', + 'image_id' => '', + 'purchase_note' => '', + ); + + /** + * Override the default constructor to set custom defaults. + * + * @param int|WC_Product|object $product Product to init. + */ + public function __construct( $product = 0 ) { + $this->data['tax_class'] = 'parent'; + $this->data['attribute_summary'] = ''; + parent::__construct( $product ); + } + + /** + * Prefix for action and filter hooks on data. + * + * @since 3.0.0 + * @return string + */ + protected function get_hook_prefix() { + return 'woocommerce_product_variation_get_'; + } + + /** + * Get internal type. + * + * @return string + */ + public function get_type() { + return 'variation'; + } + + /** + * If the stock level comes from another product ID. + * + * @since 3.0.0 + * @return int + */ + public function get_stock_managed_by_id() { + return 'parent' === $this->get_manage_stock() ? $this->get_parent_id() : $this->get_id(); + } + + /** + * Get the product's title. For variations this is the parent product name. + * + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_product_title', $this->parent_data['title'], $this ); + } + + /** + * Get product name with SKU or ID. Used within admin. + * + * @return string Formatted product name + */ + public function get_formatted_name() { + if ( $this->get_sku() ) { + $identifier = $this->get_sku(); + } else { + $identifier = '#' . $this->get_id(); + } + + $formatted_variation_list = wc_get_formatted_variation( $this, true, true, true ); + + return sprintf( '%2$s (%1$s)', $identifier, $this->get_name() ) . '' . $formatted_variation_list . ''; + } + + /** + * Get variation attribute values. Keys are prefixed with attribute_, as stored, unless $with_prefix is false. + * + * @param bool $with_prefix Whether keys should be prepended with attribute_ or not, default is true. + * @return array of attributes and their values for this variation. + */ + public function get_variation_attributes( $with_prefix = true ) { + $attributes = $this->get_attributes(); + $variation_attributes = array(); + $prefix = $with_prefix ? 'attribute_' : ''; + + foreach ( $attributes as $key => $value ) { + $variation_attributes[ $prefix . $key ] = $value; + } + return $variation_attributes; + } + + /** + * Returns a single product attribute as a string. + * + * @param string $attribute to get. + * @return string + */ + public function get_attribute( $attribute ) { + $attributes = $this->get_attributes(); + $attribute = sanitize_title( $attribute ); + + if ( isset( $attributes[ $attribute ] ) ) { + $value = $attributes[ $attribute ]; + $term = taxonomy_exists( $attribute ) ? get_term_by( 'slug', $value, $attribute ) : false; + return ! is_wp_error( $term ) && $term ? $term->name : $value; + } + + $att_str = 'pa_' . $attribute; + if ( isset( $attributes[ $att_str ] ) ) { + $value = $attributes[ $att_str ]; + $term = taxonomy_exists( $att_str ) ? get_term_by( 'slug', $value, $att_str ) : false; + return ! is_wp_error( $term ) && $term ? $term->name : $value; + } + + return ''; + } + + /** + * Wrapper for get_permalink. Adds this variations attributes to the URL. + * + * @param array|null $item_object item array If a cart or order item is passed, we can get a link containing the exact attributes selected for the variation, rather than the default attributes. + * @return string + */ + public function get_permalink( $item_object = null ) { + $url = get_permalink( $this->get_parent_id() ); + + if ( ! empty( $item_object['variation'] ) ) { + $data = $item_object['variation']; + } elseif ( ! empty( $item_object['item_meta_array'] ) ) { + $data_keys = array_map( 'wc_variation_attribute_name', wp_list_pluck( $item_object['item_meta_array'], 'key' ) ); + $data_values = wp_list_pluck( $item_object['item_meta_array'], 'value' ); + $data = array_intersect_key( array_combine( $data_keys, $data_values ), $this->get_variation_attributes() ); + } else { + $data = $this->get_variation_attributes(); + } + + $data = array_filter( $data, 'wc_array_filter_default_attributes' ); + + if ( empty( $data ) ) { + return $url; + } + + // Filter and encode keys and values so this is not broken by add_query_arg. + $data = array_map( 'urlencode', $data ); + $keys = array_map( 'urlencode', array_keys( $data ) ); + + return add_query_arg( array_combine( $keys, $data ), $url ); + } + + /** + * Get the add to url used mainly in loops. + * + * @return string + */ + public function add_to_cart_url() { + $url = $this->is_purchasable() ? remove_query_arg( + 'added-to-cart', + add_query_arg( + array( + 'variation_id' => $this->get_id(), + 'add-to-cart' => $this->get_parent_id(), + ), + $this->get_permalink() + ) + ) : $this->get_permalink(); + return apply_filters( 'woocommerce_product_add_to_cart_url', $url, $this ); + } + + /** + * Get SKU (Stock-keeping unit) - product unique ID. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_sku( $context = 'view' ) { + $value = $this->get_prop( 'sku', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'sku', $this->parent_data['sku'], $this ); + } + return $value; + } + + /** + * Returns the product's weight. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_weight( $context = 'view' ) { + $value = $this->get_prop( 'weight', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'weight', $this->parent_data['weight'], $this ); + } + return $value; + } + + /** + * Returns the product length. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_length( $context = 'view' ) { + $value = $this->get_prop( 'length', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'length', $this->parent_data['length'], $this ); + } + return $value; + } + + /** + * Returns the product width. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_width( $context = 'view' ) { + $value = $this->get_prop( 'width', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'width', $this->parent_data['width'], $this ); + } + return $value; + } + + /** + * Returns the product height. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_height( $context = 'view' ) { + $value = $this->get_prop( 'height', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'height', $this->parent_data['height'], $this ); + } + return $value; + } + + /** + * Returns the tax class. + * + * Does not use get_prop so it can handle 'parent' inheritance correctly. + * + * @param string $context view, edit, or unfiltered. + * @return string + */ + public function get_tax_class( $context = 'view' ) { + $value = null; + + if ( array_key_exists( 'tax_class', $this->data ) ) { + $value = array_key_exists( 'tax_class', $this->changes ) ? $this->changes['tax_class'] : $this->data['tax_class']; + + if ( 'edit' !== $context && 'parent' === $value ) { + $value = $this->parent_data['tax_class']; + } + + if ( 'view' === $context ) { + $value = apply_filters( $this->get_hook_prefix() . 'tax_class', $value, $this ); + } + } + return $value; + } + + /** + * Return if product manage stock. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return boolean|string true, false, or parent. + */ + public function get_manage_stock( $context = 'view' ) { + $value = $this->get_prop( 'manage_stock', $context ); + + // Inherit value from parent. + if ( 'view' === $context && false === $value && true === wc_string_to_bool( $this->parent_data['manage_stock'] ) ) { + $value = 'parent'; + } + return $value; + } + + /** + * Returns number of items available for sale. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return int|null + */ + public function get_stock_quantity( $context = 'view' ) { + $value = $this->get_prop( 'stock_quantity', $context ); + + // Inherit value from parent. + if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { + $value = apply_filters( $this->get_hook_prefix() . 'stock_quantity', $this->parent_data['stock_quantity'], $this ); + } + return $value; + } + + /** + * Get backorders. + * + * @param string $context What the value is for. Valid values are view and edit. + * @since 3.0.0 + * @return string yes no or notify + */ + public function get_backorders( $context = 'view' ) { + $value = $this->get_prop( 'backorders', $context ); + + // Inherit value from parent. + if ( 'view' === $context && 'parent' === $this->get_manage_stock() ) { + $value = apply_filters( $this->get_hook_prefix() . 'backorders', $this->parent_data['backorders'], $this ); + } + return $value; + } + + /** + * Get main image ID. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_image_id( $context = 'view' ) { + $image_id = $this->get_prop( 'image_id', $context ); + + if ( 'view' === $context && ! $image_id ) { + $image_id = apply_filters( $this->get_hook_prefix() . 'image_id', $this->parent_data['image_id'], $this ); + } + + return $image_id; + } + + /** + * Get purchase note. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_purchase_note( $context = 'view' ) { + $value = $this->get_prop( 'purchase_note', $context ); + + // Inherit value from parent. + if ( 'view' === $context && empty( $value ) ) { + $value = apply_filters( $this->get_hook_prefix() . 'purchase_note', $this->parent_data['purchase_note'], $this ); + } + return $value; + } + + /** + * Get shipping class ID. + * + * @since 3.0.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return int + */ + public function get_shipping_class_id( $context = 'view' ) { + $shipping_class_id = $this->get_prop( 'shipping_class_id', $context ); + + if ( 'view' === $context && ! $shipping_class_id ) { + $shipping_class_id = apply_filters( $this->get_hook_prefix() . 'shipping_class_id', $this->parent_data['shipping_class_id'], $this ); + } + + return $shipping_class_id; + } + + /** + * Get catalog visibility. + * + * @param string $context What the value is for. Valid values are view and edit. + * @return string + */ + public function get_catalog_visibility( $context = 'view' ) { + return apply_filters( $this->get_hook_prefix() . 'catalog_visibility', $this->parent_data['catalog_visibility'], $this ); + } + + /** + * Get attribute summary. + * + * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. + * + * @param string $context What the value is for. Valid values are view and edit. + * + * @since 3.6.0 + * @return string + */ + public function get_attribute_summary( $context = 'view' ) { + return $this->get_prop( 'attribute_summary', $context ); + } + + + /** + * Set attribute summary. + * + * By default, attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. + * + * @since 3.6.0 + * @param string $attribute_summary Summary of attribute names and values assigned to the variation. + */ + public function set_attribute_summary( $attribute_summary ) { + $this->set_prop( 'attribute_summary', $attribute_summary ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD methods + |-------------------------------------------------------------------------- + */ + + /** + * Set the parent data array for this variation. + * + * @since 3.0.0 + * @param array $parent_data parent data array for this variation. + */ + public function set_parent_data( $parent_data ) { + $parent_data = wp_parse_args( + $parent_data, + array( + 'title' => '', + 'status' => '', + 'sku' => '', + 'manage_stock' => 'no', + 'backorders' => 'no', + 'stock_quantity' => '', + 'weight' => '', + 'length' => '', + 'width' => '', + 'height' => '', + 'tax_class' => '', + 'shipping_class_id' => 0, + 'image_id' => 0, + 'purchase_note' => '', + 'catalog_visibility' => 'visible', + ) + ); + + // Normalize tax class. + $parent_data['tax_class'] = sanitize_title( $parent_data['tax_class'] ); + $parent_data['tax_class'] = 'standard' === $parent_data['tax_class'] ? '' : $parent_data['tax_class']; + $valid_classes = $this->get_valid_tax_classes(); + + if ( ! in_array( $parent_data['tax_class'], $valid_classes, true ) ) { + $parent_data['tax_class'] = ''; + } + + $this->parent_data = $parent_data; + } + + /** + * Get the parent data array for this variation. + * + * @since 3.0.0 + * @return array + */ + public function get_parent_data() { + return $this->parent_data; + } + + /** + * Set attributes. Unlike the parent product which uses terms, variations are assigned + * specific attributes using name value pairs. + * + * @param array $raw_attributes array of raw attributes. + */ + public function set_attributes( $raw_attributes ) { + $raw_attributes = (array) $raw_attributes; + $attributes = array(); + + foreach ( $raw_attributes as $key => $value ) { + // Remove attribute prefix which meta gets stored with. + if ( 0 === strpos( $key, 'attribute_' ) ) { + $key = substr( $key, 10 ); + } + $attributes[ $key ] = $value; + } + $this->set_prop( 'attributes', $attributes ); + } + + /** + * Returns whether or not the product has any visible attributes. + * + * Variations are mapped to specific attributes unlike products, and the return + * value of ->get_attributes differs. Therefore this returns false. + * + * @return boolean + */ + public function has_attributes() { + return false; + } + + /* + |-------------------------------------------------------------------------- + | Conditionals + |-------------------------------------------------------------------------- + */ + + /** + * Returns false if the product cannot be bought. + * Override abstract method so that: i) Disabled variations are not be purchasable by admins. ii) Enabled variations are not purchasable if the parent product is not purchasable. + * + * @return bool + */ + public function is_purchasable() { + return apply_filters( 'woocommerce_variation_is_purchasable', $this->variation_is_visible() && parent::is_purchasable() && ( 'publish' === $this->parent_data['status'] || current_user_can( 'edit_post', $this->get_parent_id() ) ), $this ); + } + + /** + * Controls whether this particular variation will appear greyed-out (inactive) or not (active). + * Used by extensions to make incompatible variations appear greyed-out, etc. + * Other possible uses: prevent out-of-stock variations from being selected. + * + * @return bool + */ + public function variation_is_active() { + return apply_filters( 'woocommerce_variation_is_active', true, $this ); + } + + /** + * Checks if this particular variation is visible. Invisible variations are enabled and can be selected, but no price / stock info is displayed. + * Instead, a suitable 'unavailable' message is displayed. + * Invisible by default: Disabled variations and variations with an empty price. + * + * @return bool + */ + public function variation_is_visible() { + return apply_filters( 'woocommerce_variation_is_visible', 'publish' === get_post_status( $this->get_id() ) && '' !== $this->get_price(), $this->get_id(), $this->get_parent_id(), $this ); + } + + /** + * Return valid tax classes. Adds 'parent' to the default list of valid tax classes. + * + * @return array valid tax classes + */ + protected function get_valid_tax_classes() { + $valid_classes = WC_Tax::get_tax_class_slugs(); + $valid_classes[] = 'parent'; + + return $valid_classes; + } +} diff --git a/includes/class-wc-query.php b/includes/class-wc-query.php new file mode 100644 index 0000000..4758db3 --- /dev/null +++ b/includes/class-wc-query.php @@ -0,0 +1,1010 @@ +filterer = wc_get_container()->get( Filterer::class ); + + add_action( 'init', array( $this, 'add_endpoints' ) ); + if ( ! is_admin() ) { + add_action( 'wp_loaded', array( $this, 'get_errors' ), 20 ); + add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); + add_action( 'parse_request', array( $this, 'parse_request' ), 0 ); + add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); + add_filter( 'get_pagenum_link', array( $this, 'remove_add_to_cart_pagination' ), 10, 1 ); + } + $this->init_query_vars(); + } + + /** + * Reset the chosen attributes so that get_layered_nav_chosen_attributes will get them from the query again. + */ + public static function reset_chosen_attributes() { + self::$chosen_attributes = null; + } + + /** + * Get any errors from querystring. + */ + public function get_errors() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : ''; + + if ( $error && ! wc_has_notice( $error, 'error' ) ) { + wc_add_notice( $error, 'error' ); + } + } + + /** + * Init query vars by loading options. + */ + public function init_query_vars() { + // Query vars to add to WP. + $this->query_vars = array( + // Checkout actions. + 'order-pay' => get_option( 'woocommerce_checkout_pay_endpoint', 'order-pay' ), + 'order-received' => get_option( 'woocommerce_checkout_order_received_endpoint', 'order-received' ), + // My account actions. + 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), + 'view-order' => get_option( 'woocommerce_myaccount_view_order_endpoint', 'view-order' ), + 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), + 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), + 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), + 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), + 'lost-password' => get_option( 'woocommerce_myaccount_lost_password_endpoint', 'lost-password' ), + 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), + 'add-payment-method' => get_option( 'woocommerce_myaccount_add_payment_method_endpoint', 'add-payment-method' ), + 'delete-payment-method' => get_option( 'woocommerce_myaccount_delete_payment_method_endpoint', 'delete-payment-method' ), + 'set-default-payment-method' => get_option( 'woocommerce_myaccount_set_default_payment_method_endpoint', 'set-default-payment-method' ), + ); + } + + /** + * Get page title for an endpoint. + * + * @param string $endpoint Endpoint key. + * @param string $action Optional action or variation within the endpoint. + * + * @since 2.3.0 + * @since 4.6.0 Added $action parameter. + * @return string The page title. + */ + public function get_endpoint_title( $endpoint, $action = '' ) { + global $wp; + + switch ( $endpoint ) { + case 'order-pay': + $title = __( 'Pay for order', 'woocommerce' ); + break; + case 'order-received': + $title = __( 'Order received', 'woocommerce' ); + break; + case 'orders': + if ( ! empty( $wp->query_vars['orders'] ) ) { + /* translators: %s: page */ + $title = sprintf( __( 'Orders (page %d)', 'woocommerce' ), intval( $wp->query_vars['orders'] ) ); + } else { + $title = __( 'Orders', 'woocommerce' ); + } + break; + case 'view-order': + $order = wc_get_order( $wp->query_vars['view-order'] ); + /* translators: %s: order number */ + $title = ( $order ) ? sprintf( __( 'Order #%s', 'woocommerce' ), $order->get_order_number() ) : ''; + break; + case 'downloads': + $title = __( 'Downloads', 'woocommerce' ); + break; + case 'edit-account': + $title = __( 'Account details', 'woocommerce' ); + break; + case 'edit-address': + $title = __( 'Addresses', 'woocommerce' ); + break; + case 'payment-methods': + $title = __( 'Payment methods', 'woocommerce' ); + break; + case 'add-payment-method': + $title = __( 'Add payment method', 'woocommerce' ); + break; + case 'lost-password': + if ( in_array( $action, array( 'rp', 'resetpass', 'newaccount' ), true ) ) { + $title = __( 'Set password', 'woocommerce' ); + } else { + $title = __( 'Lost password', 'woocommerce' ); + } + break; + default: + $title = ''; + break; + } + + /** + * Filters the page title used for my-account endpoints. + * + * @since 2.6.0 + * @since 4.6.0 Added $action parameter. + * + * @see get_endpoint_title() + * + * @param string $title Default title. + * @param string $endpoint Endpoint key. + * @param string $action Optional action or variation within the endpoint. + */ + return apply_filters( 'woocommerce_endpoint_' . $endpoint . '_title', $title, $endpoint, $action ); + } + + /** + * Endpoint mask describing the places the endpoint should be added. + * + * @since 2.6.2 + * @return int + */ + public function get_endpoints_mask() { + if ( 'page' === get_option( 'show_on_front' ) ) { + $page_on_front = get_option( 'page_on_front' ); + $myaccount_page_id = get_option( 'woocommerce_myaccount_page_id' ); + $checkout_page_id = get_option( 'woocommerce_checkout_page_id' ); + + if ( in_array( $page_on_front, array( $myaccount_page_id, $checkout_page_id ), true ) ) { + return EP_ROOT | EP_PAGES; + } + } + + return EP_PAGES; + } + + /** + * Add endpoints for query vars. + */ + public function add_endpoints() { + $mask = $this->get_endpoints_mask(); + + foreach ( $this->get_query_vars() as $key => $var ) { + if ( ! empty( $var ) ) { + add_rewrite_endpoint( $var, $mask ); + } + } + } + + /** + * Add query vars. + * + * @param array $vars Query vars. + * @return array + */ + public function add_query_vars( $vars ) { + foreach ( $this->get_query_vars() as $key => $var ) { + $vars[] = $key; + } + return $vars; + } + + /** + * Get query vars. + * + * @return array + */ + public function get_query_vars() { + return apply_filters( 'woocommerce_get_query_vars', $this->query_vars ); + } + + /** + * Get query current active query var. + * + * @return string + */ + public function get_current_endpoint() { + global $wp; + + foreach ( $this->get_query_vars() as $key => $value ) { + if ( isset( $wp->query_vars[ $key ] ) ) { + return $key; + } + } + return ''; + } + + /** + * Parse the request and look for query vars - endpoints may not be supported. + */ + public function parse_request() { + global $wp; + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + // Map query vars to their keys, or get them if endpoints are not supported. + foreach ( $this->get_query_vars() as $key => $var ) { + if ( isset( $_GET[ $var ] ) ) { + $wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) ); + } elseif ( isset( $wp->query_vars[ $var ] ) ) { + $wp->query_vars[ $key ] = $wp->query_vars[ $var ]; + } + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended + } + + /** + * Are we currently on the front page? + * + * @param WP_Query $q Query instance. + * @return bool + */ + private function is_showing_page_on_front( $q ) { + return ( $q->is_home() && ! $q->is_posts_page ) && 'page' === get_option( 'show_on_front' ); + } + + /** + * Is the front page a page we define? + * + * @param int $page_id Page ID. + * @return bool + */ + private function page_on_front_is( $page_id ) { + return absint( get_option( 'page_on_front' ) ) === absint( $page_id ); + } + + /** + * Hook into pre_get_posts to do the main product query. + * + * @param WP_Query $q Query instance. + */ + public function pre_get_posts( $q ) { + // We only want to affect the main query. + if ( ! $q->is_main_query() ) { + return; + } + + // Fixes for queries on static homepages. + if ( $this->is_showing_page_on_front( $q ) ) { + + // Fix for endpoints on the homepage. + if ( ! $this->page_on_front_is( $q->get( 'page_id' ) ) ) { + $_query = wp_parse_args( $q->query ); + if ( ! empty( $_query ) && array_intersect( array_keys( $_query ), array_keys( $this->get_query_vars() ) ) ) { + $q->is_page = true; + $q->is_home = false; + $q->is_singular = true; + $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); + add_filter( 'redirect_canonical', '__return_false' ); + } + } + + // When orderby is set, WordPress shows posts on the front-page. Get around that here. + if ( $this->page_on_front_is( wc_get_page_id( 'shop' ) ) ) { + $_query = wp_parse_args( $q->query ); + if ( empty( $_query ) || ! array_diff( array_keys( $_query ), array( 'preview', 'page', 'paged', 'cpage', 'orderby' ) ) ) { + $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); + $q->is_page = true; + $q->is_home = false; + + // WP supporting themes show post type archive. + if ( current_theme_supports( 'woocommerce' ) ) { + $q->set( 'post_type', 'product' ); + } else { + $q->is_singular = true; + } + } + } elseif ( ! empty( $_GET['orderby'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $q->set( 'page_id', (int) get_option( 'page_on_front' ) ); + $q->is_page = true; + $q->is_home = false; + $q->is_singular = true; + } + } + + // Fix product feeds. + if ( $q->is_feed() && $q->is_post_type_archive( 'product' ) ) { + $q->is_comment_feed = false; + } + + // Special check for shops with the PRODUCT POST TYPE ARCHIVE on front. + if ( current_theme_supports( 'woocommerce' ) && $q->is_page() && 'page' === get_option( 'show_on_front' ) && absint( $q->get( 'page_id' ) ) === wc_get_page_id( 'shop' ) ) { + // This is a front-page shop. + $q->set( 'post_type', 'product' ); + $q->set( 'page_id', '' ); + + if ( isset( $q->query['paged'] ) ) { + $q->set( 'paged', $q->query['paged'] ); + } + + // Define a variable so we know this is the front page shop later on. + wc_maybe_define_constant( 'SHOP_IS_ON_FRONT', true ); + + // Get the actual WP page to avoid errors and let us use is_front_page(). + // This is hacky but works. Awaiting https://core.trac.wordpress.org/ticket/21096. + global $wp_post_types; + + $shop_page = get_post( wc_get_page_id( 'shop' ) ); + + $wp_post_types['product']->ID = $shop_page->ID; + $wp_post_types['product']->post_title = $shop_page->post_title; + $wp_post_types['product']->post_name = $shop_page->post_name; + $wp_post_types['product']->post_type = $shop_page->post_type; + $wp_post_types['product']->ancestors = get_ancestors( $shop_page->ID, $shop_page->post_type ); + + // Fix conditional Functions like is_front_page. + $q->is_singular = false; + $q->is_post_type_archive = true; + $q->is_archive = true; + $q->is_page = true; + + // Remove post type archive name from front page title tag. + add_filter( 'post_type_archive_title', '__return_empty_string', 5 ); + + // Fix WP SEO. + if ( class_exists( 'WPSEO_Meta' ) ) { + add_filter( 'wpseo_metadesc', array( $this, 'wpseo_metadesc' ) ); + add_filter( 'wpseo_metakey', array( $this, 'wpseo_metakey' ) ); + } + } elseif ( ! $q->is_post_type_archive( 'product' ) && ! $q->is_tax( get_object_taxonomies( 'product' ) ) ) { + // Only apply to product categories, the product post archive, the shop page, product tags, and product attribute taxonomies. + return; + } + + $this->product_query( $q ); + } + + /** + * Handler for the 'the_posts' WP filter. + * + * @param array $posts Posts from WP Query. + * @param WP_Query $query Current query. + * + * @return array + */ + public function handle_get_posts( $posts, $query ) { + if ( 'product_query' !== $query->get( 'wc_query' ) ) { + return $posts; + } + $this->remove_product_query_filters( $posts ); + return $posts; + } + + + /** + * Pre_get_posts above may adjust the main query to add WooCommerce logic. When this query is done, we need to ensure + * all custom filters are removed. + * + * This is done here during the_posts filter. The input is not changed. + * + * @param array $posts Posts from WP Query. + * @return array + */ + public function remove_product_query_filters( $posts ) { + $this->remove_ordering_args(); + remove_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 ); + return $posts; + } + + /** + * This function used to be hooked to found_posts and adjust the posts count when the filtering by attribute + * widget was used and variable products were present. Now it isn't hooked anymore and does nothing but return + * the input unchanged, since the pull request in which it was introduced has been reverted. + * + * @since 4.4.0 + * @param int $count Original posts count, as supplied by the found_posts filter. + * @param WP_Query $query The current WP_Query object. + * + * @return int Adjusted posts count. + */ + public function adjust_posts_count( $count, $query ) { + return $count; + } + + /** + * Instance version of get_layered_nav_chosen_attributes, needed for unit tests. + * + * @return array + */ + protected function get_layered_nav_chosen_attributes_inst() { + return self::get_layered_nav_chosen_attributes(); + } + + /** + * Get the posts (or the ids of the posts) found in the current WP loop. + * + * @return array Array of posts or post ids. + */ + protected function get_current_posts() { + return $GLOBALS['wp_query']->posts; + } + + /** + * WP SEO meta description. + * + * Hooked into wpseo_ hook already, so no need for function_exist. + * + * @return string + */ + public function wpseo_metadesc() { + return WPSEO_Meta::get_value( 'metadesc', wc_get_page_id( 'shop' ) ); + } + + /** + * WP SEO meta key. + * + * Hooked into wpseo_ hook already, so no need for function_exist. + * + * @return string + */ + public function wpseo_metakey() { + return WPSEO_Meta::get_value( 'metakey', wc_get_page_id( 'shop' ) ); + } + + /** + * Query the products, applying sorting/ordering etc. + * This applies to the main WordPress loop. + * + * @param WP_Query $q Query instance. + */ + public function product_query( $q ) { + if ( ! is_feed() ) { + $ordering = $this->get_catalog_ordering_args(); + $q->set( 'orderby', $ordering['orderby'] ); + $q->set( 'order', $ordering['order'] ); + + if ( isset( $ordering['meta_key'] ) ) { + $q->set( 'meta_key', $ordering['meta_key'] ); + } + } + + // Query vars that affect posts shown. + $q->set( 'meta_query', $this->get_meta_query( $q->get( 'meta_query' ), true ) ); + $q->set( 'tax_query', $this->get_tax_query( $q->get( 'tax_query' ), true ) ); + $q->set( 'wc_query', 'product_query' ); + $q->set( 'post__in', array_unique( (array) apply_filters( 'loop_shop_post_in', array() ) ) ); + + // Work out how many products to query. + $q->set( 'posts_per_page', $q->get( 'posts_per_page' ) ? $q->get( 'posts_per_page' ) : apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ) ); + + // Store reference to this query. + self::$product_query = $q; + + // Additonal hooks to change WP Query. + add_filter( + 'posts_clauses', + function( $args, $wp_query ) { + return $this->product_query_post_clauses( $args, $wp_query ); + }, + 10, + 2 + ); + add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 ); + + do_action( 'woocommerce_product_query', $q, $this ); + } + + /** + * Add extra clauses to the product query. + * + * @param array $args Product query clauses. + * @param WP_Query $wp_query The current product query. + * @return array The updated product query clauses array. + */ + private function product_query_post_clauses( $args, $wp_query ) { + $args = $this->price_filter_post_clauses( $args, $wp_query ); + $args = $this->filterer->filter_by_attribute_post_clauses( $args, $wp_query, $this->get_layered_nav_chosen_attributes() ); + + return $args; + } + + /** + * Remove the query. + */ + public function remove_product_query() { + remove_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); + } + + /** + * Remove ordering queries. + */ + public function remove_ordering_args() { + remove_filter( 'posts_clauses', array( $this, 'order_by_price_asc_post_clauses' ) ); + remove_filter( 'posts_clauses', array( $this, 'order_by_price_desc_post_clauses' ) ); + remove_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); + remove_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); + } + + /** + * Returns an array of arguments for ordering products based on the selected values. + * + * @param string $orderby Order by param. + * @param string $order Order param. + * @return array + */ + public function get_catalog_ordering_args( $orderby = '', $order = '' ) { + // Get ordering from query string unless defined. + if ( ! $orderby ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) ); + + if ( ! $orderby_value ) { + if ( is_search() ) { + $orderby_value = 'relevance'; + } else { + $orderby_value = apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', 'menu_order' ) ); + } + } + + // Get order + orderby args from string. + $orderby_value = is_array( $orderby_value ) ? $orderby_value : explode( '-', $orderby_value ); + $orderby = esc_attr( $orderby_value[0] ); + $order = ! empty( $orderby_value[1] ) ? $orderby_value[1] : $order; + } + + // Convert to correct format. + $orderby = strtolower( is_array( $orderby ) ? (string) current( $orderby ) : (string) $orderby ); + $order = strtoupper( is_array( $order ) ? (string) current( $order ) : (string) $order ); + $args = array( + 'orderby' => $orderby, + 'order' => ( 'DESC' === $order ) ? 'DESC' : 'ASC', + 'meta_key' => '', // @codingStandardsIgnoreLine + ); + + switch ( $orderby ) { + case 'id': + $args['orderby'] = 'ID'; + break; + case 'menu_order': + $args['orderby'] = 'menu_order title'; + break; + case 'title': + $args['orderby'] = 'title'; + $args['order'] = ( 'DESC' === $order ) ? 'DESC' : 'ASC'; + break; + case 'relevance': + $args['orderby'] = 'relevance'; + $args['order'] = 'DESC'; + break; + case 'rand': + $args['orderby'] = 'rand'; // @codingStandardsIgnoreLine + break; + case 'date': + $args['orderby'] = 'date ID'; + $args['order'] = ( 'ASC' === $order ) ? 'ASC' : 'DESC'; + break; + case 'price': + $callback = 'DESC' === $order ? 'order_by_price_desc_post_clauses' : 'order_by_price_asc_post_clauses'; + add_filter( 'posts_clauses', array( $this, $callback ) ); + break; + case 'popularity': + add_filter( 'posts_clauses', array( $this, 'order_by_popularity_post_clauses' ) ); + break; + case 'rating': + add_filter( 'posts_clauses', array( $this, 'order_by_rating_post_clauses' ) ); + break; + } + + return apply_filters( 'woocommerce_get_catalog_ordering_args', $args, $orderby, $order ); + } + + /** + * Custom query used to filter products by price. + * + * @since 3.6.0 + * + * @param array $args Query args. + * @param WP_Query $wp_query WP_Query object. + * + * @return array + */ + public function price_filter_post_clauses( $args, $wp_query ) { + global $wpdb; + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! $wp_query->is_main_query() || ( ! isset( $_GET['max_price'] ) && ! isset( $_GET['min_price'] ) ) ) { + return $args; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + $current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0; + $current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX; + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + /** + * Adjust if the store taxes are not displayed how they are stored. + * Kicks in when prices excluding tax are displayed including tax. + */ + if ( wc_tax_enabled() && 'incl' === get_option( 'woocommerce_tax_display_shop' ) && ! wc_prices_include_tax() ) { + $tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // Uses standard tax class. + $tax_rates = WC_Tax::get_rates( $tax_class ); + + if ( $tax_rates ) { + $current_min_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_min_price, $tax_rates ) ); + $current_max_price -= WC_Tax::get_tax_total( WC_Tax::calc_inclusive_tax( $current_max_price, $tax_rates ) ); + } + } + + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['where'] .= $wpdb->prepare( + ' AND NOT (%fwc_product_meta_lookup.max_price ) ', + $current_max_price, + $current_min_price + ); + return $args; + } + + /** + * Handle numeric price sorting. + * + * @param array $args Query args. + * @return array + */ + public function order_by_price_asc_post_clauses( $args ) { + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['orderby'] = ' wc_product_meta_lookup.min_price ASC, wc_product_meta_lookup.product_id ASC '; + return $args; + } + + /** + * Handle numeric price sorting. + * + * @param array $args Query args. + * @return array + */ + public function order_by_price_desc_post_clauses( $args ) { + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['orderby'] = ' wc_product_meta_lookup.max_price DESC, wc_product_meta_lookup.product_id DESC '; + return $args; + } + + /** + * WP Core does not let us change the sort direction for individual orderby params - https://core.trac.wordpress.org/ticket/17065. + * + * This lets us sort by meta value desc, and have a second orderby param. + * + * @param array $args Query args. + * @return array + */ + public function order_by_popularity_post_clauses( $args ) { + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['orderby'] = ' wc_product_meta_lookup.total_sales DESC, wc_product_meta_lookup.product_id DESC '; + return $args; + } + + /** + * Order by rating post clauses. + * + * @param array $args Query args. + * @return array + */ + public function order_by_rating_post_clauses( $args ) { + $args['join'] = $this->append_product_sorting_table_join( $args['join'] ); + $args['orderby'] = ' wc_product_meta_lookup.average_rating DESC, wc_product_meta_lookup.rating_count DESC, wc_product_meta_lookup.product_id DESC '; + return $args; + } + + /** + * Join wc_product_meta_lookup to posts if not already joined. + * + * @param string $sql SQL join. + * @return string + */ + private function append_product_sorting_table_join( $sql ) { + global $wpdb; + + if ( ! strstr( $sql, 'wc_product_meta_lookup' ) ) { + $sql .= " LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON $wpdb->posts.ID = wc_product_meta_lookup.product_id "; + } + return $sql; + } + + /** + * Appends meta queries to an array. + * + * @param array $meta_query Meta query. + * @param bool $main_query If is main query. + * @return array + */ + public function get_meta_query( $meta_query = array(), $main_query = false ) { + if ( ! is_array( $meta_query ) ) { + $meta_query = array(); + } + return array_filter( apply_filters( 'woocommerce_product_query_meta_query', $meta_query, $this ) ); + } + + /** + * Appends tax queries to an array. + * + * @param array $tax_query Tax query. + * @param bool $main_query If is main query. + * @return array + */ + public function get_tax_query( $tax_query = array(), $main_query = false ) { + if ( ! is_array( $tax_query ) ) { + $tax_query = array( + 'relation' => 'AND', + ); + } + + if ( $main_query && ! $this->filterer->filtering_via_lookup_table_is_active() ) { + // Layered nav filters on terms. + foreach ( $this->get_layered_nav_chosen_attributes() as $taxonomy => $data ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'slug', + 'terms' => $data['terms'], + 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', + 'include_children' => false, + ); + } + } + + $product_visibility_terms = wc_get_product_visibility_term_ids(); + $product_visibility_not_in = array( is_search() && $main_query ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] ); + + // Hide out of stock products. + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $product_visibility_not_in[] = $product_visibility_terms['outofstock']; + } + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + // Filter by rating. + if ( isset( $_GET['rating_filter'] ) ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $rating_filter = array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ); + $rating_terms = array(); + for ( $i = 1; $i <= 5; $i ++ ) { + if ( in_array( $i, $rating_filter, true ) && isset( $product_visibility_terms[ 'rated-' . $i ] ) ) { + $rating_terms[] = $product_visibility_terms[ 'rated-' . $i ]; + } + } + if ( ! empty( $rating_terms ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $rating_terms, + 'operator' => 'IN', + 'rating_filter' => true, + ); + } + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + if ( ! empty( $product_visibility_not_in ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_not_in, + 'operator' => 'NOT IN', + ); + } + + return array_filter( apply_filters( 'woocommerce_product_query_tax_query', $tax_query, $this ) ); + } + + /** + * Get the main query which product queries ran against. + * + * @return WP_Query + */ + public static function get_main_query() { + return self::$product_query; + } + + /** + * Get the tax query which was used by the main query. + * + * @return array + */ + public static function get_main_tax_query() { + $tax_query = isset( self::$product_query->tax_query, self::$product_query->tax_query->queries ) ? self::$product_query->tax_query->queries : array(); + + return $tax_query; + } + + /** + * Get the meta query which was used by the main query. + * + * @return array + */ + public static function get_main_meta_query() { + $args = self::$product_query->query_vars; + $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); + + return $meta_query; + } + + /** + * Based on WP_Query::parse_search + */ + public static function get_main_search_query_sql() { + global $wpdb; + + $args = self::$product_query->query_vars; + $search_terms = isset( $args['search_terms'] ) ? $args['search_terms'] : array(); + $sql = array(); + + foreach ( $search_terms as $term ) { + // Terms prefixed with '-' should be excluded. + $include = '-' !== substr( $term, 0, 1 ); + + if ( $include ) { + $like_op = 'LIKE'; + $andor_op = 'OR'; + } else { + $like_op = 'NOT LIKE'; + $andor_op = 'AND'; + $term = substr( $term, 1 ); + } + + $like = '%' . $wpdb->esc_like( $term ) . '%'; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like ); + } + + if ( ! empty( $sql ) && ! is_user_logged_in() ) { + $sql[] = "($wpdb->posts.post_password = '')"; + } + + return implode( ' AND ', $sql ); + } + + /** + * Get an array of attributes and terms selected with the layered nav widget. + * + * @return array + */ + public static function get_layered_nav_chosen_attributes() { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( ! is_array( self::$chosen_attributes ) ) { + self::$chosen_attributes = array(); + + if ( ! empty( $_GET ) ) { + foreach ( $_GET as $key => $value ) { + if ( 0 === strpos( $key, 'filter_' ) ) { + $attribute = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) ); + $taxonomy = wc_attribute_taxonomy_name( $attribute ); + $filter_terms = ! empty( $value ) ? explode( ',', wc_clean( wp_unslash( $value ) ) ) : array(); + + if ( empty( $filter_terms ) || ! taxonomy_exists( $taxonomy ) || ! wc_attribute_taxonomy_id_by_name( $attribute ) ) { + continue; + } + + $query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : ''; + self::$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding. + self::$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' ); + } + } + } + } + return self::$chosen_attributes; + // phpcs:disable WordPress.Security.NonceVerification.Recommended + } + + /** + * Remove the add-to-cart param from pagination urls. + * + * @param string $url URL. + * @return string + */ + public function remove_add_to_cart_pagination( $url ) { + return remove_query_arg( 'add-to-cart', $url ); + } + + /** + * Return a meta query for filtering by rating. + * + * @deprecated 3.0.0 Replaced with taxonomy. + * @return array + */ + public function rating_filter_meta_query() { + return array(); + } + + /** + * Returns a meta query to handle product visibility. + * + * @deprecated 3.0.0 Replaced with taxonomy. + * @param string $compare (default: 'IN'). + * @return array + */ + public function visibility_meta_query( $compare = 'IN' ) { + return array(); + } + + /** + * Returns a meta query to handle product stock status. + * + * @deprecated 3.0.0 Replaced with taxonomy. + * @param string $status (default: 'instock'). + * @return array + */ + public function stock_status_meta_query( $status = 'instock' ) { + return array(); + } + + /** + * Layered nav init. + * + * @deprecated 2.6.0 + */ + public function layered_nav_init() { + wc_deprecated_function( 'layered_nav_init', '2.6' ); + } + + /** + * Get an unpaginated list all product IDs (both filtered and unfiltered). Makes use of transients. + * + * @deprecated 2.6.0 due to performance concerns + */ + public function get_products_in_view() { + wc_deprecated_function( 'get_products_in_view', '2.6' ); + } + + /** + * Layered Nav post filter. + * + * @deprecated 2.6.0 due to performance concerns + * + * @param mixed $deprecated Deprecated. + */ + public function layered_nav_query( $deprecated ) { + wc_deprecated_function( 'layered_nav_query', '2.6' ); + } + + /** + * Search post excerpt. + * + * @param string $where Where clause. + * + * @deprecated 3.2.0 - Not needed anymore since WordPress 4.5. + */ + public function search_post_excerpt( $where = '' ) { + wc_deprecated_function( 'WC_Query::search_post_excerpt', '3.2.0', 'Excerpt added to search query by default since WordPress 4.5.' ); + return $where; + } + + /** + * Remove the posts_where filter. + * + * @deprecated 3.2.0 - Nothing to remove anymore because search_post_excerpt() is deprecated. + */ + public function remove_posts_where() { + wc_deprecated_function( 'WC_Query::remove_posts_where', '3.2.0', 'Nothing to remove anymore because search_post_excerpt() is deprecated.' ); + } +} diff --git a/includes/class-wc-rate-limiter.php b/includes/class-wc-rate-limiter.php new file mode 100644 index 0000000..aba4fb0 --- /dev/null +++ b/includes/class-wc-rate-limiter.php @@ -0,0 +1,79 @@ +prefix = 'wp_' . get_current_blog_id(); + $this->action = 'wc_regenerate_images'; + + // This is needed to prevent timeouts due to threading. See https://core.trac.wordpress.org/ticket/36534. + @putenv( 'MAGICK_THREAD_LIMIT=1' ); // @codingStandardsIgnoreLine. + + parent::__construct(); + } + + /** + * Is job running? + * + * @return boolean + */ + public function is_running() { + return $this->is_queue_empty(); + } + + /** + * Limit each task ran per batch to 1 for image regen. + * + * @return bool + */ + protected function batch_limit_exceeded() { + return true; + } + + /** + * Determines whether an attachment can have its thumbnails regenerated. + * + * Adapted from Regenerate Thumbnails by Alex Mills. + * + * @param WP_Post $attachment An attachment's post object. + * @return bool Whether the given attachment can have its thumbnails regenerated. + */ + protected function is_regeneratable( $attachment ) { + if ( 'site-icon' === get_post_meta( $attachment->ID, '_wp_attachment_context', true ) ) { + return false; + } + + if ( wp_attachment_is_image( $attachment ) ) { + return true; + } + + return false; + } + + /** + * Code to execute for each item in the queue + * + * @param mixed $item Queue item to iterate over. + * @return bool + */ + protected function task( $item ) { + if ( ! is_array( $item ) && ! isset( $item['attachment_id'] ) ) { + return false; + } + + $this->attachment_id = absint( $item['attachment_id'] ); + $attachment = get_post( $this->attachment_id ); + + if ( ! $attachment || 'attachment' !== $attachment->post_type || ! $this->is_regeneratable( $attachment ) ) { + return false; + } + + if ( ! function_exists( 'wp_crop_image' ) ) { + include ABSPATH . 'wp-admin/includes/image.php'; + } + + $log = wc_get_logger(); + + $log->info( + sprintf( + // translators: %s: ID of the attachment. + __( 'Regenerating images for attachment ID: %s', 'woocommerce' ), + $this->attachment_id + ), + array( + 'source' => 'wc-image-regeneration', + ) + ); + + $fullsizepath = get_attached_file( $this->attachment_id ); + + // Check if the file exists, if not just remove item from queue. + if ( false === $fullsizepath || is_wp_error( $fullsizepath ) || ! file_exists( $fullsizepath ) ) { + return false; + } + + $old_metadata = wp_get_attachment_metadata( $this->attachment_id ); + + // We only want to regen WC images. + add_filter( 'intermediate_image_sizes', array( $this, 'adjust_intermediate_image_sizes' ) ); + + // We only want to resize images if they do not already exist. + add_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 3 ); + + // This function will generate the new image sizes. + $new_metadata = wp_generate_attachment_metadata( $this->attachment_id, $fullsizepath ); + + // Remove custom filters. + remove_filter( 'intermediate_image_sizes', array( $this, 'adjust_intermediate_image_sizes' ) ); + remove_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 3 ); + + // If something went wrong lets just remove the item from the queue. + if ( is_wp_error( $new_metadata ) || empty( $new_metadata ) ) { + return false; + } + + if ( ! empty( $old_metadata ) && ! empty( $old_metadata['sizes'] ) && is_array( $old_metadata['sizes'] ) ) { + foreach ( $old_metadata['sizes'] as $old_size => $old_size_data ) { + if ( empty( $new_metadata['sizes'][ $old_size ] ) ) { + $new_metadata['sizes'][ $old_size ] = $old_metadata['sizes'][ $old_size ]; + } + } + // Handle legacy sizes. + if ( isset( $new_metadata['sizes']['shop_thumbnail'], $new_metadata['sizes']['woocommerce_gallery_thumbnail'] ) ) { + $new_metadata['sizes']['shop_thumbnail'] = $new_metadata['sizes']['woocommerce_gallery_thumbnail']; + } + if ( isset( $new_metadata['sizes']['shop_catalog'], $new_metadata['sizes']['woocommerce_thumbnail'] ) ) { + $new_metadata['sizes']['shop_catalog'] = $new_metadata['sizes']['woocommerce_thumbnail']; + } + if ( isset( $new_metadata['sizes']['shop_single'], $new_metadata['sizes']['woocommerce_single'] ) ) { + $new_metadata['sizes']['shop_single'] = $new_metadata['sizes']['woocommerce_single']; + } + } + + // Update the meta data with the new size values. + wp_update_attachment_metadata( $this->attachment_id, $new_metadata ); + + // We made it till the end, now lets remove the item from the queue. + return false; + } + + /** + * Filters the list of thumbnail sizes to only include those which have missing files. + * + * @param array $sizes An associative array of registered thumbnail image sizes. + * @param array $metadata An associative array of fullsize image metadata: width, height, file. + * @param int $attachment_id Attachment ID. Only passed from WP 5.0+. + * @return array An associative array of image sizes. + */ + public function filter_image_sizes_to_only_missing_thumbnails( $sizes, $metadata, $attachment_id = null ) { + $attachment_id = is_null( $attachment_id ) ? $this->attachment_id : $attachment_id; + + if ( ! $sizes || ! $attachment_id ) { + return $sizes; + } + + $fullsizepath = get_attached_file( $attachment_id ); + $editor = wp_get_image_editor( $fullsizepath ); + + if ( is_wp_error( $editor ) ) { + return $sizes; + } + + $metadata = wp_get_attachment_metadata( $attachment_id ); + + // This is based on WP_Image_Editor_GD::multi_resize() and others. + foreach ( $sizes as $size => $size_data ) { + if ( empty( $metadata['sizes'][ $size ] ) ) { + continue; + } + if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { + continue; + } + if ( ! isset( $size_data['width'] ) ) { + $size_data['width'] = null; + } + if ( ! isset( $size_data['height'] ) ) { + $size_data['height'] = null; + } + if ( ! isset( $size_data['crop'] ) ) { + $size_data['crop'] = false; + } + + $image_sizes = getimagesize( $fullsizepath ); + if ( false === $image_sizes ) { + continue; + } + list( $orig_w, $orig_h ) = $image_sizes; + + $dimensions = image_resize_dimensions( $orig_w, $orig_h, $size_data['width'], $size_data['height'], $size_data['crop'] ); + + if ( ! $dimensions || ! is_array( $dimensions ) ) { + continue; + } + + $info = pathinfo( $fullsizepath ); + $ext = $info['extension']; + $dst_w = $dimensions[4]; + $dst_h = $dimensions[5]; + $suffix = "{$dst_w}x{$dst_h}"; + $dst_rel_path = str_replace( '.' . $ext, '', $fullsizepath ); + $thumbnail = "{$dst_rel_path}-{$suffix}.{$ext}"; + + if ( $dst_w === $metadata['sizes'][ $size ]['width'] && $dst_h === $metadata['sizes'][ $size ]['height'] && file_exists( $thumbnail ) ) { + unset( $sizes[ $size ] ); + } + } + + return $sizes; + } + + /** + * Returns the sizes we want to regenerate. + * + * @param array $sizes Sizes to generate. + * @return array + */ + public function adjust_intermediate_image_sizes( $sizes ) { + // Prevent a filter loop. + $unfiltered_sizes = array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single' ); + static $in_filter = false; + if ( $in_filter ) { + return $unfiltered_sizes; + } + $in_filter = true; + $filtered_sizes = apply_filters( 'woocommerce_regenerate_images_intermediate_image_sizes', $unfiltered_sizes ); + $in_filter = false; + return $filtered_sizes; + } + + /** + * This runs once the job has completed all items on the queue. + * + * @return void + */ + protected function complete() { + parent::complete(); + $log = wc_get_logger(); + $log->info( + __( 'Completed product image regeneration job.', 'woocommerce' ), + array( + 'source' => 'wc-image-regeneration', + ) + ); + } +} diff --git a/includes/class-wc-regenerate-images.php b/includes/class-wc-regenerate-images.php new file mode 100644 index 0000000..6e6ed20 --- /dev/null +++ b/includes/class-wc-regenerate-images.php @@ -0,0 +1,471 @@ +is_running() ) { + WC_Admin_Notices::add_notice( 'regenerating_thumbnails' ); + } else { + WC_Admin_Notices::remove_notice( 'regenerating_thumbnails' ); + } + } + + /** + * Dismiss notice and cancel jobs. + */ + public static function dismiss_regenerating_notice() { + if ( self::$background_process ) { + self::$background_process->kill_process(); + + $log = wc_get_logger(); + $log->info( + __( 'Cancelled product image regeneration job.', 'woocommerce' ), + array( + 'source' => 'wc-image-regeneration', + ) + ); + } + WC_Admin_Notices::remove_notice( 'regenerating_thumbnails' ); + } + + /** + * Regenerate images if the settings have changed since last re-generation. + * + * @return void + */ + public static function maybe_regenerate_images() { + $size_hash = md5( + wp_json_encode( + array( + wc_get_image_size( 'thumbnail' ), + wc_get_image_size( 'single' ), + wc_get_image_size( 'gallery_thumbnail' ), + ) + ) + ); + + if ( update_option( 'woocommerce_maybe_regenerate_images_hash', $size_hash ) ) { + // Size settings have changed. Trigger regen. + self::queue_image_regeneration(); + } + } + + /** + * Check if we should maybe generate a new image size if not already there. + * + * @param array $image Properties of the image. + * @param int $attachment_id Attachment ID. + * @param string|array $size Image size. + * @param bool $icon If icon or not. + * @return array + */ + public static function maybe_resize_image( $image, $attachment_id, $size, $icon ) { + if ( ! apply_filters( 'woocommerce_resize_images', true ) ) { + return $image; + } + + // List of sizes we want to resize. Ignore others. + if ( ! $image || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single', 'shop_thumbnail', 'shop_catalog', 'shop_single' ) ), true ) ) { + return $image; + } + + $target_size = wc_get_image_size( $size ); + $image_width = $image[1]; + $image_height = $image[2]; + $ratio_match = false; + $target_uncropped = '' === $target_size['width'] || '' === $target_size['height'] || ! $target_size['crop']; + + // If '' is passed to either size, we test ratios against the original file. It's uncropped. + if ( $target_uncropped ) { + $full_size = self::get_full_size_image_dimensions( $attachment_id ); + + if ( ! $full_size || ! $full_size['width'] || ! $full_size['height'] ) { + return $image; + } + + $ratio_match = wp_image_matches_ratio( $image_width, $image_height, $full_size['width'], $full_size['height'] ); + } else { + $ratio_match = wp_image_matches_ratio( $image_width, $image_height, $target_size['width'], $target_size['height'] ); + } + + if ( ! $ratio_match ) { + $full_size = self::get_full_size_image_dimensions( $attachment_id ); + + if ( ! $full_size ) { + return $image; + } + + // Check if the actual image has a larger dimension than the requested image size. Smaller images are not zoom-cropped. + if ( $image_width === $target_size['width'] && $full_size['height'] < $target_size['height'] ) { + return $image; + } + + if ( $image_height === $target_size['height'] && $full_size['width'] < $target_size['width'] ) { + return $image; + } + + // If the full size image is smaller both ways, don't scale it up. + if ( $full_size['height'] < $target_size['height'] && $full_size['width'] < $target_size['width'] ) { + return $image; + } + + return self::resize_and_return_image( $attachment_id, $image, $size, $icon ); + } + + return $image; + } + + /** + * Get full size image dimensions. + * + * @param int $attachment_id Attachment ID of image. + * @return array Width and height. Empty array if the dimensions cannot be found. + */ + private static function get_full_size_image_dimensions( $attachment_id ) { + $imagedata = wp_get_attachment_metadata( $attachment_id ); + + if ( ! $imagedata ) { + return array(); + } + + if ( ! isset( $imagedata['file'] ) && isset( $imagedata['sizes']['full'] ) ) { + $imagedata['height'] = $imagedata['sizes']['full']['height']; + $imagedata['width'] = $imagedata['sizes']['full']['width']; + } + + return array( + 'width' => $imagedata['width'], + 'height' => $imagedata['height'], + ); + } + + /** + * Ensure we are dealing with the correct image attachment + * + * @param int|WP_Post $attachment Attachment object or ID. + * @return boolean + */ + public static function is_regeneratable( $attachment ) { + if ( 'site-icon' === get_post_meta( is_object( $attachment ) ? $attachment->ID : $attachment, '_wp_attachment_context', true ) ) { + return false; + } + + if ( wp_attachment_is_image( $attachment ) ) { + return true; + } + + return false; + } + + /** + * Only regenerate images for the requested size. + * + * @param array $sizes Array of image sizes. + * @return array + */ + public static function adjust_intermediate_image_sizes( $sizes ) { + return array( self::$regenerate_size ); + } + + /** + * Generate the thumbnail filename and dimensions for a given file. + * + * @param string $fullsizepath Path to full size image. + * @param int $thumbnail_width The width of the thumbnail. + * @param int $thumbnail_height The height of the thumbnail. + * @param bool $crop Whether to crop or not. + * @return array|false An array of the filename, thumbnail width, and thumbnail height, or false on failure to resize such as the thumbnail being larger than the fullsize image. + */ + private static function get_image( $fullsizepath, $thumbnail_width, $thumbnail_height, $crop ) { + list( $fullsize_width, $fullsize_height ) = getimagesize( $fullsizepath ); + + $dimensions = image_resize_dimensions( $fullsize_width, $fullsize_height, $thumbnail_width, $thumbnail_height, $crop ); + $editor = wp_get_image_editor( $fullsizepath ); + + if ( is_wp_error( $editor ) ) { + return false; + } + + if ( ! $dimensions || ! is_array( $dimensions ) ) { + return false; + } + + list( , , , , $dst_w, $dst_h ) = $dimensions; + $suffix = "{$dst_w}x{$dst_h}"; + $file_ext = strtolower( pathinfo( $fullsizepath, PATHINFO_EXTENSION ) ); + + return array( + 'filename' => $editor->generate_filename( $suffix, null, $file_ext ), + 'width' => $dst_w, + 'height' => $dst_h, + ); + } + + /** + * Regenerate the image according to the required size + * + * @param int $attachment_id Attachment ID. + * @param array $image Original Image. + * @param string $size Size to return for new URL. + * @param bool $icon If icon or not. + * @return string + */ + private static function resize_and_return_image( $attachment_id, $image, $size, $icon ) { + if ( ! self::is_regeneratable( $attachment_id ) ) { + return $image; + } + + $fullsizepath = get_attached_file( $attachment_id ); + + if ( false === $fullsizepath || is_wp_error( $fullsizepath ) || ! file_exists( $fullsizepath ) ) { + return $image; + } + + if ( ! function_exists( 'wp_crop_image' ) ) { + include ABSPATH . 'wp-admin/includes/image.php'; + } + + self::$regenerate_size = is_customize_preview() ? $size . '_preview' : $size; + + if ( is_customize_preview() ) { + $image_size = wc_get_image_size( $size ); + + // Make sure registered image size matches the size we're requesting. + add_image_size( self::$regenerate_size, absint( $image_size['width'] ), absint( $image_size['height'] ), $image_size['crop'] ); + + $thumbnail = self::get_image( $fullsizepath, absint( $image_size['width'] ), absint( $image_size['height'] ), $image_size['crop'] ); + + // If the file is already there perhaps just load it if we're using the customizer. No need to store in meta data. + if ( $thumbnail && file_exists( $thumbnail['filename'] ) ) { + $wp_uploads = wp_upload_dir( null, false ); + $wp_uploads_dir = $wp_uploads['basedir']; + $wp_uploads_url = $wp_uploads['baseurl']; + + return array( + 0 => str_replace( $wp_uploads_dir, $wp_uploads_url, $thumbnail['filename'] ), + 1 => $thumbnail['width'], + 2 => $thumbnail['height'], + ); + } + } + + $metadata = wp_get_attachment_metadata( $attachment_id ); + + // Fix for images with no metadata. + if ( ! is_array( $metadata ) ) { + $metadata = array(); + } + + // We only want to regen a specific image size. + add_filter( 'intermediate_image_sizes', array( __CLASS__, 'adjust_intermediate_image_sizes' ) ); + + // This function will generate the new image sizes. + $new_metadata = wp_generate_attachment_metadata( $attachment_id, $fullsizepath ); + + // Remove custom filter. + remove_filter( 'intermediate_image_sizes', array( __CLASS__, 'adjust_intermediate_image_sizes' ) ); + + // If something went wrong lets just return the original image. + if ( is_wp_error( $new_metadata ) || empty( $new_metadata ) ) { + return $image; + } + + if ( isset( $new_metadata['sizes'][ self::$regenerate_size ] ) ) { + $metadata['sizes'][ self::$regenerate_size ] = $new_metadata['sizes'][ self::$regenerate_size ]; + wp_update_attachment_metadata( $attachment_id, $metadata ); + } + + // Now we've done our regen, attempt to return the new size. + $new_image = self::unfiltered_image_downsize( $attachment_id, self::$regenerate_size ); + + return $new_image ? $new_image : $image; + } + + /** + * Image downsize, without this classes filtering on the results. + * + * @param int $attachment_id Attachment ID. + * @param string $size Size to downsize to. + * @return string New image URL. + */ + private static function unfiltered_image_downsize( $attachment_id, $size ) { + remove_action( 'image_get_intermediate_size', array( __CLASS__, 'filter_image_get_intermediate_size' ), 10, 3 ); + + $return = image_downsize( $attachment_id, $size ); + + add_action( 'image_get_intermediate_size', array( __CLASS__, 'filter_image_get_intermediate_size' ), 10, 3 ); + + return $return; + } + + /** + * Get list of images and queue them for regeneration + * + * @return void + */ + public static function queue_image_regeneration() { + global $wpdb; + // First lets cancel existing running queue to avoid running it more than once. + self::$background_process->kill_process(); + + // Now lets find all product image attachments IDs and pop them onto the queue. + $images = $wpdb->get_results( // @codingStandardsIgnoreLine + "SELECT ID + FROM $wpdb->posts + WHERE post_type = 'attachment' + AND post_mime_type LIKE 'image/%' + ORDER BY ID DESC" + ); + foreach ( $images as $image ) { + self::$background_process->push_to_queue( + array( + 'attachment_id' => $image->ID, + ) + ); + } + + // Lets dispatch the queue to start processing. + self::$background_process->save()->dispatch(); + } +} + +add_action( 'init', array( 'WC_Regenerate_Images', 'init' ) ); 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 0000000..442cfb8 --- /dev/null +++ b/includes/class-wc-register-wp-admin-settings.php @@ -0,0 +1,185 @@ +object = $object; + + if ( 'page' === $type ) { + add_filter( 'woocommerce_settings_groups', array( $this, 'register_page_group' ) ); + add_filter( 'woocommerce_settings-' . $this->object->get_id(), array( $this, 'register_page_settings' ) ); + } elseif ( 'email' === $type ) { + add_filter( 'woocommerce_settings_groups', array( $this, 'register_email_group' ) ); + add_filter( 'woocommerce_settings-email_' . $this->object->id, array( $this, 'register_email_settings' ) ); + } + } + + /** + * Register's all of our different notification emails as sub groups + * of email settings. + * + * @since 3.0.0 + * @param array $groups Existing registered groups. + * @return array + */ + public function register_email_group( $groups ) { + $groups[] = array( + 'id' => 'email_' . $this->object->id, + 'label' => $this->object->title, + 'description' => $this->object->description, + 'parent_id' => 'email', + ); + return $groups; + } + + /** + * Registers all of the setting form fields for emails to each email type's group. + * + * @since 3.0.0 + * @param array $settings Existing registered settings. + * @return array + */ + public function register_email_settings( $settings ) { + foreach ( $this->object->form_fields as $id => $setting ) { + $setting['id'] = $id; + $setting['option_key'] = array( $this->object->get_option_key(), $id ); + $new_setting = $this->register_setting( $setting ); + if ( $new_setting ) { + $settings[] = $new_setting; + } + } + return $settings; + } + + /** + * Registers a setting group, based on admin page ID & label as parent group. + * + * @since 3.0.0 + * @param array $groups Array of previously registered groups. + * @return array + */ + public function register_page_group( $groups ) { + $groups[] = array( + 'id' => $this->object->get_id(), + 'label' => $this->object->get_label(), + ); + return $groups; + } + + /** + * Registers settings to a specific group. + * + * @since 3.0.0 + * @param array $settings Existing registered settings. + * @return array + */ + public function register_page_settings( $settings ) { + /** + * WP admin settings can be broken down into separate sections from + * a UI standpoint. This will grab all the sections associated with + * a particular setting group (like 'products') and register them + * to the REST API. + */ + $sections = $this->object->get_sections(); + if ( empty( $sections ) ) { + // Default section is just an empty string, per admin page classes. + $sections = array( '' => '' ); + } + + /** + * We are using 'WC_Settings_Page::get_settings' on purpose even thought it's deprecated. + * See the method documentation for an explanation. + */ + + foreach ( $sections as $section => $section_label ) { + $settings_from_section = $this->object->get_settings( $section ); + foreach ( $settings_from_section as $setting ) { + if ( ! isset( $setting['id'] ) ) { + continue; + } + $setting['option_key'] = $setting['id']; + $new_setting = $this->register_setting( $setting ); + if ( $new_setting ) { + $settings[] = $new_setting; + } + } + } + return $settings; + } + + /** + * Register a setting into the format expected for the Settings REST API. + * + * @since 3.0.0 + * @param array $setting Setting data. + * @return array|bool + */ + public function register_setting( $setting ) { + if ( ! isset( $setting['id'] ) ) { + return false; + } + + $description = ''; + if ( ! empty( $setting['desc'] ) ) { + $description = $setting['desc']; + } elseif ( ! empty( $setting['description'] ) ) { + $description = $setting['description']; + } + + $new_setting = array( + 'id' => $setting['id'], + 'label' => ( ! empty( $setting['title'] ) ? $setting['title'] : '' ), + 'description' => $description, + 'type' => $setting['type'], + 'option_key' => $setting['option_key'], + ); + + if ( isset( $setting['default'] ) ) { + $new_setting['default'] = $setting['default']; + } + if ( isset( $setting['options'] ) ) { + $new_setting['options'] = $setting['options']; + } + if ( isset( $setting['desc_tip'] ) ) { + if ( true === $setting['desc_tip'] ) { + $new_setting['tip'] = $description; + } elseif ( ! empty( $setting['desc_tip'] ) ) { + $new_setting['tip'] = $setting['desc_tip']; + } + } + + return $new_setting; + } + +} diff --git a/includes/class-wc-rest-authentication.php b/includes/class-wc-rest-authentication.php new file mode 100644 index 0000000..d31cad0 --- /dev/null +++ b/includes/class-wc-rest-authentication.php @@ -0,0 +1,640 @@ +is_request_to_rest_api() ) { + return $user_id; + } + + if ( is_ssl() ) { + $user_id = $this->perform_basic_authentication(); + } + + if ( $user_id ) { + return $user_id; + } + + return $this->perform_oauth_authentication(); + } + + /** + * Authenticate the user if authentication wasn't performed during the + * determine_current_user action. + * + * Necessary in cases where wp_get_current_user() is called before WooCommerce is loaded. + * + * @see https://github.com/woocommerce/woocommerce/issues/26847 + * + * @param WP_Error|null|bool $error Error data. + * @return WP_Error|null|bool + */ + public function authentication_fallback( $error ) { + if ( ! empty( $error ) ) { + // Another plugin has already declared a failure. + return $error; + } + if ( empty( $this->error ) && empty( $this->auth_method ) && empty( $this->user ) && 0 === get_current_user_id() ) { + // Authentication hasn't occurred during `determine_current_user`, so check auth. + $user_id = $this->authenticate( false ); + if ( $user_id ) { + wp_set_current_user( $user_id ); + return true; + } + } + return $error; + } + + /** + * Check for authentication error. + * + * @param WP_Error|null|bool $error Error data. + * @return WP_Error|null|bool + */ + public function check_authentication_error( $error ) { + // Pass through other errors. + if ( ! empty( $error ) ) { + return $error; + } + + return $this->get_error(); + } + + /** + * Set authentication error. + * + * @param WP_Error $error Authentication 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'] ) ) { // WPCS: CSRF ok. + $consumer_key = $_GET['consumer_key']; // WPCS: CSRF ok, sanitization ok. + $consumer_secret = $_GET['consumer_secret']; // WPCS: CSRF ok, sanitization ok. + } + + // 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']; // WPCS: CSRF ok, sanitization ok. + $consumer_secret = $_SERVER['PHP_AUTH_PW']; // WPCS: CSRF ok, sanitization ok. + } + + // 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 ) ) { // @codingStandardsIgnoreLine + $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'] ); // WPCS: sanitization ok. + } + + 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 ); // WPCS: CSRF ok. + $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( + /* translators: %s: amount of errors */ + _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 User data. + * @param array $params The request parameters. + * @return true|WP_Error + */ + private function check_oauth_signature( $user, $params ) { + $http_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : ''; // WPCS: sanitization ok. + $request_path = isset( $_SERVER['REQUEST_URI'] ) ? wp_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ) : ''; // WPCS: sanitization ok. + $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, is_ssl() ? 'https' : 'http' ) ); + + // 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 ) ) { // @codingStandardsIgnoreLine + 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 User data. + * @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, true ) ) { + 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 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; + case 'OPTIONS': + return true; + + 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/class-wc-rest-exception.php b/includes/class-wc-rest-exception.php new file mode 100644 index 0000000..0545b53 --- /dev/null +++ b/includes/class-wc-rest-exception.php @@ -0,0 +1,16 @@ +_cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ); + $this->_table = $GLOBALS['wpdb']->prefix . 'woocommerce_sessions'; + } + + /** + * Init hooks and session data. + * + * @since 3.3.0 + */ + public function init() { + $this->init_session_cookie(); + + add_action( 'woocommerce_set_cart_cookies', array( $this, 'set_customer_session_cookie' ), 10 ); + add_action( 'shutdown', array( $this, 'save_data' ), 20 ); + add_action( 'wp_logout', array( $this, 'destroy_session' ) ); + + if ( ! is_user_logged_in() ) { + add_filter( 'nonce_user_logged_out', array( $this, 'maybe_update_nonce_user_logged_out' ), 10, 2 ); + } + } + + /** + * Setup cookie and customer ID. + * + * @since 3.6.0 + */ + public function init_session_cookie() { + $cookie = $this->get_session_cookie(); + + if ( $cookie ) { + $this->_customer_id = $cookie[0]; + $this->_session_expiration = $cookie[1]; + $this->_session_expiring = $cookie[2]; + $this->_has_cookie = true; + $this->_data = $this->get_session_data(); + + // If the user logs in, update session. + if ( is_user_logged_in() && strval( get_current_user_id() ) !== $this->_customer_id ) { + $guest_session_id = $this->_customer_id; + $this->_customer_id = strval( get_current_user_id() ); + $this->_dirty = true; + $this->save_data( $guest_session_id ); + $this->set_customer_session_cookie( true ); + } + + // Update session if its close to expiring. + if ( time() > $this->_session_expiring ) { + $this->set_session_expiration(); + $this->update_session_timestamp( $this->_customer_id, $this->_session_expiration ); + } + } else { + $this->set_session_expiration(); + $this->_customer_id = $this->generate_customer_id(); + $this->_data = $this->get_session_data(); + } + } + + /** + * Sets the session cookie on-demand (usually after adding an item to the cart). + * + * Since the cookie name (as of 2.1) is prepended with wp, cache systems like batcache will not cache pages when set. + * + * Warning: Cookies will only be set if this is called before the headers are sent. + * + * @param bool $set Should the session cookie be set. + */ + public function set_customer_session_cookie( $set ) { + if ( $set ) { + $to_hash = $this->_customer_id . '|' . $this->_session_expiration; + $cookie_hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); + $cookie_value = $this->_customer_id . '||' . $this->_session_expiration . '||' . $this->_session_expiring . '||' . $cookie_hash; + $this->_has_cookie = true; + + if ( ! isset( $_COOKIE[ $this->_cookie ] ) || $_COOKIE[ $this->_cookie ] !== $cookie_value ) { + wc_setcookie( $this->_cookie, $cookie_value, $this->_session_expiration, $this->use_secure_cookie(), true ); + } + } + } + + /** + * Should the session cookie be secure? + * + * @since 3.6.0 + * @return bool + */ + protected function use_secure_cookie() { + return apply_filters( 'wc_session_use_secure_cookie', wc_site_is_https() && is_ssl() ); + } + + /** + * Return true if the current user has an active session, i.e. a cookie to retrieve values. + * + * @return bool + */ + public function has_session() { + return isset( $_COOKIE[ $this->_cookie ] ) || $this->_has_cookie || is_user_logged_in(); // @codingStandardsIgnoreLine. + } + + /** + * Set session expiration. + */ + public function set_session_expiration() { + $this->_session_expiring = time() + intval( apply_filters( 'wc_session_expiring', 60 * 60 * 47 ) ); // 47 Hours. + $this->_session_expiration = time() + intval( apply_filters( 'wc_session_expiration', 60 * 60 * 48 ) ); // 48 Hours. + } + + /** + * Generate a unique customer ID for guests, or return user ID if logged in. + * + * Uses Portable PHP password hashing framework to generate a unique cryptographically strong ID. + * + * @return string + */ + public function generate_customer_id() { + $customer_id = ''; + + if ( is_user_logged_in() ) { + $customer_id = strval( get_current_user_id() ); + } + + if ( empty( $customer_id ) ) { + require_once ABSPATH . 'wp-includes/class-phpass.php'; + $hasher = new PasswordHash( 8, false ); + $customer_id = md5( $hasher->get_random_bytes( 32 ) ); + } + + return $customer_id; + } + + /** + * Get session unique ID for requests if session is initialized or user ID if logged in. + * Introduced to help with unit tests. + * + * @since 5.3.0 + * @return string + */ + public function get_customer_unique_id() { + $customer_id = ''; + + if ( $this->has_session() && $this->_customer_id ) { + $customer_id = $this->_customer_id; + } elseif ( is_user_logged_in() ) { + $customer_id = (string) get_current_user_id(); + } + + return $customer_id; + } + + /** + * Get the session cookie, if set. Otherwise return false. + * + * Session cookies without a customer ID are invalid. + * + * @return bool|array + */ + public function get_session_cookie() { + $cookie_value = isset( $_COOKIE[ $this->_cookie ] ) ? wp_unslash( $_COOKIE[ $this->_cookie ] ) : false; // @codingStandardsIgnoreLine. + + if ( empty( $cookie_value ) || ! is_string( $cookie_value ) ) { + return false; + } + + list( $customer_id, $session_expiration, $session_expiring, $cookie_hash ) = explode( '||', $cookie_value ); + + if ( empty( $customer_id ) ) { + return false; + } + + // Validate hash. + $to_hash = $customer_id . '|' . $session_expiration; + $hash = hash_hmac( 'md5', $to_hash, wp_hash( $to_hash ) ); + + if ( empty( $cookie_hash ) || ! hash_equals( $hash, $cookie_hash ) ) { + return false; + } + + return array( $customer_id, $session_expiration, $session_expiring, $cookie_hash ); + } + + /** + * Get session data. + * + * @return array + */ + public function get_session_data() { + return $this->has_session() ? (array) $this->get_session( $this->_customer_id, array() ) : array(); + } + + /** + * Gets a cache prefix. This is used in session names so the entire cache can be invalidated with 1 function call. + * + * @return string + */ + private function get_cache_prefix() { + return WC_Cache_Helper::get_cache_prefix( WC_SESSION_CACHE_GROUP ); + } + + /** + * Save data and delete guest session. + * + * @param int $old_session_key session ID before user logs in. + */ + public function save_data( $old_session_key = 0 ) { + // Dirty if something changed - prevents saving nothing new. + if ( $this->_dirty && $this->has_session() ) { + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "INSERT INTO {$wpdb->prefix}woocommerce_sessions (`session_key`, `session_value`, `session_expiry`) VALUES (%s, %s, %d) + ON DUPLICATE KEY UPDATE `session_value` = VALUES(`session_value`), `session_expiry` = VALUES(`session_expiry`)", + $this->_customer_id, + maybe_serialize( $this->_data ), + $this->_session_expiration + ) + ); + + wp_cache_set( $this->get_cache_prefix() . $this->_customer_id, $this->_data, WC_SESSION_CACHE_GROUP, $this->_session_expiration - time() ); + $this->_dirty = false; + if ( get_current_user_id() != $old_session_key && ! is_object( get_user_by( 'id', $old_session_key ) ) ) { + $this->delete_session( $old_session_key ); + } + } + } + + /** + * Destroy all session data. + */ + public function destroy_session() { + $this->delete_session( $this->_customer_id ); + $this->forget_session(); + } + + /** + * Forget all session data without destroying it. + */ + public function forget_session() { + wc_setcookie( $this->_cookie, '', time() - YEAR_IN_SECONDS, $this->use_secure_cookie(), true ); + + wc_empty_cart(); + + $this->_data = array(); + $this->_dirty = false; + $this->_customer_id = $this->generate_customer_id(); + } + + /** + * When a user is logged out, ensure they have a unique nonce by using the customer/session ID. + * + * @deprecated 5.3.0 + * @param int $uid User ID. + * @return int|string + */ + public function nonce_user_logged_out( $uid ) { + wc_deprecated_function( 'WC_Session_Handler::nonce_user_logged_out', '5.3', 'WC_Session_Handler::maybe_update_nonce_user_logged_out' ); + + return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid; + } + + /** + * When a user is logged out, ensure they have a unique nonce to manage cart and more using the customer/session ID. + * This filter runs everything `wp_verify_nonce()` and `wp_create_nonce()` gets called. + * + * @since 5.3.0 + * @param int $uid User ID. + * @param string $action The nonce action. + * @return int|string + */ + public function maybe_update_nonce_user_logged_out( $uid, $action ) { + if ( Automattic\WooCommerce\Utilities\StringUtil::starts_with( $action, 'woocommerce' ) ) { + return $this->has_session() && $this->_customer_id ? $this->_customer_id : $uid; + } + + return $uid; + } + + /** + * Cleanup session data from the database and clear caches. + */ + public function cleanup_sessions() { + global $wpdb; + + $wpdb->query( $wpdb->prepare( "DELETE FROM $this->_table WHERE session_expiry < %d", time() ) ); // @codingStandardsIgnoreLine. + + if ( class_exists( 'WC_Cache_Helper' ) ) { + WC_Cache_Helper::invalidate_cache_group( WC_SESSION_CACHE_GROUP ); + } + } + + /** + * Returns the session. + * + * @param string $customer_id Custo ID. + * @param mixed $default Default session value. + * @return string|array + */ + public function get_session( $customer_id, $default = false ) { + global $wpdb; + + if ( Constants::is_defined( 'WP_SETUP_CONFIG' ) ) { + return false; + } + + // Try to get it from the cache, it will return false if not present or if object cache not in use. + $value = wp_cache_get( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); + + if ( false === $value ) { + $value = $wpdb->get_var( $wpdb->prepare( "SELECT session_value FROM $this->_table WHERE session_key = %s", $customer_id ) ); // @codingStandardsIgnoreLine. + + if ( is_null( $value ) ) { + $value = $default; + } + + $cache_duration = $this->_session_expiration - time(); + if ( 0 < $cache_duration ) { + wp_cache_add( $this->get_cache_prefix() . $customer_id, $value, WC_SESSION_CACHE_GROUP, $cache_duration ); + } + } + + return maybe_unserialize( $value ); + } + + /** + * Delete the session from the cache and database. + * + * @param int $customer_id Customer ID. + */ + public function delete_session( $customer_id ) { + global $wpdb; + + wp_cache_delete( $this->get_cache_prefix() . $customer_id, WC_SESSION_CACHE_GROUP ); + + $wpdb->delete( + $this->_table, + array( + 'session_key' => $customer_id, + ) + ); + } + + /** + * Update the session expiry timestamp. + * + * @param string $customer_id Customer ID. + * @param int $timestamp Timestamp to expire the cookie. + */ + public function update_session_timestamp( $customer_id, $timestamp ) { + global $wpdb; + + $wpdb->update( + $this->_table, + array( + 'session_expiry' => $timestamp, + ), + array( + 'session_key' => $customer_id, + ), + array( + '%d', + ) + ); + } +} diff --git a/includes/class-wc-shipping-rate.php b/includes/class-wc-shipping-rate.php new file mode 100644 index 0000000..72bb068 --- /dev/null +++ b/includes/class-wc-shipping-rate.php @@ -0,0 +1,252 @@ + '', + 'method_id' => '', + 'instance_id' => 0, + 'label' => '', + 'cost' => 0, + 'taxes' => array(), + ); + + /** + * Stores meta data for this rate. + * + * @since 2.6.0 + * @var array + */ + protected $meta_data = array(); + + /** + * Constructor. + * + * @param string $id Shipping rate ID. + * @param string $label Shipping rate label. + * @param integer $cost Cost. + * @param array $taxes Taxes applied to shipping rate. + * @param string $method_id Shipping method ID. + * @param int $instance_id Shipping instance ID. + */ + public function __construct( $id = '', $label = '', $cost = 0, $taxes = array(), $method_id = '', $instance_id = 0 ) { + $this->set_id( $id ); + $this->set_label( $label ); + $this->set_cost( $cost ); + $this->set_taxes( $taxes ); + $this->set_method_id( $method_id ); + $this->set_instance_id( $instance_id ); + } + + /** + * Magic methods to support direct access to props. + * + * @since 3.2.0 + * @param string $key Key. + * @return bool + */ + public function __isset( $key ) { + return isset( $this->data[ $key ] ); + } + + /** + * Magic methods to support direct access to props. + * + * @since 3.2.0 + * @param string $key Key. + * @return mixed + */ + public function __get( $key ) { + if ( is_callable( array( $this, "get_{$key}" ) ) ) { + return $this->{"get_{$key}"}(); + } elseif ( isset( $this->data[ $key ] ) ) { + return $this->data[ $key ]; + } else { + return ''; + } + } + + /** + * Magic methods to support direct access to props. + * + * @since 3.2.0 + * @param string $key Key. + * @param mixed $value Value. + */ + public function __set( $key, $value ) { + if ( is_callable( array( $this, "set_{$key}" ) ) ) { + $this->{"set_{$key}"}( $value ); + } else { + $this->data[ $key ] = $value; + } + } + + /** + * Set ID for the rate. This is usually a combination of the method and instance IDs. + * + * @since 3.2.0 + * @param string $id Shipping rate ID. + */ + public function set_id( $id ) { + $this->data['id'] = (string) $id; + } + + /** + * Set shipping method ID the rate belongs to. + * + * @since 3.2.0 + * @param string $method_id Shipping method ID. + */ + public function set_method_id( $method_id ) { + $this->data['method_id'] = (string) $method_id; + } + + /** + * Set instance ID the rate belongs to. + * + * @since 3.2.0 + * @param int $instance_id Instance ID. + */ + public function set_instance_id( $instance_id ) { + $this->data['instance_id'] = absint( $instance_id ); + } + + /** + * Set rate label. + * + * @since 3.2.0 + * @param string $label Shipping rate label. + */ + public function set_label( $label ) { + $this->data['label'] = (string) $label; + } + + /** + * Set rate cost. + * + * @todo 4.0 Prevent negative value being set. #19293 + * @since 3.2.0 + * @param string $cost Shipping rate cost. + */ + public function set_cost( $cost ) { + $this->data['cost'] = $cost; + } + + /** + * Set rate taxes. + * + * @since 3.2.0 + * @param array $taxes List of taxes applied to shipping rate. + */ + public function set_taxes( $taxes ) { + $this->data['taxes'] = ! empty( $taxes ) && is_array( $taxes ) ? $taxes : array(); + } + + /** + * Get ID for the rate. This is usually a combination of the method and instance IDs. + * + * @since 3.2.0 + * @return string + */ + public function get_id() { + return apply_filters( 'woocommerce_shipping_rate_id', $this->data['id'], $this ); + } + + /** + * Get shipping method ID the rate belongs to. + * + * @since 3.2.0 + * @return string + */ + public function get_method_id() { + return apply_filters( 'woocommerce_shipping_rate_method_id', $this->data['method_id'], $this ); + } + + /** + * Get instance ID the rate belongs to. + * + * @since 3.2.0 + * @return int + */ + public function get_instance_id() { + return apply_filters( 'woocommerce_shipping_rate_instance_id', $this->data['instance_id'], $this ); + } + + /** + * Get rate label. + * + * @return string + */ + public function get_label() { + return apply_filters( 'woocommerce_shipping_rate_label', $this->data['label'], $this ); + } + + /** + * Get rate cost. + * + * @since 3.2.0 + * @return string + */ + public function get_cost() { + return apply_filters( 'woocommerce_shipping_rate_cost', $this->data['cost'], $this ); + } + + /** + * Get rate taxes. + * + * @since 3.2.0 + * @return array + */ + public function get_taxes() { + return apply_filters( 'woocommerce_shipping_rate_taxes', $this->data['taxes'], $this ); + } + + /** + * Get shipping tax. + * + * @return array + */ + public function get_shipping_tax() { + return apply_filters( 'woocommerce_get_shipping_tax', count( $this->taxes ) > 0 && ! WC()->customer->get_is_vat_exempt() ? array_sum( $this->taxes ) : 0, $this ); + } + + /** + * Add some meta data for this rate. + * + * @since 2.6.0 + * @param string $key Key. + * @param string $value Value. + */ + public function add_meta_data( $key, $value ) { + $this->meta_data[ wc_clean( $key ) ] = wc_clean( $value ); + } + + /** + * Get all meta data for this rate. + * + * @since 2.6.0 + * @return array + */ + public function get_meta_data() { + return $this->meta_data; + } +} diff --git a/includes/class-wc-shipping-zone.php b/includes/class-wc-shipping-zone.php new file mode 100644 index 0000000..f092789 --- /dev/null +++ b/includes/class-wc-shipping-zone.php @@ -0,0 +1,462 @@ + '', + '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 View or edit context. + * @return string + */ + public function get_zone_name( $context = 'view' ) { + return $this->get_prop( 'zone_name', $context ); + } + + /** + * Get zone order. + * + * @param string $context View or edit context. + * @return int + */ + public function get_zone_order( $context = 'view' ) { + return $this->get_prop( 'zone_order', $context ); + } + + /** + * Get zone locations. + * + * @param string $context View or edit 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 Max locations to return. + * @param string $context View or edit 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 ( count( $location_parts ) > $max ) { + $remaining = count( $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 $enabled_only Only return enabled methods. + * @param string $context Getting shipping methods for what context. Valid values, admin, json. + * @return array of objects + */ + public function get_shipping_methods( $enabled_only = false, $context = 'admin' ) { + 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 ]; + $instance_id = $raw_method->instance_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[ $instance_id ] = new $class_name_of_instance( $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[ $instance_id ] = new $class_name( $instance_id ); + } + } + + // Let's make sure that we have an instance before setting its attributes. + if ( is_object( $methods[ $instance_id ] ) ) { + $methods[ $instance_id ]->method_order = absint( $raw_method->method_order ); + $methods[ $instance_id ]->enabled = $raw_method->is_enabled ? 'yes' : 'no'; + $methods[ $instance_id ]->has_settings = $methods[ $instance_id ]->has_settings(); + $methods[ $instance_id ]->settings_html = $methods[ $instance_id ]->supports( 'instance-settings-modal' ) ? $methods[ $instance_id ]->get_admin_options_html() : false; + $methods[ $instance_id ]->method_description = wp_kses_post( wpautop( $methods[ $instance_id ]->method_description ) ); + } + + if ( 'json' === $context ) { + // We don't want the entire object in this context, just the public props. + $methods[ $instance_id ] = (object) get_object_vars( $methods[ $instance_id ] ); + unset( $methods[ $instance_id ]->instance_form_fields, $methods[ $instance_id ]->form_fields ); + } + } + } + + 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 Value to set. + */ + public function set_zone_name( $set ) { + $this->set_prop( 'zone_name', wc_clean( $set ) ); + } + + /** + * Set zone order. Value to set. + * + * @param int $set Value to set. + */ + public function set_zone_order( $set ) { + $this->set_prop( 'zone_order', absint( $set ) ); + } + + /** + * Set zone locations. + * + * @since 3.0.0 + * @param array $locations Value to set. + */ + public function set_zone_locations( $locations ) { + if ( 0 !== $this->get_id() ) { + $this->set_prop( 'zone_locations', $locations ); + } + } + + /** + * -------------------------------------------------------------------------- + * Other + * -------------------------------------------------------------------------- + */ + + /** + * 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 ) { + return $this->get_id(); + } + + /** + * Trigger action before saving to the DB. Allows you to adjust object props before save. + * + * @param WC_Data $this The object being saved. + * @param WC_Data_Store_WP $data_store THe data store persisting the data. + */ + do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); + + if ( null !== $this->get_id() ) { + $this->data_store->update( $this ); + } else { + $this->data_store->create( $this ); + } + + /** + * Trigger action after saving to the DB. + * + * @param WC_Data $this The object being saved. + * @param WC_Data_Store_WP $data_store THe data store persisting the data. + */ + do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store ); + + return $this->get_id(); + } + + /** + * 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 Location to check. + * @return boolean + */ + private function location_is_continent( $location ) { + return 'continent' === $location->type; + } + + /** + * Location type detection. + * + * @param object $location Location to check. + * @return boolean + */ + private function location_is_country( $location ) { + return 'country' === $location->type; + } + + /** + * Location type detection. + * + * @param object $location Location to check. + * @return boolean + */ + private function location_is_state( $location ) { + return 'state' === $location->type; + } + + /** + * Location type detection. + * + * @param object $location Location to check. + * @return boolean + */ + private function location_is_postcode( $location ) { + return 'postcode' === $location->type; + } + + /** + * Is passed location type valid? + * + * @param string $type Type to check. + * @return boolean + */ + public function is_valid_location_type( $type ) { + return in_array( $type, apply_filters( 'woocommerce_valid_location_types', array( 'postcode', 'state', 'country', 'continent' ) ), true ); + } + + /** + * Add location (state or postcode) to a zone. + * + * @param string $code Location 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, true ) ) { + 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 ), true ) ) { + $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 Shipping method 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 0000000..4acff8b --- /dev/null +++ b/includes/class-wc-shipping-zones.php @@ -0,0 +1,142 @@ +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( false, $context ); + } + + return $zones; + } + + /** + * Get shipping zone using it's ID + * + * @since 2.6.0 + * @param int $zone_id 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 Get by 'zone_id' or 'instance_id'. + * @param int $id ID. + * @return WC_Shipping_Zone|bool + */ + public static function get_zone_by( $by = 'zone_id', $id = 0 ) { + $zone_id = false; + + 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 int $instance_id Instance ID. + * @return bool|WC_Shipping_Method + */ + 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 ), true ) ) { + $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 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 array $package Shipping 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 new file mode 100644 index 0000000..d915798 --- /dev/null +++ b/includes/class-wc-shipping.php @@ -0,0 +1,407 @@ +cart->get_shipping_total(); + } + if ( 'shipping_taxes' === $name ) { + return WC()->cart->get_shipping_taxes(); + } + } + + /** + * Initialize shipping. + */ + public function __construct() { + $this->enabled = wc_shipping_enabled(); + + if ( $this->enabled ) { + $this->init(); + } + } + + /** + * Initialize shipping. + */ + public function init() { + do_action( 'woocommerce_shipping_init' ); + } + + /** + * Shipping methods register themselves by returning their main class name through the woocommerce_shipping_methods filter. + * + * @return array + */ + public function get_shipping_method_class_names() { + // Unique Method ID => Method Class name. + $shipping_methods = array( + 'flat_rate' => 'WC_Shipping_Flat_Rate', + 'free_shipping' => 'WC_Shipping_Free_Shipping', + 'local_pickup' => 'WC_Shipping_Local_Pickup', + ); + + // For backwards compatibility with 2.5.x we load any ENABLED legacy shipping methods here. + $maybe_load_legacy_methods = array( 'flat_rate', 'free_shipping', 'international_delivery', 'local_delivery', 'local_pickup' ); + + foreach ( $maybe_load_legacy_methods as $method ) { + $options = get_option( 'woocommerce_' . $method . '_settings' ); + if ( $options && isset( $options['enabled'] ) && 'yes' === $options['enabled'] ) { + $shipping_methods[ 'legacy_' . $method ] = 'WC_Shipping_Legacy_' . $method; + } + } + + return apply_filters( 'woocommerce_shipping_methods', $shipping_methods ); + } + + /** + * Loads all shipping methods which are hooked in. + * If a $package is passed, some methods may add themselves conditionally and zones will be used. + * + * @param array $package Package information. + * @return WC_Shipping_Method[] + */ + public function load_shipping_methods( $package = array() ) { + if ( ! empty( $package ) ) { + $debug_mode = 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ); + $shipping_zone = WC_Shipping_Zones::get_zone_matching_package( $package ); + $this->shipping_methods = $shipping_zone->get_shipping_methods( true ); + + // translators: %s: shipping zone name. + $matched_zone_notice = sprintf( __( 'Customer matched zone "%s"', 'woocommerce' ), $shipping_zone->get_zone_name() ); + + // Debug output. + if ( $debug_mode && ! Constants::is_defined( 'WOOCOMMERCE_CHECKOUT' ) && ! Constants::is_defined( 'WC_DOING_AJAX' ) && ! wc_has_notice( $matched_zone_notice ) ) { + wc_add_notice( $matched_zone_notice ); + } + } else { + $this->shipping_methods = array(); + } + + // For the settings in the backend, and for non-shipping zone methods, we still need to load any registered classes here. + foreach ( $this->get_shipping_method_class_names() as $method_id => $method_class ) { + $this->register_shipping_method( $method_class ); + } + + // Methods can register themselves manually through this hook if necessary. + do_action( 'woocommerce_load_shipping_methods', $package ); + + // Return loaded methods. + return $this->get_shipping_methods(); + } + + /** + * Register a shipping method. + * + * @param object|string $method Either the name of the method's class, or an instance of the method's class. + * + * @return bool|void + */ + public function register_shipping_method( $method ) { + if ( ! is_object( $method ) ) { + if ( ! class_exists( $method ) ) { + return false; + } + $method = new $method(); + } + if ( is_null( $this->shipping_methods ) ) { + $this->shipping_methods = array(); + } + $this->shipping_methods[ $method->id ] = $method; + } + + /** + * Unregister shipping methods. + */ + public function unregister_shipping_methods() { + $this->shipping_methods = null; + } + + /** + * Returns all registered shipping methods for usage. + * + * @return WC_Shipping_Method[] + */ + public function get_shipping_methods() { + if ( is_null( $this->shipping_methods ) ) { + $this->load_shipping_methods(); + } + return $this->shipping_methods; + } + + /** + * Get an array of shipping classes. + * + * @return array + */ + public function get_shipping_classes() { + if ( empty( $this->shipping_classes ) ) { + $classes = get_terms( + 'product_shipping_class', + array( + 'hide_empty' => '0', + 'orderby' => 'name', + ) + ); + $this->shipping_classes = ! is_wp_error( $classes ) ? $classes : array(); + } + return apply_filters( 'woocommerce_get_shipping_classes', $this->shipping_classes ); + } + + /** + * Calculate shipping for (multiple) packages of cart items. + * + * @param array $packages multi-dimensional array of cart items to calc shipping for. + * @return array Array of calculated packages. + */ + public function calculate_shipping( $packages = array() ) { + $this->packages = array(); + + if ( ! $this->enabled || empty( $packages ) ) { + return array(); + } + + // Calculate costs for passed packages. + foreach ( $packages as $package_key => $package ) { + $this->packages[ $package_key ] = $this->calculate_shipping_for_package( $package, $package_key ); + } + + /** + * Allow packages to be reorganized after calculating the shipping. + * + * This filter can be used to apply some extra manipulation after the shipping costs are calculated for the packages + * but before WooCommerce does anything with them. A good example of usage is to merge the shipping methods for multiple + * packages for marketplaces. + * + * @since 2.6.0 + * + * @param array $packages The array of packages after shipping costs are calculated. + */ + $this->packages = array_filter( (array) apply_filters( 'woocommerce_shipping_packages', $this->packages ) ); + + return $this->packages; + } + + /** + * See if package is shippable. + * + * Packages are shippable until proven otherwise e.g. after getting a shipping country. + * + * @param array $package Package of cart items. + * @return bool + */ + public function is_package_shippable( $package ) { + // Packages are shippable until proven otherwise. + if ( empty( $package['destination']['country'] ) ) { + return true; + } + + $allowed = array_keys( WC()->countries->get_shipping_countries() ); + return in_array( $package['destination']['country'], $allowed, true ); + } + + /** + * Calculate shipping rates for a package, + * + * Calculates each shipping methods cost. Rates are stored in the session based on the package hash to avoid re-calculation every page load. + * + * @param array $package Package of cart items. + * @param int $package_key Index of the package being calculated. Used to cache multiple package rates. + * + * @return array|bool + */ + public function calculate_shipping_for_package( $package = array(), $package_key = 0 ) { + // If shipping is disabled or the package is invalid, return false. + if ( ! $this->enabled || empty( $package ) ) { + return false; + } + + $package['rates'] = array(); + + // If the package is not shippable, e.g. trying to ship to an invalid country, do not calculate rates. + if ( ! $this->is_package_shippable( $package ) ) { + return $package; + } + + // Check if we need to recalculate shipping for this package. + $package_to_hash = $package; + + // Remove data objects so hashes are consistent. + foreach ( $package_to_hash['contents'] as $item_id => $item ) { + unset( $package_to_hash['contents'][ $item_id ]['data'] ); + } + + // Get rates stored in the WC session data for this package. + $wc_session_key = 'shipping_for_package_' . $package_key; + $stored_rates = WC()->session->get( $wc_session_key ); + + // Calculate the hash for this package so we can tell if it's changed since last calculation. + $package_hash = 'wc_ship_' . md5( wp_json_encode( $package_to_hash ) . WC_Cache_Helper::get_transient_version( 'shipping' ) ); + + if ( ! is_array( $stored_rates ) || $package_hash !== $stored_rates['package_hash'] || 'yes' === get_option( 'woocommerce_shipping_debug_mode', 'no' ) ) { + foreach ( $this->load_shipping_methods( $package ) as $shipping_method ) { + if ( ! $shipping_method->supports( 'shipping-zones' ) || $shipping_method->get_instance_id() ) { + /** + * Fires before getting shipping rates for a package. + * + * @since 4.3.0 + * @param array $package Package of cart items. + * @param WC_Shipping_Method $shipping_method Shipping method instance. + */ + do_action( 'woocommerce_before_get_rates_for_package', $package, $shipping_method ); + + // Use + instead of array_merge to maintain numeric keys. + $package['rates'] = $package['rates'] + $shipping_method->get_rates_for_package( $package ); + + /** + * Fires after getting shipping rates for a package. + * + * @since 4.3.0 + * @param array $package Package of cart items. + * @param WC_Shipping_Method $shipping_method Shipping method instance. + */ + do_action( 'woocommerce_after_get_rates_for_package', $package, $shipping_method ); + } + } + + // Filter the calculated rates. + $package['rates'] = apply_filters( 'woocommerce_package_rates', $package['rates'], $package ); + + // Store in session to avoid recalculation. + WC()->session->set( + $wc_session_key, + array( + 'package_hash' => $package_hash, + 'rates' => $package['rates'], + ) + ); + } else { + $package['rates'] = $stored_rates['rates']; + } + + return $package; + } + + /** + * Get packages. + * + * @return array + */ + public function get_packages() { + return $this->packages; + } + + /** + * Reset shipping. + * + * Reset the totals for shipping as a whole. + */ + public function reset_shipping() { + unset( WC()->session->chosen_shipping_methods ); + $this->packages = array(); + } + + /** + * Deprecated + * + * @deprecated 2.6.0 Was previously used to determine sort order of methods, but this is now controlled by zones and thus unused. + */ + public function sort_shipping_methods() { + wc_deprecated_function( 'sort_shipping_methods', '2.6' ); + return $this->shipping_methods; + } +} diff --git a/includes/class-wc-shortcodes.php b/includes/class-wc-shortcodes.php new file mode 100644 index 0000000..b8eaa08 --- /dev/null +++ b/includes/class-wc-shortcodes.php @@ -0,0 +1,699 @@ + __CLASS__ . '::product', + 'product_page' => __CLASS__ . '::product_page', + 'product_category' => __CLASS__ . '::product_category', + 'product_categories' => __CLASS__ . '::product_categories', + 'add_to_cart' => __CLASS__ . '::product_add_to_cart', + 'add_to_cart_url' => __CLASS__ . '::product_add_to_cart_url', + 'products' => __CLASS__ . '::products', + 'recent_products' => __CLASS__ . '::recent_products', + 'sale_products' => __CLASS__ . '::sale_products', + 'best_selling_products' => __CLASS__ . '::best_selling_products', + 'top_rated_products' => __CLASS__ . '::top_rated_products', + 'featured_products' => __CLASS__ . '::featured_products', + 'product_attribute' => __CLASS__ . '::product_attribute', + 'related_products' => __CLASS__ . '::related_products', + 'shop_messages' => __CLASS__ . '::shop_messages', + 'woocommerce_order_tracking' => __CLASS__ . '::order_tracking', + 'woocommerce_cart' => __CLASS__ . '::cart', + 'woocommerce_checkout' => __CLASS__ . '::checkout', + 'woocommerce_my_account' => __CLASS__ . '::my_account', + ); + + foreach ( $shortcodes as $shortcode => $function ) { + add_shortcode( apply_filters( "{$shortcode}_shortcode_tag", $shortcode ), $function ); + } + + // Alias for pre 2.1 compatibility. + add_shortcode( 'woocommerce_messages', __CLASS__ . '::shop_messages' ); + } + + /** + * Shortcode Wrapper. + * + * @param string[] $function Callback function. + * @param array $atts Attributes. Default to empty array. + * @param array $wrapper Customer wrapper data. + * + * @return string + */ + public static function shortcode_wrapper( + $function, + $atts = array(), + $wrapper = array( + 'class' => 'woocommerce', + 'before' => null, + 'after' => null, + ) + ) { + ob_start(); + + // @codingStandardsIgnoreStart + echo empty( $wrapper['before'] ) ? '
    ' : $wrapper['before']; + call_user_func( $function, $atts ); + echo empty( $wrapper['after'] ) ? '
    ' : $wrapper['after']; + // @codingStandardsIgnoreEnd + + return 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. + * + * @param array $atts Attributes. + * @return string + */ + public static function checkout( $atts ) { + return self::shortcode_wrapper( array( 'WC_Shortcode_Checkout', 'output' ), $atts ); + } + + /** + * Order tracking page shortcode. + * + * @param array $atts Attributes. + * @return string + */ + public static function order_tracking( $atts ) { + return self::shortcode_wrapper( array( 'WC_Shortcode_Order_Tracking', 'output' ), $atts ); + } + + /** + * My account page shortcode. + * + * @param array $atts Attributes. + * @return string + */ + public static function my_account( $atts ) { + return self::shortcode_wrapper( array( 'WC_Shortcode_My_Account', 'output' ), $atts ); + } + + /** + * List products in a category shortcode. + * + * @param array $atts Attributes. + * @return string + */ + public static function product_category( $atts ) { + if ( empty( $atts['category'] ) ) { + return ''; + } + + $atts = array_merge( + array( + 'limit' => '12', + 'columns' => '4', + 'orderby' => 'menu_order title', + 'order' => 'ASC', + 'category' => '', + 'cat_operator' => 'IN', + ), + (array) $atts + ); + + $shortcode = new WC_Shortcode_Products( $atts, 'product_category' ); + + return $shortcode->get_content(); + } + + /** + * List all (or limited) product categories. + * + * @param array $atts Attributes. + * @return string + */ + public static function product_categories( $atts ) { + if ( isset( $atts['number'] ) ) { + $atts['limit'] = $atts['number']; + } + + $atts = shortcode_atts( + array( + 'limit' => '-1', + 'orderby' => 'name', + 'order' => 'ASC', + 'columns' => '4', + 'hide_empty' => 1, + 'parent' => '', + 'ids' => '', + ), + $atts, + 'product_categories' + ); + + $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' => $atts['orderby'], + 'order' => $atts['order'], + 'hide_empty' => $hide_empty, + 'include' => $ids, + 'pad_counts' => true, + 'child_of' => $atts['parent'], + ); + + $product_categories = apply_filters( + 'woocommerce_product_categories', + get_terms( 'product_cat', $args ) + ); + + if ( '' !== $atts['parent'] ) { + $product_categories = wp_list_filter( + $product_categories, + array( + 'parent' => $atts['parent'], + ) + ); + } + + if ( $hide_empty ) { + foreach ( $product_categories as $key => $category ) { + if ( 0 === $category->count ) { + unset( $product_categories[ $key ] ); + } + } + } + + $atts['limit'] = '-1' === $atts['limit'] ? null : intval( $atts['limit'] ); + if ( $atts['limit'] ) { + $product_categories = array_slice( $product_categories, 0, $atts['limit'] ); + } + + $columns = absint( $atts['columns'] ); + + wc_set_loop_prop( 'columns', $columns ); + wc_set_loop_prop( 'is_shortcode', true ); + + ob_start(); + + if ( $product_categories ) { + woocommerce_product_loop_start(); + + foreach ( $product_categories as $category ) { + wc_get_template( + 'content-product_cat.php', + array( + 'category' => $category, + ) + ); + } + + woocommerce_product_loop_end(); + } + + wc_reset_loop(); + + return '
    ' . ob_get_clean() . '
    '; + } + + /** + * Recent Products shortcode. + * + * @param array $atts Attributes. + * @return string + */ + public static function recent_products( $atts ) { + $atts = array_merge( + array( + 'limit' => '12', + 'columns' => '4', + 'orderby' => 'date', + 'order' => 'DESC', + 'category' => '', + 'cat_operator' => 'IN', + ), + (array) $atts + ); + + $shortcode = new WC_Shortcode_Products( $atts, 'recent_products' ); + + return $shortcode->get_content(); + } + + /** + * List multiple products shortcode. + * + * @param array $atts Attributes. + * @return string + */ + public static function products( $atts ) { + $atts = (array) $atts; + $type = 'products'; + + // Allow list product based on specific cases. + if ( isset( $atts['on_sale'] ) && wc_string_to_bool( $atts['on_sale'] ) ) { + $type = 'sale_products'; + } elseif ( isset( $atts['best_selling'] ) && wc_string_to_bool( $atts['best_selling'] ) ) { + $type = 'best_selling_products'; + } elseif ( isset( $atts['top_rated'] ) && wc_string_to_bool( $atts['top_rated'] ) ) { + $type = 'top_rated_products'; + } + + $shortcode = new WC_Shortcode_Products( $atts, $type ); + + return $shortcode->get_content(); + } + + /** + * Display a single product. + * + * @param array $atts Attributes. + * @return string + */ + public static function product( $atts ) { + if ( empty( $atts ) ) { + return ''; + } + + $atts['skus'] = isset( $atts['sku'] ) ? $atts['sku'] : ''; + $atts['ids'] = isset( $atts['id'] ) ? $atts['id'] : ''; + $atts['limit'] = '1'; + $shortcode = new WC_Shortcode_Products( (array) $atts, 'product' ); + + return $shortcode->get_content(); + } + + /** + * Display a single product price + cart button. + * + * @param array $atts Attributes. + * @return string + */ + public static function product_add_to_cart( $atts ) { + global $post; + + if ( empty( $atts ) ) { + return ''; + } + + $atts = shortcode_atts( + array( + 'id' => '', + 'class' => '', + 'quantity' => '1', + 'sku' => '', + 'style' => 'border:4px solid #ccc; padding: 12px;', + 'show_price' => 'true', + ), + $atts, + 'product_add_to_cart' + ); + + 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 = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ), true ) ? wc_setup_product_data( $product_data ) : false; + + if ( ! $product ) { + return ''; + } + + ob_start(); + + echo '

    '; + + if ( wc_string_to_bool( $atts['show_price'] ) ) { + // @codingStandardsIgnoreStart + echo $product->get_price_html(); + // @codingStandardsIgnoreEnd + } + + woocommerce_template_loop_add_to_cart( + array( + 'quantity' => $atts['quantity'], + ) + ); + + echo '

    '; + + // Restore Product global in case this is shown inside a product post. + wc_setup_product_data( $post ); + + return ob_get_clean(); + } + + /** + * Get the add to cart URL for a product. + * + * @param array $atts Attributes. + * @return string + */ + public static function product_add_to_cart_url( $atts ) { + if ( empty( $atts ) ) { + return ''; + } + + if ( isset( $atts['id'] ) ) { + $product_data = get_post( $atts['id'] ); + } elseif ( isset( $atts['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $atts['sku'] ); + $product_data = get_post( $product_id ); + } else { + return ''; + } + + $product = is_object( $product_data ) && in_array( $product_data->post_type, array( 'product', 'product_variation' ), true ) ? wc_setup_product_data( $product_data ) : false; + + if ( ! $product ) { + return ''; + } + + $_product = wc_get_product( $product_data ); + + return esc_url( $_product->add_to_cart_url() ); + } + + /** + * List all products on sale. + * + * @param array $atts Attributes. + * @return string + */ + public static function sale_products( $atts ) { + $atts = array_merge( + array( + 'limit' => '12', + 'columns' => '4', + 'orderby' => 'title', + 'order' => 'ASC', + 'category' => '', + 'cat_operator' => 'IN', + ), + (array) $atts + ); + + $shortcode = new WC_Shortcode_Products( $atts, 'sale_products' ); + + return $shortcode->get_content(); + } + + /** + * List best selling products on sale. + * + * @param array $atts Attributes. + * @return string + */ + public static function best_selling_products( $atts ) { + $atts = array_merge( + array( + 'limit' => '12', + 'columns' => '4', + 'category' => '', + 'cat_operator' => 'IN', + ), + (array) $atts + ); + + $shortcode = new WC_Shortcode_Products( $atts, 'best_selling_products' ); + + return $shortcode->get_content(); + } + + /** + * List top rated products on sale. + * + * @param array $atts Attributes. + * @return string + */ + public static function top_rated_products( $atts ) { + $atts = array_merge( + array( + 'limit' => '12', + 'columns' => '4', + 'orderby' => 'title', + 'order' => 'ASC', + 'category' => '', + 'cat_operator' => 'IN', + ), + (array) $atts + ); + + $shortcode = new WC_Shortcode_Products( $atts, 'top_rated_products' ); + + return $shortcode->get_content(); + } + + /** + * Output featured products. + * + * @param array $atts Attributes. + * @return string + */ + public static function featured_products( $atts ) { + $atts = array_merge( + array( + 'limit' => '12', + 'columns' => '4', + 'orderby' => 'date', + 'order' => 'DESC', + 'category' => '', + 'cat_operator' => 'IN', + ), + (array) $atts + ); + + $atts['visibility'] = 'featured'; + + $shortcode = new WC_Shortcode_Products( $atts, 'featured_products' ); + + return $shortcode->get_content(); + } + + /** + * Show a single product page. + * + * @param array $atts Attributes. + * @return string + */ + public static function product_page( $atts ) { + if ( empty( $atts ) ) { + return ''; + } + + if ( ! isset( $atts['id'] ) && ! isset( $atts['sku'] ) ) { + return ''; + } + + $args = array( + 'posts_per_page' => 1, + 'post_type' => 'product', + 'post_status' => ( ! empty( $atts['status'] ) ) ? $atts['status'] : 'publish', + 'ignore_sticky_posts' => 1, + 'no_found_rows' => 1, + ); + + if ( isset( $atts['sku'] ) ) { + $args['meta_query'][] = array( + 'key' => '_sku', + 'value' => sanitize_text_field( $atts['sku'] ), + 'compare' => '=', + ); + + $args['post_type'] = array( 'product', 'product_variation' ); + } + + if ( isset( $atts['id'] ) ) { + $args['p'] = absint( $atts['id'] ); + } + + // Don't render titles if desired. + if ( isset( $atts['show_title'] ) && ! $atts['show_title'] ) { + remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_title', 5 ); + } + + // Change form action to avoid redirect. + add_filter( 'woocommerce_add_to_cart_form_action', '__return_empty_string' ); + + $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 = wc_get_product_object( '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 ); + ?> + + is_single = true; + + ob_start(); + + global $wp_query; + + // Backup query object so following loops think this is a product page. + $previous_wp_query = $wp_query; + // @codingStandardsIgnoreStart + $wp_query = $single_product; + // @codingStandardsIgnoreEnd + + wp_enqueue_script( 'wc-single-product' ); + + while ( $single_product->have_posts() ) { + $single_product->the_post() + ?> +
    + +
    + ' . ob_get_clean() . ''; + } + + /** + * Show messages. + * + * @return string + */ + public static function shop_messages() { + if ( ! function_exists( 'wc_print_notices' ) ) { + return ''; + } + return '
    ' . wc_print_notices( true ) . '
    '; + } + + /** + * Order by rating. + * + * @deprecated 3.2.0 Use WC_Shortcode_Products::order_by_rating_post_clauses(). + * @param array $args Query args. + * @return array + */ + public static function order_by_rating_post_clauses( $args ) { + return WC_Shortcode_Products::order_by_rating_post_clauses( $args ); + } + + /** + * List products with an attribute shortcode. + * Example [product_attribute attribute="color" filter="black"]. + * + * @param array $atts Attributes. + * @return string + */ + public static function product_attribute( $atts ) { + $atts = array_merge( + array( + 'limit' => '12', + 'columns' => '4', + 'orderby' => 'title', + 'order' => 'ASC', + 'attribute' => '', + 'terms' => '', + ), + (array) $atts + ); + + if ( empty( $atts['attribute'] ) ) { + return ''; + } + + $shortcode = new WC_Shortcode_Products( $atts, 'product_attribute' ); + + return $shortcode->get_content(); + } + + /** + * List related products. + * + * @param array $atts Attributes. + * @return string + */ + public static function related_products( $atts ) { + if ( isset( $atts['per_page'] ) ) { + $atts['limit'] = $atts['per_page']; + } + + // @codingStandardsIgnoreStart + $atts = shortcode_atts( array( + 'limit' => '4', + 'columns' => '4', + 'orderby' => 'rand', + ), $atts, 'related_products' ); + // @codingStandardsIgnoreEnd + + ob_start(); + + // Rename arg. + $atts['posts_per_page'] = absint( $atts['limit'] ); + + woocommerce_related_products( $atts ); + + return ob_get_clean(); + } +} diff --git a/includes/class-wc-structured-data.php b/includes/class-wc-structured-data.php new file mode 100644 index 0000000..1d37788 --- /dev/null +++ b/includes/class-wc-structured-data.php @@ -0,0 +1,538 @@ +_data ) ) { + unset( $this->_data ); + } + + $this->_data[] = $data; + + return true; + } + + /** + * Gets data. + * + * @return array + */ + public function get_data() { + return $this->_data; + } + + /** + * Structures and returns data. + * + * List of types available by default for specific request: + * + * 'product', + * 'review', + * 'breadcrumblist', + * 'website', + * 'order', + * + * @param array $types Structured data types. + * @return array + */ + public function get_structured_data( $types ) { + $data = array(); + + // Put together the values of same type of structured data. + foreach ( $this->get_data() as $value ) { + $data[ strtolower( $value['@type'] ) ][] = $value; + } + + // Wrap the multiple values of each type inside a graph... Then add context to each type. + foreach ( $data as $type => $value ) { + $data[ $type ] = count( $value ) > 1 ? array( '@graph' => $value ) : $value[0]; + $data[ $type ] = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, $type, $value ) + $data[ $type ]; + } + + // If requested types, pick them up... Finally change the associative array to an indexed one. + $data = $types ? array_values( array_intersect_key( $data, array_flip( $types ) ) ) : array_values( $data ); + + if ( ! empty( $data ) ) { + if ( 1 < count( $data ) ) { + $data = apply_filters( 'woocommerce_structured_data_context', array( '@context' => 'https://schema.org/' ), $data, '', '' ) + array( '@graph' => $data ); + } else { + $data = $data[0]; + } + } + + return $data; + } + + /** + * Get data types for pages. + * + * @return array + */ + protected function get_data_type_for_page() { + $types = array(); + $types[] = is_shop() || is_product_category() || is_product() ? 'product' : ''; + $types[] = is_shop() && is_front_page() ? 'website' : ''; + $types[] = is_product() ? 'review' : ''; + $types[] = 'breadcrumblist'; + $types[] = 'order'; + + return array_filter( apply_filters( 'woocommerce_structured_data_type_for_page', $types ) ); + } + + /** + * Makes sure email structured data only outputs on non-plain text versions. + * + * @param WP_Order $order Order data. + * @param bool $sent_to_admin Send to admin (default: false). + * @param bool $plain_text Plain text email (default: false). + */ + public function output_email_structured_data( $order, $sent_to_admin = false, $plain_text = false ) { + if ( $plain_text ) { + return; + } + echo '
    '; + $this->output_structured_data(); + echo '
    '; + } + + /** + * Sanitizes, encodes and outputs structured data. + * + * Hooked into `wp_footer` action hook. + * Hooked into `woocommerce_email_order_details` action hook. + */ + public function output_structured_data() { + $types = $this->get_data_type_for_page(); + $data = $this->get_structured_data( $types ); + + if ( $data ) { + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + + /* + |-------------------------------------------------------------------------- + | Generators + |-------------------------------------------------------------------------- + | + | Methods for generating specific structured data types: + | + | - Product + | - Review + | - BreadcrumbList + | - WebSite + | - Order + | + | The generated data is stored into `$this->_data`. + | See the methods above for handling `$this->_data`. + | + */ + + /** + * Generates Product structured data. + * + * Hooked into `woocommerce_single_product_summary` action hook. + * + * @param WC_Product $product Product data (default: null). + */ + public function generate_product_data( $product = null ) { + if ( ! is_object( $product ) ) { + global $product; + } + + if ( ! is_a( $product, 'WC_Product' ) ) { + return; + } + + $shop_name = get_bloginfo( 'name' ); + $shop_url = home_url(); + $currency = get_woocommerce_currency(); + $permalink = get_permalink( $product->get_id() ); + $image = wp_get_attachment_url( $product->get_image_id() ); + + $markup = array( + '@type' => 'Product', + '@id' => $permalink . '#product', // Append '#product' to differentiate between this @id and the @id generated for the Breadcrumblist. + 'name' => wp_kses_post( $product->get_name() ), + 'url' => $permalink, + 'description' => wp_strip_all_tags( do_shortcode( $product->get_short_description() ? $product->get_short_description() : $product->get_description() ) ), + ); + + if ( $image ) { + $markup['image'] = $image; + } + + // Declare SKU or fallback to ID. + if ( $product->get_sku() ) { + $markup['sku'] = $product->get_sku(); + } else { + $markup['sku'] = $product->get_id(); + } + + if ( '' !== $product->get_price() ) { + // Assume prices will be valid until the end of next year, unless on sale and there is an end date. + $price_valid_until = gmdate( 'Y-12-31', time() + YEAR_IN_SECONDS ); + + if ( $product->is_type( 'variable' ) ) { + $lowest = $product->get_variation_price( 'min', false ); + $highest = $product->get_variation_price( 'max', false ); + + if ( $lowest === $highest ) { + $markup_offer = array( + '@type' => 'Offer', + 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ), + 'priceValidUntil' => $price_valid_until, + 'priceSpecification' => array( + 'price' => wc_format_decimal( $lowest, wc_get_price_decimals() ), + 'priceCurrency' => $currency, + 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false', + ), + ); + } else { + $markup_offer = array( + '@type' => 'AggregateOffer', + 'lowPrice' => wc_format_decimal( $lowest, wc_get_price_decimals() ), + 'highPrice' => wc_format_decimal( $highest, wc_get_price_decimals() ), + 'offerCount' => count( $product->get_children() ), + ); + } + } else { + if ( $product->is_on_sale() && $product->get_date_on_sale_to() ) { + $price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ); + } + $markup_offer = array( + '@type' => 'Offer', + 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), + 'priceValidUntil' => $price_valid_until, + 'priceSpecification' => array( + 'price' => wc_format_decimal( $product->get_price(), wc_get_price_decimals() ), + 'priceCurrency' => $currency, + 'valueAddedTaxIncluded' => wc_prices_include_tax() ? 'true' : 'false', + ), + ); + } + + $markup_offer += array( + 'priceCurrency' => $currency, + 'availability' => 'http://schema.org/' . ( $product->is_in_stock() ? 'InStock' : 'OutOfStock' ), + 'url' => $permalink, + 'seller' => array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ), + ); + + $markup['offers'] = array( apply_filters( 'woocommerce_structured_data_product_offer', $markup_offer, $product ) ); + } + + if ( $product->get_rating_count() && wc_review_ratings_enabled() ) { + $markup['aggregateRating'] = array( + '@type' => 'AggregateRating', + 'ratingValue' => $product->get_average_rating(), + 'reviewCount' => $product->get_review_count(), + ); + + // Markup 5 most recent rating/review. + $comments = get_comments( + array( + 'number' => 5, + 'post_id' => $product->get_id(), + 'status' => 'approve', + 'post_status' => 'publish', + 'post_type' => 'product', + 'parent' => 0, + 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + array( + 'key' => 'rating', + 'type' => 'NUMERIC', + 'compare' => '>', + 'value' => 0, + ), + ), + ) + ); + + if ( $comments ) { + $markup['review'] = array(); + foreach ( $comments as $comment ) { + $markup['review'][] = array( + '@type' => 'Review', + 'reviewRating' => array( + '@type' => 'Rating', + 'bestRating' => '5', + 'ratingValue' => get_comment_meta( $comment->comment_ID, 'rating', true ), + 'worstRating' => '1', + ), + 'author' => array( + '@type' => 'Person', + 'name' => get_comment_author( $comment ), + ), + 'reviewBody' => get_comment_text( $comment ), + 'datePublished' => get_comment_date( 'c', $comment ), + ); + } + } + } + + // Check we have required data. + if ( empty( $markup['aggregateRating'] ) && empty( $markup['offers'] ) && empty( $markup['review'] ) ) { + return; + } + + $this->set_data( apply_filters( 'woocommerce_structured_data_product', $markup, $product ) ); + } + + /** + * Generates Review structured data. + * + * Hooked into `woocommerce_review_meta` action hook. + * + * @param WP_Comment $comment Comment data. + */ + public function generate_review_data( $comment ) { + $markup = array(); + $markup['@type'] = 'Review'; + $markup['@id'] = get_comment_link( $comment->comment_ID ); + $markup['datePublished'] = get_comment_date( 'c', $comment->comment_ID ); + $markup['description'] = get_comment_text( $comment->comment_ID ); + $markup['itemReviewed'] = array( + '@type' => 'Product', + 'name' => get_the_title( $comment->comment_post_ID ), + ); + + // Skip replies unless they have a rating. + $rating = get_comment_meta( $comment->comment_ID, 'rating', true ); + + if ( $rating ) { + $markup['reviewRating'] = array( + '@type' => 'Rating', + 'bestRating' => '5', + 'ratingValue' => $rating, + 'worstRating' => '1', + ); + } elseif ( $comment->comment_parent ) { + return; + } + + $markup['author'] = array( + '@type' => 'Person', + 'name' => get_comment_author( $comment->comment_ID ), + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_review', $markup, $comment ) ); + } + + /** + * Generates BreadcrumbList structured data. + * + * Hooked into `woocommerce_breadcrumb` action hook. + * + * @param WC_Breadcrumb $breadcrumbs Breadcrumb data. + */ + public function generate_breadcrumblist_data( $breadcrumbs ) { + $crumbs = $breadcrumbs->get_breadcrumb(); + + if ( empty( $crumbs ) || ! is_array( $crumbs ) ) { + return; + } + + $markup = array(); + $markup['@type'] = 'BreadcrumbList'; + $markup['itemListElement'] = array(); + + foreach ( $crumbs as $key => $crumb ) { + $markup['itemListElement'][ $key ] = array( + '@type' => 'ListItem', + 'position' => $key + 1, + 'item' => array( + 'name' => $crumb[0], + ), + ); + + if ( ! empty( $crumb[1] ) ) { + $markup['itemListElement'][ $key ]['item'] += array( '@id' => $crumb[1] ); + } elseif ( isset( $_SERVER['HTTP_HOST'], $_SERVER['REQUEST_URI'] ) ) { + $current_url = set_url_scheme( 'http://' . wp_unslash( $_SERVER['HTTP_HOST'] ) . wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + $markup['itemListElement'][ $key ]['item'] += array( '@id' => $current_url ); + } + } + + $this->set_data( apply_filters( 'woocommerce_structured_data_breadcrumblist', $markup, $breadcrumbs ) ); + } + + /** + * Generates WebSite structured data. + * + * Hooked into `woocommerce_before_main_content` action hook. + */ + public function generate_website_data() { + $markup = array(); + $markup['@type'] = 'WebSite'; + $markup['name'] = get_bloginfo( 'name' ); + $markup['url'] = home_url(); + $markup['potentialAction'] = array( + '@type' => 'SearchAction', + 'target' => home_url( '?s={search_term_string}&post_type=product' ), + 'query-input' => 'required name=search_term_string', + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_website', $markup ) ); + } + + /** + * Generates Order structured data. + * + * Hooked into `woocommerce_email_order_details` action hook. + * + * @param WP_Order $order Order data. + * @param bool $sent_to_admin Send to admin (default: false). + * @param bool $plain_text Plain text email (default: false). + */ + public function generate_order_data( $order, $sent_to_admin = false, $plain_text = false ) { + if ( $plain_text || ! is_a( $order, 'WC_Order' ) ) { + return; + } + + $shop_name = get_bloginfo( 'name' ); + $shop_url = home_url(); + $order_url = $sent_to_admin ? $order->get_edit_order_url() : $order->get_view_order_url(); + $order_statuses = array( + 'pending' => 'https://schema.org/OrderPaymentDue', + 'processing' => 'https://schema.org/OrderProcessing', + 'on-hold' => 'https://schema.org/OrderProblem', + 'completed' => 'https://schema.org/OrderDelivered', + 'cancelled' => 'https://schema.org/OrderCancelled', + 'refunded' => 'https://schema.org/OrderReturned', + 'failed' => 'https://schema.org/OrderProblem', + ); + + $markup_offers = array(); + foreach ( $order->get_items() as $item ) { + if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { + continue; + } + + $product = $item->get_product(); + $product_exists = is_object( $product ); + $is_visible = $product_exists && $product->is_visible(); + + $markup_offers[] = array( + '@type' => 'Offer', + 'price' => $order->get_line_subtotal( $item ), + 'priceCurrency' => $order->get_currency(), + 'priceSpecification' => array( + 'price' => $order->get_line_subtotal( $item ), + 'priceCurrency' => $order->get_currency(), + 'eligibleQuantity' => array( + '@type' => 'QuantitativeValue', + 'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ), + ), + ), + 'itemOffered' => array( + '@type' => 'Product', + 'name' => wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ) ), + 'sku' => $product_exists ? $product->get_sku() : '', + 'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '', + 'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(), + ), + 'seller' => array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ), + ); + } + + $markup = array(); + $markup['@type'] = 'Order'; + $markup['url'] = $order_url; + $markup['orderStatus'] = isset( $order_statuses[ $order->get_status() ] ) ? $order_statuses[ $order->get_status() ] : ''; + $markup['orderNumber'] = $order->get_order_number(); + $markup['orderDate'] = $order->get_date_created()->format( 'c' ); + $markup['acceptedOffer'] = $markup_offers; + $markup['discount'] = $order->get_total_discount(); + $markup['discountCurrency'] = $order->get_currency(); + $markup['price'] = $order->get_total(); + $markup['priceCurrency'] = $order->get_currency(); + $markup['priceSpecification'] = array( + 'price' => $order->get_total(), + 'priceCurrency' => $order->get_currency(), + 'valueAddedTaxIncluded' => 'true', + ); + $markup['billingAddress'] = array( + '@type' => 'PostalAddress', + 'name' => $order->get_formatted_billing_full_name(), + 'streetAddress' => $order->get_billing_address_1(), + 'postalCode' => $order->get_billing_postcode(), + 'addressLocality' => $order->get_billing_city(), + 'addressRegion' => $order->get_billing_state(), + 'addressCountry' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'telephone' => $order->get_billing_phone(), + ); + $markup['customer'] = array( + '@type' => 'Person', + 'name' => $order->get_formatted_billing_full_name(), + ); + $markup['merchant'] = array( + '@type' => 'Organization', + 'name' => $shop_name, + 'url' => $shop_url, + ); + $markup['potentialAction'] = array( + '@type' => 'ViewAction', + 'name' => 'View Order', + 'url' => $order_url, + 'target' => $order_url, + ); + + $this->set_data( apply_filters( 'woocommerce_structured_data_order', $markup, $sent_to_admin, $order ), true ); + } +} diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php new file mode 100644 index 0000000..a59f60c --- /dev/null +++ b/includes/class-wc-tax.php @@ -0,0 +1,1248 @@ + $rate ) { + $taxes[ $key ] = 0; + + if ( 'yes' === $rate['compound'] ) { + $compound_rates[ $key ] = $rate['rate']; + } else { + $regular_rates[ $key ] = $rate['rate']; + } + } + + $compound_rates = array_reverse( $compound_rates, true ); // Working backwards. + + $non_compound_price = $price; + + foreach ( $compound_rates as $key => $compound_rate ) { + $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $non_compound_price - ( $non_compound_price / ( 1 + ( $compound_rate / 100 ) ) ), $key, $rates[ $key ], $price ); + $taxes[ $key ] += $tax_amount; + $non_compound_price = $non_compound_price - $tax_amount; + } + + // Regular taxes. + $regular_tax_rate = 1 + ( array_sum( $regular_rates ) / 100 ); + + foreach ( $regular_rates as $key => $regular_rate ) { + $the_rate = ( $regular_rate / 100 ) / $regular_tax_rate; + $net_price = $price - ( $the_rate * $non_compound_price ); + $tax_amount = apply_filters( 'woocommerce_price_inc_tax_amount', $price - $net_price, $key, $rates[ $key ], $price ); + $taxes[ $key ] += $tax_amount; + } + + /** + * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding + * as in the cart calculation class which, depending on settings, will round to 2DP when calculating + * final totals. Also unlike that class, this rounds .5 up for all cases. + */ + $taxes = array_map( array( __CLASS__, 'round' ), $taxes ); + + return $taxes; + } + + /** + * Calc tax from exclusive price. + * + * @param float $price Price to calculate tax for. + * @param array $rates Array of tax rates. + * @return array + */ + public static function calc_exclusive_tax( $price, $rates ) { + $taxes = array(); + + if ( ! empty( $rates ) ) { + foreach ( $rates as $key => $rate ) { + if ( 'yes' === $rate['compound'] ) { + continue; + } + + $tax_amount = $price * ( $rate['rate'] / 100 ); + $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price ); // ADVANCED: Allow third parties to modify this rate. + + if ( ! isset( $taxes[ $key ] ) ) { + $taxes[ $key ] = $tax_amount; + } else { + $taxes[ $key ] += $tax_amount; + } + } + + $pre_compound_total = array_sum( $taxes ); + + // Compound taxes. + foreach ( $rates as $key => $rate ) { + if ( 'no' === $rate['compound'] ) { + continue; + } + $the_price_inc_tax = $price + ( $pre_compound_total ); + $tax_amount = $the_price_inc_tax * ( $rate['rate'] / 100 ); + $tax_amount = apply_filters( 'woocommerce_price_ex_tax_amount', $tax_amount, $key, $rate, $price, $the_price_inc_tax, $pre_compound_total ); // ADVANCED: Allow third parties to modify this rate. + + if ( ! isset( $taxes[ $key ] ) ) { + $taxes[ $key ] = $tax_amount; + } else { + $taxes[ $key ] += $tax_amount; + } + + $pre_compound_total = array_sum( $taxes ); + } + } + + /** + * Round all taxes to precision (4DP) before passing them back. Note, this is not the same rounding + * as in the cart calculation class which, depending on settings, will round to 2DP when calculating + * final totals. Also unlike that class, this rounds .5 up for all cases. + */ + $taxes = array_map( array( __CLASS__, 'round' ), $taxes ); + + return $taxes; + } + + /** + * Searches for all matching country/state/postcode tax rates. + * + * @param array $args Args that determine the rate to find. + * @return array + */ + public static function find_rates( $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'country' => '', + 'state' => '', + 'city' => '', + 'postcode' => '', + 'tax_class' => '', + ) + ); + + $country = $args['country']; + $state = $args['state']; + $city = $args['city']; + $postcode = wc_normalize_postcode( wc_clean( $args['postcode'] ) ); + $tax_class = $args['tax_class']; + + if ( ! $country ) { + return array(); + } + + $cache_key = WC_Cache_Helper::get_cache_prefix( 'taxes' ) . 'wc_tax_rates_' . md5( sprintf( '%s+%s+%s+%s+%s', $country, $state, $city, $postcode, $tax_class ) ); + $matched_tax_rates = wp_cache_get( $cache_key, 'taxes' ); + + if ( false === $matched_tax_rates ) { + $matched_tax_rates = self::get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ); + wp_cache_set( $cache_key, $matched_tax_rates, 'taxes' ); + } + + return apply_filters( 'woocommerce_find_rates', $matched_tax_rates, $args ); + } + + /** + * Searches for all matching country/state/postcode tax rates. + * + * @param array $args Args that determine the rate to find. + * @return array + */ + public static function find_shipping_rates( $args = array() ) { + $rates = self::find_rates( $args ); + $shipping_rates = array(); + + if ( is_array( $rates ) ) { + foreach ( $rates as $key => $rate ) { + if ( 'yes' === $rate['shipping'] ) { + $shipping_rates[ $key ] = $rate; + } + } + } + + return $shipping_rates; + } + + /** + * Does the sort comparison. Compares (in this order): + * - Priority + * - Country + * - State + * - Number of postcodes + * - Number of cities + * - ID + * + * @param object $rate1 First rate to compare. + * @param object $rate2 Second rate to compare. + * @return int + */ + private static function sort_rates_callback( $rate1, $rate2 ) { + if ( $rate1->tax_rate_priority !== $rate2->tax_rate_priority ) { + return $rate1->tax_rate_priority < $rate2->tax_rate_priority ? -1 : 1; // ASC. + } + + if ( $rate1->tax_rate_country !== $rate2->tax_rate_country ) { + if ( '' === $rate1->tax_rate_country ) { + return 1; + } + if ( '' === $rate2->tax_rate_country ) { + return -1; + } + return strcmp( $rate1->tax_rate_country, $rate2->tax_rate_country ) > 0 ? 1 : -1; + } + + if ( $rate1->tax_rate_state !== $rate2->tax_rate_state ) { + if ( '' === $rate1->tax_rate_state ) { + return 1; + } + if ( '' === $rate2->tax_rate_state ) { + return -1; + } + return strcmp( $rate1->tax_rate_state, $rate2->tax_rate_state ) > 0 ? 1 : -1; + } + + if ( isset( $rate1->postcode_count, $rate2->postcode_count ) && $rate1->postcode_count !== $rate2->postcode_count ) { + return $rate1->postcode_count < $rate2->postcode_count ? 1 : -1; + } + + if ( isset( $rate1->city_count, $rate2->city_count ) && $rate1->city_count !== $rate2->city_count ) { + return $rate1->city_count < $rate2->city_count ? 1 : -1; + } + + return $rate1->tax_rate_id < $rate2->tax_rate_id ? -1 : 1; + } + + /** + * Logical sort order for tax rates based on the following in order of priority. + * + * @param array $rates Rates to be sorted. + * @return array + */ + private static function sort_rates( $rates ) { + uasort( $rates, __CLASS__ . '::sort_rates_callback' ); + $i = 0; + foreach ( $rates as $key => $rate ) { + $rates[ $key ]->tax_rate_order = $i++; + } + return $rates; + } + + /** + * Loop through a set of tax rates and get the matching rates (1 per priority). + * + * @param string $country Country code to match against. + * @param string $state State code to match against. + * @param string $postcode Postcode to match against. + * @param string $city City to match against. + * @param string $tax_class Tax class to match against. + * @return array + */ + private static function get_matched_tax_rates( $country, $state, $postcode, $city, $tax_class ) { + global $wpdb; + + // Query criteria - these will be ANDed. + $criteria = array(); + $criteria[] = $wpdb->prepare( "tax_rate_country IN ( %s, '' )", strtoupper( $country ) ); + $criteria[] = $wpdb->prepare( "tax_rate_state IN ( %s, '' )", strtoupper( $state ) ); + $criteria[] = $wpdb->prepare( 'tax_rate_class = %s', sanitize_title( $tax_class ) ); + + // Pre-query postcode ranges for PHP based matching. + $postcode_search = wc_get_wildcard_postcodes( $postcode, $country ); + $postcode_ranges = $wpdb->get_results( "SELECT tax_rate_id, location_code FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE location_type = 'postcode' AND location_code LIKE '%...%';" ); + + if ( $postcode_ranges ) { + $matches = wc_postcode_location_matcher( $postcode, $postcode_ranges, 'tax_rate_id', 'location_code', $country ); + if ( ! empty( $matches ) ) { + foreach ( $matches as $matched_postcodes ) { + $postcode_search = array_merge( $postcode_search, $matched_postcodes ); + } + } + } + + $postcode_search = array_unique( $postcode_search ); + + /** + * Location matching criteria - ORed + * Needs to match: + * - rates with no postcodes and cities + * - rates with a matching postcode and city + * - rates with matching postcode, no city + * - rates with matching city, no postcode + */ + $locations_criteria = array(); + $locations_criteria[] = 'locations.location_type IS NULL'; + $locations_criteria[] = " + locations.location_type = 'postcode' AND locations.location_code IN ('" . implode( "','", array_map( 'esc_sql', $postcode_search ) ) . "') + AND ( + ( locations2.location_type = 'city' AND locations2.location_code = '" . esc_sql( strtoupper( $city ) ) . "' ) + OR NOT EXISTS ( + SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub + WHERE sub.location_type = 'city' + AND sub.tax_rate_id = tax_rates.tax_rate_id + ) + ) + "; + $locations_criteria[] = " + locations.location_type = 'city' AND locations.location_code = '" . esc_sql( strtoupper( $city ) ) . "' + AND NOT EXISTS ( + SELECT sub.tax_rate_id FROM {$wpdb->prefix}woocommerce_tax_rate_locations as sub + WHERE sub.location_type = 'postcode' + AND sub.tax_rate_id = tax_rates.tax_rate_id + ) + "; + + $criteria[] = '( ( ' . implode( ' ) OR ( ', $locations_criteria ) . ' ) )'; + + $criteria_string = implode( ' AND ', $criteria ); + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $found_rates = $wpdb->get_results( + " + SELECT tax_rates.*, COUNT( locations.location_id ) as postcode_count, COUNT( locations2.location_id ) as city_count + FROM {$wpdb->prefix}woocommerce_tax_rates as tax_rates + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations ON tax_rates.tax_rate_id = locations.tax_rate_id + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_tax_rate_locations as locations2 ON tax_rates.tax_rate_id = locations2.tax_rate_id + WHERE 1=1 AND {$criteria_string} + GROUP BY tax_rates.tax_rate_id + ORDER BY tax_rates.tax_rate_priority + " + ); + // phpcs:enable + + $found_rates = self::sort_rates( $found_rates ); + $matched_tax_rates = array(); + $found_priority = array(); + + foreach ( $found_rates as $found_rate ) { + if ( in_array( $found_rate->tax_rate_priority, $found_priority, true ) ) { + continue; + } + + $matched_tax_rates[ $found_rate->tax_rate_id ] = array( + 'rate' => (float) $found_rate->tax_rate, + 'label' => $found_rate->tax_rate_name, + 'shipping' => $found_rate->tax_rate_shipping ? 'yes' : 'no', + 'compound' => $found_rate->tax_rate_compound ? 'yes' : 'no', + ); + + $found_priority[] = $found_rate->tax_rate_priority; + } + + return apply_filters( 'woocommerce_matched_tax_rates', $matched_tax_rates, $country, $state, $postcode, $city, $tax_class ); + } + + /** + * Get the customer tax location based on their status and the current page. + * + * Used by get_rates(), get_shipping_rates(). + * + * @param string $tax_class string Optional, passed to the filter for advanced tax setups. + * @param object $customer Override the customer object to get their location. + * @return array + */ + public static function get_tax_location( $tax_class = '', $customer = null ) { + $location = array(); + + if ( is_null( $customer ) && WC()->customer ) { + $customer = WC()->customer; + } + + if ( ! empty( $customer ) ) { + $location = $customer->get_taxable_address(); + } elseif ( wc_prices_include_tax() || 'base' === get_option( 'woocommerce_default_customer_address' ) || 'base' === get_option( 'woocommerce_tax_based_on' ) ) { + $location = array( + WC()->countries->get_base_country(), + WC()->countries->get_base_state(), + WC()->countries->get_base_postcode(), + WC()->countries->get_base_city(), + ); + } + + return apply_filters( 'woocommerce_get_tax_location', $location, $tax_class, $customer ); + } + + /** + * Get's an array of matching rates for a tax class. + * + * @param string $tax_class Tax class to get rates for. + * @param object $customer Override the customer object to get their location. + * @return array + */ + public static function get_rates( $tax_class = '', $customer = null ) { + $tax_class = sanitize_title( $tax_class ); + $location = self::get_tax_location( $tax_class, $customer ); + return self::get_rates_from_location( $tax_class, $location, $customer ); + } + + /** + * Get's an arrau of matching rates from location and tax class. $customer parameter is used to preserve backward compatibility for filter. + * + * @param string $tax_class Tax class to get rates for. + * @param array $location Location to compute rates for. Should be in form: array( country, state, postcode, city). + * @param object $customer Only used to maintain backward compatibility for filter `woocommerce-matched_rates`. + * + * @return mixed|void Tax rates. + */ + public static function get_rates_from_location( $tax_class, $location, $customer = null ) { + $tax_class = sanitize_title( $tax_class ); + $matched_tax_rates = array(); + + if ( count( $location ) === 4 ) { + list( $country, $state, $postcode, $city ) = $location; + + $matched_tax_rates = self::find_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $tax_class, + ) + ); + } + + return apply_filters( 'woocommerce_matched_rates', $matched_tax_rates, $tax_class, $customer ); + } + + /** + * Get's an array of matching rates for the shop's base country. + * + * @param string $tax_class Tax Class. + * @return array + */ + public static function get_base_tax_rates( $tax_class = '' ) { + return apply_filters( + 'woocommerce_base_tax_rates', + self::find_rates( + array( + 'country' => WC()->countries->get_base_country(), + 'state' => WC()->countries->get_base_state(), + 'postcode' => WC()->countries->get_base_postcode(), + 'city' => WC()->countries->get_base_city(), + 'tax_class' => $tax_class, + ) + ), + $tax_class + ); + } + + /** + * Alias for get_base_tax_rates(). + * + * @deprecated 2.3 + * @param string $tax_class Tax Class. + * @return array + */ + public static function get_shop_base_rate( $tax_class = '' ) { + return self::get_base_tax_rates( $tax_class ); + } + + /** + * Gets an array of matching shipping tax rates for a given class. + * + * @param string $tax_class Tax class to get rates for. + * @param object $customer Override the customer object to get their location. + * @return mixed + */ + public static function get_shipping_tax_rates( $tax_class = null, $customer = null ) { + // See if we have an explicitly set shipping tax class. + $shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); + + if ( 'inherit' !== $shipping_tax_class ) { + $tax_class = $shipping_tax_class; + } + + $location = self::get_tax_location( $tax_class, $customer ); + $matched_tax_rates = array(); + + if ( 4 === count( $location ) ) { + list( $country, $state, $postcode, $city ) = $location; + + if ( ! is_null( $tax_class ) ) { + // This will be per item shipping. + $matched_tax_rates = self::find_shipping_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $tax_class, + ) + ); + + } elseif ( WC()->cart->get_cart() ) { + + // This will be per order shipping - loop through the order and find the highest tax class rate. + $cart_tax_classes = WC()->cart->get_cart_item_tax_classes_for_shipping(); + + // No tax classes = no taxable items. + if ( empty( $cart_tax_classes ) ) { + return array(); + } + + // If multiple classes are found, use the first one found unless a standard rate item is found. This will be the first listed in the 'additional tax class' section. + if ( count( $cart_tax_classes ) > 1 && ! in_array( '', $cart_tax_classes, true ) ) { + $tax_classes = self::get_tax_class_slugs(); + + foreach ( $tax_classes as $tax_class ) { + if ( in_array( $tax_class, $cart_tax_classes, true ) ) { + $matched_tax_rates = self::find_shipping_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $tax_class, + ) + ); + break; + } + } + } elseif ( 1 === count( $cart_tax_classes ) ) { + // If a single tax class is found, use it. + $matched_tax_rates = self::find_shipping_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + 'tax_class' => $cart_tax_classes[0], + ) + ); + } + } + + // Get standard rate if no taxes were found. + if ( ! count( $matched_tax_rates ) ) { + $matched_tax_rates = self::find_shipping_rates( + array( + 'country' => $country, + 'state' => $state, + 'postcode' => $postcode, + 'city' => $city, + ) + ); + } + } + + return $matched_tax_rates; + } + + /** + * Return true/false depending on if a rate is a compound rate. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return bool + */ + public static function is_compound( $key_or_rate ) { + global $wpdb; + + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $compound = $key_or_rate->tax_rate_compound; + } else { + $key = $key_or_rate; + $compound = (bool) $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_compound FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + return (bool) apply_filters( 'woocommerce_rate_compound', $compound, $key ); + } + + /** + * Return a given rates label. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return string + */ + public static function get_rate_label( $key_or_rate ) { + global $wpdb; + + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $rate_name = $key_or_rate->tax_rate_name; + } else { + $key = $key_or_rate; + $rate_name = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_name FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + if ( ! $rate_name ) { + $rate_name = WC()->countries->tax_or_vat(); + } + + return apply_filters( 'woocommerce_rate_label', $rate_name, $key ); + } + + /** + * Return a given rates percent. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return string + */ + public static function get_rate_percent( $key_or_rate ) { + $rate_percent_value = self::get_rate_percent_value( $key_or_rate ); + $tax_rate_id = is_object( $key_or_rate ) ? $key_or_rate->tax_rate_id : $key_or_rate; + return apply_filters( 'woocommerce_rate_percent', $rate_percent_value . '%', $tax_rate_id ); + } + + /** + * Return a given rates percent. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return float + */ + public static function get_rate_percent_value( $key_or_rate ) { + global $wpdb; + + if ( is_object( $key_or_rate ) ) { + $tax_rate = $key_or_rate->tax_rate; + } else { + $key = $key_or_rate; + $tax_rate = $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + return floatval( $tax_rate ); + } + + + /** + * Get a rates code. Code is made up of COUNTRY-STATE-NAME-Priority. E.g GB-VAT-1, US-AL-TAX-1. + * + * @param mixed $key_or_rate Tax rate ID, or the db row itself in object format. + * @return string + */ + public static function get_rate_code( $key_or_rate ) { + global $wpdb; + + if ( is_object( $key_or_rate ) ) { + $key = $key_or_rate->tax_rate_id; + $rate = $key_or_rate; + } else { + $key = $key_or_rate; + $rate = $wpdb->get_row( $wpdb->prepare( "SELECT tax_rate_country, tax_rate_state, tax_rate_name, tax_rate_priority FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %s", $key ) ); + } + + $code_string = ''; + + if ( null !== $rate ) { + $code = array(); + $code[] = $rate->tax_rate_country; + $code[] = $rate->tax_rate_state; + $code[] = $rate->tax_rate_name ? $rate->tax_rate_name : 'TAX'; + $code[] = absint( $rate->tax_rate_priority ); + $code_string = strtoupper( implode( '-', array_filter( $code ) ) ); + } + + return apply_filters( 'woocommerce_rate_code', $code_string, $key ); + } + + /** + * Sums a set of taxes to form a single total. Values are pre-rounded to precision from 3.6.0. + * + * @param array $taxes Array of taxes. + * @return float + */ + public static function get_tax_total( $taxes ) { + return array_sum( $taxes ); + } + + /** + * Gets all tax rate classes from the database. + * + * @since 3.7.0 + * @return array Array of tax class objects consisting of tax_rate_class_id, name, and slug. + */ + public static function get_tax_rate_classes() { + global $wpdb; + + $cache_key = 'tax-rate-classes'; + $tax_rate_classes = wp_cache_get( $cache_key, 'taxes' ); + + if ( ! is_array( $tax_rate_classes ) ) { + $tax_rate_classes = $wpdb->get_results( + " + SELECT * FROM {$wpdb->wc_tax_rate_classes} ORDER BY name; + " + ); + wp_cache_set( $cache_key, $tax_rate_classes, 'taxes' ); + } + + return $tax_rate_classes; + } + + /** + * Get store tax class names. + * + * @return array Array of class names ("Reduced rate", "Zero rate", etc). + */ + public static function get_tax_classes() { + return wp_list_pluck( self::get_tax_rate_classes(), 'name' ); + } + + /** + * Get store tax classes as slugs. + * + * @since 3.0.0 + * @return array Array of class slugs ("reduced-rate", "zero-rate", etc). + */ + public static function get_tax_class_slugs() { + return wp_list_pluck( self::get_tax_rate_classes(), 'slug' ); + } + + /** + * Create a new tax class. + * + * @since 3.7.0 + * @param string $name Name of the tax class to add. + * @param string $slug (optional) Slug of the tax class to add. Defaults to sanitized name. + * @return WP_Error|array Returns name and slug (array) if the tax class is created, or WP_Error if something went wrong. + */ + public static function create_tax_class( $name, $slug = '' ) { + global $wpdb; + + if ( empty( $name ) ) { + return new WP_Error( 'tax_class_invalid_name', __( 'Tax class requires a valid name', 'woocommerce' ) ); + } + + $existing = self::get_tax_classes(); + $existing_slugs = self::get_tax_class_slugs(); + $name = wc_clean( $name ); + + if ( in_array( $name, $existing, true ) ) { + return new WP_Error( 'tax_class_exists', __( 'Tax class already exists', 'woocommerce' ) ); + } + + if ( ! $slug ) { + $slug = sanitize_title( $name ); + } + + // Stop if there's no slug. + if ( ! $slug ) { + return new WP_Error( 'tax_class_slug_invalid', __( 'Tax class slug is invalid', 'woocommerce' ) ); + } + + if ( in_array( $slug, $existing_slugs, true ) ) { + return new WP_Error( 'tax_class_slug_exists', __( 'Tax class slug already exists', 'woocommerce' ) ); + } + + $insert = $wpdb->insert( + $wpdb->wc_tax_rate_classes, + array( + 'name' => $name, + 'slug' => $slug, + ) + ); + + if ( is_wp_error( $insert ) ) { + return new WP_Error( 'tax_class_insert_error', $insert->get_error_message() ); + } + + wp_cache_delete( 'tax-rate-classes', 'taxes' ); + + return array( + 'name' => $name, + 'slug' => $slug, + ); + } + + /** + * Get an existing tax class. + * + * @since 3.7.0 + * @param string $field Field to get by. Valid values are id, name, or slug. + * @param string|int $item Item to get. + * @return array|bool Returns the tax class as an array. False if not found. + */ + public static function get_tax_class_by( $field, $item ) { + if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) { + return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) ); + } + + if ( 'id' === $field ) { + $field = 'tax_rate_class_id'; + } + + $matches = wp_list_filter( + self::get_tax_rate_classes(), + array( + $field => $item, + ) + ); + + if ( ! $matches ) { + return false; + } + + $tax_class = current( $matches ); + + return array( + 'name' => $tax_class->name, + 'slug' => $tax_class->slug, + ); + } + + /** + * Delete an existing tax class. + * + * @since 3.7.0 + * @param string $field Field to delete by. Valid values are id, name, or slug. + * @param string|int $item Item to delete. + * @return WP_Error|bool Returns true if deleted successfully, false if nothing was deleted, or WP_Error if there is an invalid request. + */ + public static function delete_tax_class_by( $field, $item ) { + global $wpdb; + + if ( ! in_array( $field, array( 'id', 'name', 'slug' ), true ) ) { + return new WP_Error( 'invalid_field', __( 'Invalid field', 'woocommerce' ) ); + } + + $tax_class = self::get_tax_class_by( $field, $item ); + + if ( ! $tax_class ) { + return new WP_Error( 'invalid_tax_class', __( 'Invalid tax class', 'woocommerce' ) ); + } + + if ( 'id' === $field ) { + $field = 'tax_rate_class_id'; + } + + $delete = $wpdb->delete( + $wpdb->wc_tax_rate_classes, + array( + $field => $item, + ) + ); + + if ( $delete ) { + // Delete associated tax rates. + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_class = %s;", $tax_class['slug'] ) ); + $wpdb->query( "DELETE locations FROM {$wpdb->prefix}woocommerce_tax_rate_locations locations LEFT JOIN {$wpdb->prefix}woocommerce_tax_rates rates ON rates.tax_rate_id = locations.tax_rate_id WHERE rates.tax_rate_id IS NULL;" ); + } + + wp_cache_delete( 'tax-rate-classes', 'taxes' ); + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + + return (bool) $delete; + } + + /** + * Format the city. + * + * @param string $city Value to format. + * @return string + */ + private static function format_tax_rate_city( $city ) { + return strtoupper( trim( $city ) ); + } + + /** + * Format the state. + * + * @param string $state Value to format. + * @return string + */ + private static function format_tax_rate_state( $state ) { + $state = strtoupper( $state ); + return ( '*' === $state ) ? '' : $state; + } + + /** + * Format the country. + * + * @param string $country Value to format. + * @return string + */ + private static function format_tax_rate_country( $country ) { + $country = strtoupper( $country ); + return ( '*' === $country ) ? '' : $country; + } + + /** + * Format the tax rate name. + * + * @param string $name Value to format. + * @return string + */ + private static function format_tax_rate_name( $name ) { + return $name ? $name : __( 'Tax', 'woocommerce' ); + } + + /** + * Format the rate. + * + * @param float $rate Value to format. + * @return string + */ + private static function format_tax_rate( $rate ) { + return number_format( (float) $rate, 4, '.', '' ); + } + + /** + * Format the priority. + * + * @param string $priority Value to format. + * @return int + */ + private static function format_tax_rate_priority( $priority ) { + return absint( $priority ); + } + + /** + * Format the class. + * + * @param string $class Value to format. + * @return string + */ + public static function format_tax_rate_class( $class ) { + $class = sanitize_title( $class ); + $classes = self::get_tax_class_slugs(); + if ( ! in_array( $class, $classes, true ) ) { + $class = ''; + } + return ( 'standard' === $class ) ? '' : $class; + } + + /** + * Prepare and format tax rate for DB insertion. + * + * @param array $tax_rate Tax rate to format. + * @return array + */ + private static function prepare_tax_rate( $tax_rate ) { + foreach ( $tax_rate as $key => $value ) { + if ( method_exists( __CLASS__, 'format_' . $key ) ) { + if ( 'tax_rate_state' === $key ) { + $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), sanitize_key( $value ) ); + } else { + $tax_rate[ $key ] = call_user_func( array( __CLASS__, 'format_' . $key ), $value ); + } + } + } + return $tax_rate; + } + + /** + * Insert a new tax rate. + * + * Internal use only. + * + * @since 2.3.0 + * + * @param array $tax_rate Tax rate to insert. + * @return int tax rate id + */ + public static function _insert_tax_rate( $tax_rate ) { + global $wpdb; + + $wpdb->insert( $wpdb->prefix . 'woocommerce_tax_rates', self::prepare_tax_rate( $tax_rate ) ); + + $tax_rate_id = $wpdb->insert_id; + + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + + do_action( 'woocommerce_tax_rate_added', $tax_rate_id, $tax_rate ); + + return $tax_rate_id; + } + + /** + * Get tax rate. + * + * Internal use only. + * + * @since 2.5.0 + * + * @param int $tax_rate_id Tax rate ID. + * @param string $output_type Type of output. + * @return array|object + */ + public static function _get_tax_rate( $tax_rate_id, $output_type = ARRAY_A ) { + global $wpdb; + + return $wpdb->get_row( + $wpdb->prepare( + " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE tax_rate_id = %d + ", + $tax_rate_id + ), + $output_type + ); + } + + /** + * Update a tax rate. + * + * Internal use only. + * + * @since 2.3.0 + * + * @param int $tax_rate_id Tax rate to update. + * @param array $tax_rate Tax rate values. + */ + public static function _update_tax_rate( $tax_rate_id, $tax_rate ) { + global $wpdb; + + $tax_rate_id = absint( $tax_rate_id ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_tax_rates', + self::prepare_tax_rate( $tax_rate ), + array( + 'tax_rate_id' => $tax_rate_id, + ) + ); + + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + + do_action( 'woocommerce_tax_rate_updated', $tax_rate_id, $tax_rate ); + } + + /** + * Delete a tax rate from the database. + * + * Internal use only. + * + * @since 2.3.0 + * @param int $tax_rate_id Tax rate to delete. + */ + public static function _delete_tax_rate( $tax_rate_id ) { + global $wpdb; + + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d;", $tax_rate_id ) ); + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d;", $tax_rate_id ) ); + + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + + do_action( 'woocommerce_tax_rate_deleted', $tax_rate_id ); + } + + /** + * Update postcodes for a tax rate in the DB. + * + * Internal use only. + * + * @since 2.3.0 + * + * @param int $tax_rate_id Tax rate to update. + * @param string $postcodes String of postcodes separated by ; characters. + */ + public static function _update_tax_rate_postcodes( $tax_rate_id, $postcodes ) { + if ( ! is_array( $postcodes ) ) { + $postcodes = explode( ';', $postcodes ); + } + // No normalization - postcodes are matched against both normal and formatted versions to support wildcards. + foreach ( $postcodes as $key => $postcode ) { + $postcodes[ $key ] = strtoupper( trim( str_replace( chr( 226 ) . chr( 128 ) . chr( 166 ), '...', $postcode ) ) ); + } + self::update_tax_rate_locations( $tax_rate_id, array_diff( array_filter( $postcodes ), array( '*' ) ), 'postcode' ); + } + + /** + * Update cities for a tax rate in the DB. + * + * Internal use only. + * + * @since 2.3.0 + * + * @param int $tax_rate_id Tax rate to update. + * @param string $cities Cities to set. + */ + public static function _update_tax_rate_cities( $tax_rate_id, $cities ) { + if ( ! is_array( $cities ) ) { + $cities = explode( ';', $cities ); + } + $cities = array_filter( array_diff( array_map( array( __CLASS__, 'format_tax_rate_city' ), $cities ), array( '*' ) ) ); + + self::update_tax_rate_locations( $tax_rate_id, $cities, 'city' ); + } + + /** + * Updates locations (postcode and city). + * + * Internal use only. + * + * @since 2.3.0 + * + * @param int $tax_rate_id Tax rate ID to update. + * @param array $values Values to set. + * @param string $type Location type. + */ + private static function update_tax_rate_locations( $tax_rate_id, $values, $type ) { + global $wpdb; + + $tax_rate_id = absint( $tax_rate_id ); + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_tax_rate_locations WHERE tax_rate_id = %d AND location_type = %s;", + $tax_rate_id, + $type + ) + ); + + if ( count( $values ) > 0 ) { + $sql = "( '" . implode( "', $tax_rate_id, '" . esc_sql( $type ) . "' ),( '", array_map( 'esc_sql', $values ) ) . "', $tax_rate_id, '" . esc_sql( $type ) . "' )"; + + $wpdb->query( "INSERT INTO {$wpdb->prefix}woocommerce_tax_rate_locations ( location_code, tax_rate_id, location_type ) VALUES $sql;" ); // @codingStandardsIgnoreLine. + } + + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + } + + /** + * Used by admin settings page. + * + * @param string $tax_class Tax class slug. + * + * @return array|null|object + */ + public static function get_rates_for_tax_class( $tax_class ) { + global $wpdb; + + $tax_class = self::format_tax_rate_class( $tax_class ); + + // Get all the rates and locations. Snagging all at once should significantly cut down on the number of queries. + $rates = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rates` WHERE `tax_rate_class` = %s;", $tax_class ) ); + $locations = $wpdb->get_results( "SELECT * FROM `{$wpdb->prefix}woocommerce_tax_rate_locations`" ); + + if ( ! empty( $rates ) ) { + // Set the rates keys equal to their ids. + $rates = array_combine( wp_list_pluck( $rates, 'tax_rate_id' ), $rates ); + } + + // Drop the locations into the rates array. + foreach ( $locations as $location ) { + // Don't set them for unexistent rates. + if ( ! isset( $rates[ $location->tax_rate_id ] ) ) { + continue; + } + // If the rate exists, initialize the array before appending to it. + if ( ! isset( $rates[ $location->tax_rate_id ]->{$location->location_type} ) ) { + $rates[ $location->tax_rate_id ]->{$location->location_type} = array(); + } + $rates[ $location->tax_rate_id ]->{$location->location_type}[] = $location->location_code; + } + + foreach ( $rates as $rate_id => $rate ) { + $rates[ $rate_id ]->postcode_count = isset( $rates[ $rate_id ]->postcode ) ? count( $rates[ $rate_id ]->postcode ) : 0; + $rates[ $rate_id ]->city_count = isset( $rates[ $rate_id ]->city ) ? count( $rates[ $rate_id ]->city ) : 0; + } + + $rates = self::sort_rates( $rates ); + + return $rates; + } +} +WC_Tax::init(); diff --git a/includes/class-wc-template-loader.php b/includes/class-wc-template-loader.php new file mode 100644 index 0000000..b247a1b --- /dev/null +++ b/includes/class-wc-template-loader.php @@ -0,0 +1,616 @@ +plugin_path() . '/templates/' . $cs_template; + } else { + $template = WC()->plugin_path() . '/templates/' . $default_file; + } + } + } + + return $template; + } + + /** + * Checks whether a block template with that name exists. + * + * @since 5.5.0 + * @param string $template_name Template to check. + * @return boolean + */ + private static function has_block_template( $template_name ) { + if ( ! $template_name ) { + return false; + } + + return is_readable( + get_stylesheet_directory() . '/block-templates/' . $template_name . '.html' + ); + } + + /** + * Get the default filename for a template except if a block template with + * the same name exists. + * + * @since 3.0.0 + * @since 5.5.0 If a block template with the same name exists, return an + * empty string. + * @return string + */ + private static function get_template_loader_default_file() { + if ( + is_singular( 'product' ) && + ! self::has_block_template( 'single-product' ) + ) { + $default_file = 'single-product.php'; + } elseif ( is_product_taxonomy() ) { + $object = get_queried_object(); + + if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { + if ( self::has_block_template( 'taxonomy-' . $object->taxonomy ) ) { + $default_file = ''; + } else { + $default_file = 'taxonomy-' . $object->taxonomy . '.php'; + } + } elseif ( ! self::has_block_template( 'archive-product' ) ) { + $default_file = 'archive-product.php'; + } + } elseif ( + ( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) && + ! self::has_block_template( 'archive-product' ) + ) { + $default_file = self::$theme_support ? 'archive-product.php' : ''; + } else { + $default_file = ''; + } + return $default_file; + } + + /** + * Get an array of filenames to search for a given template. + * + * @since 3.0.0 + * @param string $default_file The default file name. + * @return string[] + */ + private static function get_template_loader_files( $default_file ) { + $templates = apply_filters( 'woocommerce_template_loader_files', array(), $default_file ); + $templates[] = 'woocommerce.php'; + + if ( is_page_template() ) { + $page_template = get_page_template_slug(); + + if ( $page_template ) { + $validated_file = validate_file( $page_template ); + if ( 0 === $validated_file ) { + $templates[] = $page_template; + } else { + error_log( "WooCommerce: Unable to validate template path: \"$page_template\". Error Code: $validated_file." ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + } + } + } + + if ( is_singular( 'product' ) ) { + $object = get_queried_object(); + $name_decoded = urldecode( $object->post_name ); + if ( $name_decoded !== $object->post_name ) { + $templates[] = "single-product-{$name_decoded}.php"; + } + $templates[] = "single-product-{$object->post_name}.php"; + } + + if ( is_product_taxonomy() ) { + $object = get_queried_object(); + + $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; + $templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; + $templates[] = 'taxonomy-' . $object->taxonomy . '.php'; + $templates[] = WC()->template_path() . 'taxonomy-' . $object->taxonomy . '.php'; + + if ( is_tax( 'product_cat' ) || is_tax( 'product_tag' ) ) { + $cs_taxonomy = str_replace( '_', '-', $object->taxonomy ); + $cs_default = str_replace( '_', '-', $default_file ); + $templates[] = 'taxonomy-' . $object->taxonomy . '-' . $object->slug . '.php'; + $templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '-' . $object->slug . '.php'; + $templates[] = 'taxonomy-' . $object->taxonomy . '.php'; + $templates[] = WC()->template_path() . 'taxonomy-' . $cs_taxonomy . '.php'; + $templates[] = $cs_default; + } + } + + $templates[] = $default_file; + if ( isset( $cs_default ) ) { + $templates[] = WC()->template_path() . $cs_default; + } + $templates[] = WC()->template_path() . $default_file; + + return array_unique( $templates ); + } + + /** + * Load comments template. + * + * @param string $template template to load. + * @return string + */ + public static function comments_template_loader( $template ) { + if ( get_post_type() !== 'product' ) { + return $template; + } + + $check_dirs = array( + trailingslashit( get_stylesheet_directory() ) . WC()->template_path(), + trailingslashit( get_template_directory() ) . WC()->template_path(), + trailingslashit( get_stylesheet_directory() ), + trailingslashit( get_template_directory() ), + trailingslashit( WC()->plugin_path() ) . 'templates/', + ); + + if ( WC_TEMPLATE_DEBUG_MODE ) { + $check_dirs = array( array_pop( $check_dirs ) ); + } + + foreach ( $check_dirs as $dir ) { + if ( file_exists( trailingslashit( $dir ) . 'single-product-reviews.php' ) ) { + return trailingslashit( $dir ) . 'single-product-reviews.php'; + } + } + } + + /** + * Unsupported theme compatibility methods. + */ + + /** + * Hook in methods to enhance the unsupported theme experience on pages. + * + * @since 3.3.0 + */ + public static function unsupported_theme_init() { + if ( 0 < self::$shop_page_id ) { + if ( is_product_taxonomy() ) { + self::unsupported_theme_tax_archive_init(); + } elseif ( is_product() ) { + self::unsupported_theme_product_page_init(); + } else { + self::unsupported_theme_shop_page_init(); + } + } + } + + /** + * Hook in methods to enhance the unsupported theme experience on the Shop page. + * + * @since 3.3.0 + */ + private static function unsupported_theme_shop_page_init() { + add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ), 10 ); + add_filter( 'the_title', array( __CLASS__, 'unsupported_theme_title_filter' ), 10, 2 ); + add_filter( 'comments_number', array( __CLASS__, 'unsupported_theme_comments_number_filter' ) ); + } + + /** + * Hook in methods to enhance the unsupported theme experience on Product pages. + * + * @since 3.3.0 + */ + private static function unsupported_theme_product_page_init() { + add_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ), 10 ); + add_filter( 'post_thumbnail_html', array( __CLASS__, 'unsupported_theme_single_featured_image_filter' ) ); + add_filter( 'woocommerce_product_tabs', array( __CLASS__, 'unsupported_theme_remove_review_tab' ) ); + remove_action( 'woocommerce_before_main_content', 'woocommerce_output_content_wrapper', 10 ); + remove_action( 'woocommerce_after_main_content', 'woocommerce_output_content_wrapper_end', 10 ); + add_theme_support( 'wc-product-gallery-zoom' ); + add_theme_support( 'wc-product-gallery-lightbox' ); + add_theme_support( 'wc-product-gallery-slider' ); + } + + /** + * Enhance the unsupported theme experience on Product Category and Attribute pages by rendering + * those pages using the single template and shortcode-based content. To do this we make a dummy + * post and set a shortcode as the post content. This approach is adapted from bbPress. + * + * @since 3.3.0 + */ + private static function unsupported_theme_tax_archive_init() { + global $wp_query, $post; + + $queried_object = get_queried_object(); + $args = self::get_current_shop_view_args(); + $shortcode_args = array( + 'page' => $args->page, + 'columns' => $args->columns, + 'rows' => $args->rows, + 'orderby' => '', + 'order' => '', + 'paginate' => true, + 'cache' => false, + ); + + if ( is_product_category() ) { + $shortcode_args['category'] = sanitize_title( $queried_object->slug ); + } elseif ( taxonomy_is_product_attribute( $queried_object->taxonomy ) ) { + $shortcode_args['attribute'] = sanitize_title( $queried_object->taxonomy ); + $shortcode_args['terms'] = sanitize_title( $queried_object->slug ); + } elseif ( is_product_tag() ) { + $shortcode_args['tag'] = sanitize_title( $queried_object->slug ); + } else { + // Default theme archive for all other taxonomies. + return; + } + + // Description handling. + if ( ! empty( $queried_object->description ) && ( empty( $_GET['product-page'] ) || 1 === absint( $_GET['product-page'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $prefix = '
    ' . wc_format_content( wp_kses_post( $queried_object->description ) ) . '
    '; + } else { + $prefix = ''; + } + + add_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) ); + $shortcode = new WC_Shortcode_Products( $shortcode_args ); + remove_filter( 'woocommerce_shortcode_products_query', array( __CLASS__, 'unsupported_archive_layered_nav_compatibility' ) ); + $shop_page = get_post( self::$shop_page_id ); + + $dummy_post_properties = array( + 'ID' => 0, + 'post_status' => 'publish', + 'post_author' => $shop_page->post_author, + 'post_parent' => 0, + 'post_type' => 'page', + 'post_date' => $shop_page->post_date, + 'post_date_gmt' => $shop_page->post_date_gmt, + 'post_modified' => $shop_page->post_modified, + 'post_modified_gmt' => $shop_page->post_modified_gmt, + 'post_content' => $prefix . $shortcode->get_content(), + 'post_title' => wc_clean( $queried_object->name ), + 'post_excerpt' => '', + 'post_content_filtered' => '', + 'post_mime_type' => '', + 'post_password' => '', + 'post_name' => $queried_object->slug, + 'guid' => '', + 'menu_order' => 0, + 'pinged' => '', + 'to_ping' => '', + 'ping_status' => '', + 'comment_status' => 'closed', + 'comment_count' => 0, + 'filter' => 'raw', + ); + + // Set the $post global. + $post = new WP_Post( (object) $dummy_post_properties ); // @codingStandardsIgnoreLine. + + // Copy the new post global into the main $wp_query. + $wp_query->post = $post; + $wp_query->posts = array( $post ); + + // Prevent comments form from appearing. + $wp_query->post_count = 1; + $wp_query->is_404 = false; + $wp_query->is_page = true; + $wp_query->is_single = true; + $wp_query->is_archive = false; + $wp_query->is_tax = true; + $wp_query->max_num_pages = 0; + + // Prepare everything for rendering. + setup_postdata( $post ); + remove_all_filters( 'the_content' ); + remove_all_filters( 'the_excerpt' ); + add_filter( 'template_include', array( __CLASS__, 'force_single_template_filter' ) ); + } + + /** + * Add layered nav args to WP_Query args generated by the 'products' shortcode. + * + * @since 3.3.4 + * @param array $query WP_Query args. + * @return array + */ + public static function unsupported_archive_layered_nav_compatibility( $query ) { + foreach ( WC()->query->get_layered_nav_chosen_attributes() as $taxonomy => $data ) { + $query['tax_query'][] = array( + 'taxonomy' => $taxonomy, + 'field' => 'slug', + 'terms' => $data['terms'], + 'operator' => 'and' === $data['query_type'] ? 'AND' : 'IN', + 'include_children' => false, + ); + } + return $query; + } + + /** + * Force the loading of one of the single templates instead of whatever template was about to be loaded. + * + * @since 3.3.0 + * @param string $template Path to template. + * @return string + */ + public static function force_single_template_filter( $template ) { + $possible_templates = array( + 'page', + 'single', + 'singular', + 'index', + ); + + foreach ( $possible_templates as $possible_template ) { + $path = get_query_template( $possible_template ); + if ( $path ) { + return $path; + } + } + + return $template; + } + + /** + * Get information about the current shop page view. + * + * @since 3.3.0 + * @return array + */ + private static function get_current_shop_view_args() { + return (object) array( + 'page' => absint( max( 1, absint( get_query_var( 'paged' ) ) ) ), + 'columns' => wc_get_default_products_per_row(), + 'rows' => wc_get_default_product_rows_per_page(), + ); + } + + /** + * Filter the title and insert WooCommerce content on the shop page. + * + * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. + * + * @since 3.3.0 + * @param string $title Existing title. + * @param int $id ID of the post being filtered. + * @return string + */ + public static function unsupported_theme_title_filter( $title, $id ) { + if ( self::$theme_support || ! $id !== self::$shop_page_id ) { + return $title; + } + + if ( is_page( self::$shop_page_id ) || ( is_home() && 'page' === get_option( 'show_on_front' ) && absint( get_option( 'page_on_front' ) ) === self::$shop_page_id ) ) { + $args = self::get_current_shop_view_args(); + $title_suffix = array(); + + if ( $args->page > 1 ) { + /* translators: %d: Page number. */ + $title_suffix[] = sprintf( esc_html__( 'Page %d', 'woocommerce' ), $args->page ); + } + + if ( $title_suffix ) { + $title = $title . ' – ' . implode( ', ', $title_suffix ); + } + } + return $title; + } + + /** + * Filter the content and insert WooCommerce content on the shop page. + * + * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. + * + * @since 3.3.0 + * @param string $content Existing post content. + * @return string + */ + public static function unsupported_theme_shop_content_filter( $content ) { + global $wp_query; + + if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) { + return $content; + } + + self::$in_content_filter = true; + + // Remove the filter we're in to avoid nested calls. + remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_shop_content_filter' ) ); + + // Unsupported theme shop page. + if ( is_page( self::$shop_page_id ) ) { + $args = self::get_current_shop_view_args(); + $shortcode = new WC_Shortcode_Products( + array_merge( + WC()->query->get_catalog_ordering_args(), + array( + 'page' => $args->page, + 'columns' => $args->columns, + 'rows' => $args->rows, + 'orderby' => '', + 'order' => '', + 'paginate' => true, + 'cache' => false, + ) + ), + 'products' + ); + + // Allow queries to run e.g. layered nav. + add_action( 'pre_get_posts', array( WC()->query, 'product_query' ) ); + + $content = $content . $shortcode->get_content(); + + // Remove actions and self to avoid nested calls. + remove_action( 'pre_get_posts', array( WC()->query, 'product_query' ) ); + WC()->query->remove_ordering_args(); + } + + self::$in_content_filter = false; + + return $content; + } + + /** + * Filter the content and insert WooCommerce content on the shop page. + * + * For non-WC themes, this will setup the main shop page to be shortcode based to improve default appearance. + * + * @since 3.3.0 + * @param string $content Existing post content. + * @return string + */ + public static function unsupported_theme_product_content_filter( $content ) { + global $wp_query; + + if ( self::$theme_support || ! is_main_query() || ! in_the_loop() ) { + return $content; + } + + self::$in_content_filter = true; + + // Remove the filter we're in to avoid nested calls. + remove_filter( 'the_content', array( __CLASS__, 'unsupported_theme_product_content_filter' ) ); + + if ( is_product() ) { + $content = do_shortcode( '[product_page id="' . get_the_ID() . '" show_title=0 status="any"]' ); + } + + self::$in_content_filter = false; + + return $content; + } + + /** + * Suppress the comments number on the Shop page for unsupported themes since there is no commenting on the Shop page. + * + * @since 3.4.5 + * @param string $comments_number The comments number text. + * @return string + */ + public static function unsupported_theme_comments_number_filter( $comments_number ) { + if ( is_page( self::$shop_page_id ) ) { + return ''; + } + + return $comments_number; + } + + /** + * Are we filtering content for unsupported themes? + * + * @since 3.3.2 + * @return bool + */ + public static function in_content_filter() { + return (bool) self::$in_content_filter; + } + + /** + * Prevent the main featured image on product pages because there will be another featured image + * in the gallery. + * + * @since 3.3.0 + * @param string $html Img element HTML. + * @return string + */ + public static function unsupported_theme_single_featured_image_filter( $html ) { + if ( self::in_content_filter() || ! is_product() || ! is_main_query() ) { + return $html; + } + + return ''; + } + + /** + * Remove the Review tab and just use the regular comment form. + * + * @param array $tabs Tab info. + * @return array + */ + public static function unsupported_theme_remove_review_tab( $tabs ) { + unset( $tabs['reviews'] ); + return $tabs; + } +} + +add_action( 'init', array( 'WC_Template_Loader', 'init' ) ); diff --git a/includes/class-wc-tracker.php b/includes/class-wc-tracker.php new file mode 100644 index 0000000..9346ce8 --- /dev/null +++ b/includes/class-wc-tracker.php @@ -0,0 +1,773 @@ + apply_filters( 'woocommerce_tracker_last_send_interval', strtotime( '-1 week' ) ) ) { + return; + } + } else { + // Make sure there is at least a 1 hour delay between override sends, we don't want duplicate calls due to double clicking links. + $last_send = self::get_last_send_time(); + if ( $last_send && $last_send > strtotime( '-1 hours' ) ) { + return; + } + } + + // Update time first before sending to ensure it is set. + update_option( 'woocommerce_tracker_last_send', time() ); + + $params = self::get_tracking_data(); + wp_safe_remote_post( + self::$api_url, + array( + 'method' => 'POST', + 'timeout' => 45, + 'redirection' => 5, + 'httpversion' => '1.0', + 'blocking' => false, + 'headers' => array( 'user-agent' => 'WooCommerceTracker/' . md5( esc_url_raw( home_url( '/' ) ) ) . ';' ), + 'body' => wp_json_encode( $params ), + 'cookies' => array(), + ) + ); + } + + /** + * Get the last time tracking data was sent. + * + * @return int|bool + */ + private static function get_last_send_time() { + return apply_filters( 'woocommerce_tracker_last_send_time', get_option( 'woocommerce_tracker_last_send', false ) ); + } + + /** + * Test whether this site is a staging site according to the Jetpack criteria. + * + * With Jetpack 8.1+, Jetpack::is_staging_site has been deprecated. + * \Automattic\Jetpack\Status::is_staging_site is the replacement. + * However, there are version of JP where \Automattic\Jetpack\Status exists, but does *not* contain is_staging_site method, + * so with those, code still needs to use the previous check as a fallback. + * + * @return bool + */ + private static function is_jetpack_staging_site() { + if ( class_exists( '\Automattic\Jetpack\Status' ) ) { + // Preferred way of checking with Jetpack 8.1+. + $jp_status = new \Automattic\Jetpack\Status(); + if ( is_callable( array( $jp_status, 'is_staging_site' ) ) ) { + return $jp_status->is_staging_site(); + } + } + + return ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_staging_site' ) && Jetpack::is_staging_site() ); + } + + /** + * Get all the tracking data. + * + * @return array + */ + public static function get_tracking_data() { + $data = array(); + + // General site info. + $data['url'] = home_url(); + $data['email'] = apply_filters( 'woocommerce_tracker_admin_email', get_option( 'admin_email' ) ); + $data['theme'] = self::get_theme_info(); + + // WordPress Info. + $data['wp'] = self::get_wordpress_info(); + + // Server Info. + $data['server'] = self::get_server_info(); + + // Plugin info. + $all_plugins = self::get_all_plugins(); + $data['active_plugins'] = $all_plugins['active_plugins']; + $data['inactive_plugins'] = $all_plugins['inactive_plugins']; + + // Jetpack & WooCommerce Connect. + + $data['jetpack_version'] = Constants::is_defined( 'JETPACK__VERSION' ) ? Constants::get_constant( 'JETPACK__VERSION' ) : 'none'; + $data['jetpack_connected'] = ( class_exists( 'Jetpack' ) && is_callable( 'Jetpack::is_active' ) && Jetpack::is_active() ) ? 'yes' : 'no'; + $data['jetpack_is_staging'] = self::is_jetpack_staging_site() ? 'yes' : 'no'; + $data['connect_installed'] = class_exists( 'WC_Connect_Loader' ) ? 'yes' : 'no'; + $data['connect_active'] = ( class_exists( 'WC_Connect_Loader' ) && wp_next_scheduled( 'wc_connect_fetch_service_schemas' ) ) ? 'yes' : 'no'; + $data['helper_connected'] = self::get_helper_connected(); + + // Store count info. + $data['users'] = self::get_user_counts(); + $data['products'] = self::get_product_counts(); + $data['orders'] = self::get_orders(); + $data['reviews'] = self::get_review_counts(); + $data['categories'] = self::get_category_counts(); + + // Payment gateway info. + $data['gateways'] = self::get_active_payment_gateways(); + + // Shipping method info. + $data['shipping_methods'] = self::get_active_shipping_methods(); + + // Get all WooCommerce options info. + $data['settings'] = self::get_all_woocommerce_options_values(); + + // Template overrides. + $data['template_overrides'] = self::get_all_template_overrides(); + + // Cart & checkout tech (blocks or shortcodes). + $data['cart_checkout'] = self::get_cart_checkout_info(); + + // WooCommerce Admin info. + $data['wc_admin_disabled'] = apply_filters( 'woocommerce_admin_disabled', false ) ? 'yes' : 'no'; + + // Mobile info. + $data['wc_mobile_usage'] = self::get_woocommerce_mobile_usage(); + + return apply_filters( 'woocommerce_tracker_data', $data ); + } + + /** + * Get the current theme info, theme name and version. + * + * @return array + */ + public static function get_theme_info() { + $theme_data = wp_get_theme(); + $theme_child_theme = wc_bool_to_string( is_child_theme() ); + $theme_wc_support = wc_bool_to_string( current_theme_supports( 'woocommerce' ) ); + + return array( + 'name' => $theme_data->Name, // @phpcs:ignore + 'version' => $theme_data->Version, // @phpcs:ignore + 'child_theme' => $theme_child_theme, + 'wc_support' => $theme_wc_support, + ); + } + + /** + * Get WordPress related data. + * + * @return array + */ + private static function get_wordpress_info() { + $wp_data = array(); + + $memory = wc_let_to_num( WP_MEMORY_LIMIT ); + + if ( function_exists( 'memory_get_usage' ) ) { + $system_memory = wc_let_to_num( @ini_get( 'memory_limit' ) ); + $memory = max( $memory, $system_memory ); + } + + // WordPress 5.5+ environment type specification. + // 'production' is the default in WP, thus using it as a default here, too. + $environment_type = 'production'; + if ( function_exists( 'wp_get_environment_type' ) ) { + $environment_type = wp_get_environment_type(); + } + + $wp_data['memory_limit'] = size_format( $memory ); + $wp_data['debug_mode'] = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'Yes' : 'No'; + $wp_data['locale'] = get_locale(); + $wp_data['version'] = get_bloginfo( 'version' ); + $wp_data['multisite'] = is_multisite() ? 'Yes' : 'No'; + $wp_data['env_type'] = $environment_type; + + return $wp_data; + } + + /** + * Get server related info. + * + * @return array + */ + private static function get_server_info() { + $server_data = array(); + + if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) ) { + $server_data['software'] = $_SERVER['SERVER_SOFTWARE']; // @phpcs:ignore + } + + if ( function_exists( 'phpversion' ) ) { + $server_data['php_version'] = phpversion(); + } + + if ( function_exists( 'ini_get' ) ) { + $server_data['php_post_max_size'] = size_format( wc_let_to_num( ini_get( 'post_max_size' ) ) ); + $server_data['php_time_limt'] = ini_get( 'max_execution_time' ); + $server_data['php_max_input_vars'] = ini_get( 'max_input_vars' ); + $server_data['php_suhosin'] = extension_loaded( 'suhosin' ) ? 'Yes' : 'No'; + } + + $database_version = wc_get_server_database_version(); + $server_data['mysql_version'] = $database_version['number']; + + $server_data['php_max_upload_size'] = size_format( wp_max_upload_size() ); + $server_data['php_default_timezone'] = date_default_timezone_get(); + $server_data['php_soap'] = class_exists( 'SoapClient' ) ? 'Yes' : 'No'; + $server_data['php_fsockopen'] = function_exists( 'fsockopen' ) ? 'Yes' : 'No'; + $server_data['php_curl'] = function_exists( 'curl_init' ) ? 'Yes' : 'No'; + + return $server_data; + } + + /** + * Get all plugins grouped into activated or not. + * + * @return array + */ + private static function get_all_plugins() { + // Ensure get_plugins function is loaded. + if ( ! function_exists( 'get_plugins' ) ) { + include ABSPATH . '/wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + $active_plugins_keys = get_option( 'active_plugins', array() ); + $active_plugins = array(); + + foreach ( $plugins as $k => $v ) { + // Take care of formatting the data how we want it. + $formatted = array(); + $formatted['name'] = strip_tags( $v['Name'] ); + if ( isset( $v['Version'] ) ) { + $formatted['version'] = strip_tags( $v['Version'] ); + } + if ( isset( $v['Author'] ) ) { + $formatted['author'] = strip_tags( $v['Author'] ); + } + if ( isset( $v['Network'] ) ) { + $formatted['network'] = strip_tags( $v['Network'] ); + } + if ( isset( $v['PluginURI'] ) ) { + $formatted['plugin_uri'] = strip_tags( $v['PluginURI'] ); + } + if ( in_array( $k, $active_plugins_keys ) ) { + // Remove active plugins from list so we can show active and inactive separately. + unset( $plugins[ $k ] ); + $active_plugins[ $k ] = $formatted; + } else { + $plugins[ $k ] = $formatted; + } + } + + return array( + 'active_plugins' => $active_plugins, + 'inactive_plugins' => $plugins, + ); + } + + /** + * Check to see if the helper is connected to woocommerce.com + * + * @return string + */ + private static function get_helper_connected() { + if ( class_exists( 'WC_Helper_Options' ) && is_callable( 'WC_Helper_Options::get' ) ) { + $authenticated = WC_Helper_Options::get( 'auth' ); + } else { + $authenticated = ''; + } + return ( ! empty( $authenticated ) ) ? 'yes' : 'no'; + } + + + /** + * Get user totals based on user role. + * + * @return array + */ + private static function get_user_counts() { + $user_count = array(); + $user_count_data = count_users(); + $user_count['total'] = $user_count_data['total_users']; + + // Get user count based on user role. + foreach ( $user_count_data['avail_roles'] as $role => $count ) { + $user_count[ $role ] = $count; + } + + return $user_count; + } + + /** + * Get product totals based on product type. + * + * @return array + */ + public static function get_product_counts() { + $product_count = array(); + $product_count_data = wp_count_posts( 'product' ); + $product_count['total'] = $product_count_data->publish; + + $product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); + foreach ( $product_statuses as $product_status ) { + $product_count[ $product_status->name ] = $product_status->count; + } + + return $product_count; + } + + /** + * Get order counts. + * + * @return array + */ + private static function get_order_counts() { + $order_count = array(); + $order_count_data = wp_count_posts( 'shop_order' ); + foreach ( wc_get_order_statuses() as $status_slug => $status_name ) { + $order_count[ $status_slug ] = $order_count_data->{ $status_slug }; + } + return $order_count; + } + + /** + * Combine all order data. + * + * @return array + */ + private static function get_orders() { + $order_dates = self::get_order_dates(); + $order_counts = self::get_order_counts(); + $order_totals = self::get_order_totals(); + $order_gateways = self::get_orders_by_gateway(); + + return array_merge( $order_dates, $order_counts, $order_totals, $order_gateways ); + } + + /** + * Get order totals. + * + * @since 5.4.0 + * @return array + */ + private static function get_order_totals() { + global $wpdb; + + $gross_total = $wpdb->get_var( + " + SELECT + SUM( order_meta.meta_value ) AS 'gross_total' + FROM {$wpdb->prefix}posts AS orders + LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID + WHERE order_meta.meta_key = '_order_total' + AND orders.post_status in ( 'wc-completed', 'wc-refunded' ) + GROUP BY order_meta.meta_key + " + ); + + if ( is_null( $gross_total ) ) { + $gross_total = 0; + } + + $processing_gross_total = $wpdb->get_var( + " + SELECT + SUM( order_meta.meta_value ) AS 'gross_total' + FROM {$wpdb->prefix}posts AS orders + LEFT JOIN {$wpdb->prefix}postmeta AS order_meta ON order_meta.post_id = orders.ID + WHERE order_meta.meta_key = '_order_total' + AND orders.post_status = 'wc-processing' + GROUP BY order_meta.meta_key + " + ); + + if ( is_null( $processing_gross_total ) ) { + $processing_gross_total = 0; + } + + return array( + 'gross' => $gross_total, + 'processing_gross' => $processing_gross_total, + ); + } + + /** + * Get last order date. + * + * @return string + */ + private static function get_order_dates() { + global $wpdb; + + $min_max = $wpdb->get_row( + " + SELECT + MIN( post_date_gmt ) as 'first', MAX( post_date_gmt ) as 'last' + FROM {$wpdb->prefix}posts + WHERE post_type = 'shop_order' + AND post_status = 'wc-completed' + ", + ARRAY_A + ); + + if ( is_null( $min_max ) ) { + $min_max = array( + 'first' => '-', + 'last' => '-', + ); + } + + $processing_min_max = $wpdb->get_row( + " + SELECT + MIN( post_date_gmt ) as 'processing_first', MAX( post_date_gmt ) as 'processing_last' + FROM {$wpdb->prefix}posts + WHERE post_type = 'shop_order' + AND post_status = 'wc-processing' + ", + ARRAY_A + ); + + if ( is_null( $processing_min_max ) ) { + $processing_min_max = array( + 'processing_first' => '-', + 'processing_last' => '-', + ); + } + + return array_merge( $min_max, $processing_min_max ); + } + + /** + * Get order details by gateway. + * + * @return array + */ + private static function get_orders_by_gateway() { + global $wpdb; + + $orders_by_gateway = $wpdb->get_results( + " + SELECT + gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts + FROM ( + SELECT + orders.id AS order_id, + MAX(CASE WHEN meta_key = '_payment_method' THEN meta_value END) gateway, + MAX(CASE WHEN meta_key = '_order_total' THEN meta_value END) total, + MAX(CASE WHEN meta_key = '_order_currency' THEN meta_value END) currency + FROM + {$wpdb->prefix}posts orders + LEFT JOIN + {$wpdb->prefix}postmeta order_meta ON order_meta.post_id = orders.id + WHERE orders.post_type = 'shop_order' + AND orders.post_status in ( 'wc-completed', 'wc-processing', 'wc-refunded' ) + AND meta_key in( '_payment_method','_order_total','_order_currency') + GROUP BY orders.id + ) order_gateways + GROUP BY gateway, currency + " + ); + + $orders_by_gateway_currency = array(); + foreach ( $orders_by_gateway as $orders_details ) { + $gateway = 'gateway_' . $orders_details->gateway; + $currency = $orders_details->currency; + $count = $gateway . '_' . $currency . '_count'; + $total = $gateway . '_' . $currency . '_total'; + + $orders_by_gateway_currency[ $count ] = $orders_details->counts; + $orders_by_gateway_currency[ $total ] = $orders_details->totals; + } + + return $orders_by_gateway_currency; + } + + /** + * Get review counts for different statuses. + * + * @return array + */ + private static function get_review_counts() { + global $wpdb; + $review_count = array( 'total' => 0 ); + $status_map = array( + '0' => 'pending', + '1' => 'approved', + 'trash' => 'trash', + 'spam' => 'spam', + ); + $counts = $wpdb->get_results( + " + SELECT comment_approved, COUNT(*) AS num_reviews + FROM {$wpdb->comments} + WHERE comment_type = 'review' + GROUP BY comment_approved + ", + ARRAY_A + ); + + if ( ! $counts ) { + return $review_count; + } + + foreach ( $counts as $count ) { + $status = $count['comment_approved']; + if ( array_key_exists( $status, $status_map ) ) { + $review_count[ $status_map[ $status ] ] = $count['num_reviews']; + } + $review_count['total'] += $count['num_reviews']; + } + + return $review_count; + } + + /** + * Get the number of product categories. + * + * @return int + */ + private static function get_category_counts() { + return wp_count_terms( 'product_cat' ); + } + + /** + * Get a list of all active payment gateways. + * + * @return array + */ + private static function get_active_payment_gateways() { + $active_gateways = array(); + $gateways = WC()->payment_gateways->payment_gateways(); + foreach ( $gateways as $id => $gateway ) { + if ( isset( $gateway->enabled ) && 'yes' === $gateway->enabled ) { + $active_gateways[ $id ] = array( + 'title' => $gateway->title, + 'supports' => $gateway->supports, + ); + } + } + + return $active_gateways; + } + + /** + * Get a list of all active shipping methods. + * + * @return array + */ + private static function get_active_shipping_methods() { + $active_methods = array(); + $shipping_methods = WC()->shipping()->get_shipping_methods(); + foreach ( $shipping_methods as $id => $shipping_method ) { + if ( isset( $shipping_method->enabled ) && 'yes' === $shipping_method->enabled ) { + $active_methods[ $id ] = array( + 'title' => $shipping_method->title, + 'tax_status' => $shipping_method->tax_status, + ); + } + } + + return $active_methods; + } + + /** + * Get all options starting with woocommerce_ prefix. + * + * @return array + */ + private static function get_all_woocommerce_options_values() { + return array( + 'version' => WC()->version, + 'currency' => get_woocommerce_currency(), + 'base_location' => WC()->countries->get_base_country(), + 'base_state' => WC()->countries->get_base_state(), + 'base_postcode' => WC()->countries->get_base_postcode(), + 'selling_locations' => WC()->countries->get_allowed_countries(), + 'api_enabled' => get_option( 'woocommerce_api_enabled' ), + 'weight_unit' => get_option( 'woocommerce_weight_unit' ), + 'dimension_unit' => get_option( 'woocommerce_dimension_unit' ), + 'download_method' => get_option( 'woocommerce_file_download_method' ), + 'download_require_login' => get_option( 'woocommerce_downloads_require_login' ), + 'calc_taxes' => get_option( 'woocommerce_calc_taxes' ), + 'coupons_enabled' => get_option( 'woocommerce_enable_coupons' ), + 'guest_checkout' => get_option( 'woocommerce_enable_guest_checkout' ), + 'checkout_login_reminder' => get_option( 'woocommerce_enable_checkout_login_reminder' ), + 'secure_checkout' => get_option( 'woocommerce_force_ssl_checkout' ), + 'enable_signup_and_login_from_checkout' => get_option( 'woocommerce_enable_signup_and_login_from_checkout' ), + 'enable_myaccount_registration' => get_option( 'woocommerce_enable_myaccount_registration' ), + 'registration_generate_username' => get_option( 'woocommerce_registration_generate_username' ), + 'registration_generate_password' => get_option( 'woocommerce_registration_generate_password' ), + ); + } + + /** + * Look for any template override and return filenames. + * + * @return array + */ + private static function get_all_template_overrides() { + $override_data = array(); + $template_paths = apply_filters( 'woocommerce_template_overrides_scan_paths', array( 'WooCommerce' => WC()->plugin_path() . '/templates/' ) ); + $scanned_files = array(); + + require_once WC()->plugin_path() . '/includes/admin/class-wc-admin-status.php'; + + foreach ( $template_paths as $plugin_name => $template_path ) { + $scanned_files[ $plugin_name ] = WC_Admin_Status::scan_template_files( $template_path ); + } + + foreach ( $scanned_files as $plugin_name => $files ) { + foreach ( $files as $file ) { + if ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . $file; + } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; + } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { + $theme_file = get_template_directory() . '/' . $file; + } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; + } else { + $theme_file = false; + } + + if ( false !== $theme_file ) { + $override_data[] = basename( $theme_file ); + } + } + } + return $override_data; + } + + /** + * Search a specific post for text content. + * + * @param integer $post_id The id of the post to search. + * @param string $text The text to search for. + * @return string 'Yes' if post contains $text (otherwise 'No'). + */ + public static function post_contains_text( $post_id, $text ) { + global $wpdb; + + // Search for the text anywhere in the post. + $wildcarded = "%{$text}%"; + + $result = $wpdb->get_var( + $wpdb->prepare( + " + SELECT COUNT( * ) FROM {$wpdb->prefix}posts + WHERE ID=%d + AND {$wpdb->prefix}posts.post_content LIKE %s + ", + array( $post_id, $wildcarded ) + ) + ); + + return ( '0' !== $result ) ? 'Yes' : 'No'; + } + + + /** + * Get tracker data for a specific block type on a woocommerce page. + * + * @param string $block_name The name (id) of a block, e.g. `woocommerce/cart`. + * @param string $woo_page_name The woo page to search, e.g. `cart`. + * @return array Associative array of tracker data with keys: + * - page_contains_block + * - block_attributes + */ + public static function get_block_tracker_data( $block_name, $woo_page_name ) { + $blocks = WC_Blocks_Utils::get_blocks_from_page( $block_name, $woo_page_name ); + + $block_present = false; + $attributes = array(); + if ( $blocks && count( $blocks ) ) { + // Return any customised attributes from the first block. + $block_present = true; + $attributes = $blocks[0]['attrs']; + } + + return array( + 'page_contains_block' => $block_present ? 'Yes' : 'No', + 'block_attributes' => $attributes, + ); + } + + /** + * Get info about the cart & checkout pages. + * + * @return array + */ + public static function get_cart_checkout_info() { + $cart_page_id = wc_get_page_id( 'cart' ); + $checkout_page_id = wc_get_page_id( 'checkout' ); + + $cart_block_data = self::get_block_tracker_data( 'woocommerce/cart', 'cart' ); + $checkout_block_data = self::get_block_tracker_data( 'woocommerce/checkout', 'checkout' ); + + return array( + 'cart_page_contains_cart_shortcode' => self::post_contains_text( + $cart_page_id, + '[woocommerce_cart]' + ), + 'checkout_page_contains_checkout_shortcode' => self::post_contains_text( + $checkout_page_id, + '[woocommerce_checkout]' + ), + + 'cart_page_contains_cart_block' => $cart_block_data['page_contains_block'], + 'cart_block_attributes' => $cart_block_data['block_attributes'], + 'checkout_page_contains_checkout_block' => $checkout_block_data['page_contains_block'], + 'checkout_block_attributes' => $checkout_block_data['block_attributes'], + ); + } + + /** + * Get info about WooCommerce Mobile App usage + * + * @return array + */ + public static function get_woocommerce_mobile_usage() { + return get_option( 'woocommerce_mobile_app_usage' ); + } +} + +WC_Tracker::init(); diff --git a/includes/class-wc-validation.php b/includes/class-wc-validation.php new file mode 100644 index 0000000..c2380d4 --- /dev/null +++ b/includes/class-wc-validation.php @@ -0,0 +1,201 @@ + 0 ) { + return false; + } + + switch ( $country ) { + case 'AT': + $valid = (bool) preg_match( '/^([0-9]{4})$/', $postcode ); + break; + case 'BA': + $valid = (bool) preg_match( '/^([7-8]{1})([0-9]{4})$/', $postcode ); + break; + case 'BE': + $valid = (bool) preg_match( '/^([0-9]{4})$/i', $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': + case 'IT': + $valid = (bool) preg_match( '/^([0-9]{5})$/i', $postcode ); + break; + case 'GB': + $valid = self::is_gb_postcode( $postcode ); + break; + case 'HU': + $valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode ); + break; + case 'IE': + $valid = (bool) preg_match( '/([AC-FHKNPRTV-Y]\d{2}|D6W)[0-9AC-FHKNPRTV-Y]{4}/', wc_normalize_postcode( $postcode ) ); + break; + case 'IN': + $valid = (bool) preg_match( '/^[1-9]{1}[0-9]{2}\s{0,1}[0-9]{3}$/', $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 'PR': + 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; + case 'CZ': + case 'SK': + $valid = (bool) preg_match( '/^([0-9]{3})(\s?)([0-9]{2})$/', $postcode ); + break; + case 'NL': + $valid = (bool) preg_match( '/^([1-9][0-9]{3})(\s?)(?!SA|SD|SS)[A-Z]{2}$/i', $postcode ); + break; + case 'SI': + $valid = (bool) preg_match( '/^([1-9][0-9]{3})$/', $postcode ); + break; + case 'LI': + $valid = (bool) preg_match( '/^(94[8-9][0-9])$/', $postcode ); + break; + default: + $valid = true; + break; + } + + return apply_filters( 'woocommerce_validate_postcode', $valid, $postcode, $country ); + } + + /** + * Check if is a GB postcode. + * + * @param string $to_check A postcode. + * @return bool + */ + public static function is_gb_postcode( $to_check ) { + + // Permitted letters depend upon their position in the postcode. + // 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})$/'; + + // Expression for postcodes: ANA NAA. + $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})$/'; + + // Exception for the special postcode GIR 0AA. + $pcexp[3] = '/^(gir)(0aa)$/'; + + // Standard BFPO numbers. + $pcexp[4] = '/^(bfpo)([0-9]{1,4})$/'; + + // c/o BFPO numbers. + $pcexp[5] = '/^(bfpo)(c\/o[0-9]{1,3})$/'; + + // Load up the string to check, converting into lowercase and removing spaces. + $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 ) ) { + // Remember that we have found that the code is valid and break from loop. + $valid = true; + break; + } + } + + return $valid; + } + + /** + * Format the postcode according to the country and length of the 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 ) { + return wc_format_postcode( $postcode, $country ); + } + + /** + * Format a given phone number. + * + * @param mixed $tel Phone number to format. + * @return string + */ + public static function format_phone( $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 0000000..1d55682 --- /dev/null +++ b/includes/class-wc-webhook.php @@ -0,0 +1,1073 @@ + null, + 'date_modified' => null, + 'status' => 'disabled', + 'delivery_url' => '', + 'secret' => '', + 'name' => '', + 'topic' => '', + 'hooks' => '', + 'resource' => '', + 'event' => '', + 'failure_count' => 0, + 'user_id' => 0, + 'api_version' => 3, + 'pending_delivery' => false, + ); + + /** + * Load webhook data based on how WC_Webhook is called. + * + * @param WC_Webhook|int $data Webhook ID or data. + * @throws Exception If webhook cannot be read/found and $data is set. + */ + public function __construct( $data = 0 ) { + parent::__construct( $data ); + + if ( $data instanceof WC_Webhook ) { + $this->set_id( absint( $data->get_id() ) ); + } elseif ( is_numeric( $data ) ) { + $this->set_id( $data ); + } + + $this->data_store = WC_Data_Store::load( 'webhook' ); + + // If we have an ID, load the webhook 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 ); + } + } + + /** + * Enqueue the hooks associated with the webhook. + * + * @since 2.2.0 + */ + 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.0 + * @param mixed $arg The first argument provided from the associated hooks. + * @return mixed $arg Returns the argument in case the webhook was hooked into a filter. + */ + public function process( $arg ) { + + // Verify that webhook should be processed for delivery. + if ( ! $this->should_deliver( $arg ) ) { + return; + } + + // Mark this $arg as processed to ensure it doesn't get processed again within the current request. + $this->processed[] = $arg; + + /** + * Process webhook delivery. + * + * @since 3.3.0 + * @hooked wc_webhook_process_delivery - 10 + */ + do_action( 'woocommerce_webhook_process_delivery', $this, $arg ); + + return $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.0 + * @param mixed $arg First hook argument. + * @return bool True if webhook should be delivered, false otherwise. + */ + private function should_deliver( $arg ) { + $should_deliver = $this->is_active() && $this->is_valid_topic() && $this->is_valid_action( $arg ) && $this->is_valid_resource( $arg ) && ! $this->is_already_processed( $arg ); + + /** + * Let other plugins intercept deliver for some messages queue like rabbit/zeromq. + * + * @param bool $should_deliver True if the webhook should be sent, or false to not send it. + * @param WC_Webhook $this The current webhook class. + * @param mixed $arg First hook argument. + */ + return apply_filters( 'woocommerce_webhook_should_deliver', $should_deliver, $this, $arg ); + } + + /** + * Returns if webhook is active. + * + * @since 3.6.0 + * @return bool True if validation passes. + */ + private function is_active() { + return 'active' === $this->get_status(); + } + + /** + * Returns if topic is valid. + * + * @since 3.6.0 + * @return bool True if validation passes. + */ + private function is_valid_topic() { + return wc_is_webhook_valid_topic( $this->get_topic() ); + } + + /** + * Validates the criteria for certain actions. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_action( $arg ) { + $current_action = current_action(); + $return = true; + + switch ( $current_action ) { + case 'delete_post': + case 'wp_trash_post': + case 'untrashed_post': + $return = $this->is_valid_post_action( $arg ); + break; + case 'delete_user': + $return = $this->is_valid_user_action( $arg ); + break; + } + + if ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) { + $return = $this->is_valid_processing_action( $arg ); + } + + return $return; + } + + /** + * Validates post actions. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_post_action( $arg ) { + // 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' ), true ) ) { + return false; + } + + // Check if is delivering for the correct resource. + if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) { + return false; + } + return true; + } + + /** + * Validates user actions. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_user_action( $arg ) { + $user = get_userdata( absint( $arg ) ); + + // Only deliver deleted customer event for users with customer role. + if ( ! $user || ! in_array( 'customer', (array) $user->roles, true ) ) { + return false; + } + + return true; + } + + /** + * Validates WC processing actions. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_processing_action( $arg ) { + // 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 ) { + return false; + } elseif ( 'updated' === $this->get_event() && $resource_created ) { + return false; + } + return true; + } + + /** + * Checks the resource for this webhook is valid e.g. valid post status. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_resource( $arg ) { + $resource = $this->get_resource(); + + if ( in_array( $resource, array( 'order', 'product', 'coupon' ), true ) ) { + $status = get_post_status( absint( $arg ) ); + + // Ignore auto drafts for all resources. + if ( in_array( $status, array( 'auto-draft', 'new' ), true ) ) { + return false; + } + + // Ignore standard drafts for orders. + if ( 'order' === $resource && 'draft' === $status ) { + return false; + } + + // Check registered order types for order types args. + if ( 'order' === $resource && ! in_array( get_post_type( absint( $arg ) ), wc_get_order_types( 'order-webhooks' ), true ) ) { + return false; + } + } + return true; + } + + /** + * Checks if the specified resource has already been queued for delivery within the current request. + * + * Helps avoid duplication of data being sent for topics that have more than one hook defined. + * + * @param mixed $arg First hook argument. + * + * @return bool + */ + protected function is_already_processed( $arg ) { + return false !== array_search( $arg, $this->processed, true ); + } + + /** + * Deliver the webhook payload using wp_safe_remote_request(). + * + * @since 2.2.0 + * @param mixed $arg First hook argument. + */ + public function deliver( $arg ) { + $start_time = microtime( true ); + $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)', Constants::get_constant( 'WC_VERSION' ), $GLOBALS['wp_version'] ), + 'body' => trim( wp_json_encode( $payload ) ), + 'headers' => array( + 'Content-Type' => 'application/json', + ), + 'cookies' => array(), + ); + + $http_args = apply_filters( 'woocommerce_webhook_http_args', $http_args, $arg, $this->get_id() ); + + // Add custom headers. + $delivery_id = $this->get_new_delivery_id(); + $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->get_id(); + $http_args['headers']['X-WC-Webhook-Delivery-ID'] = $delivery_id; + + // Webhook away! + $response = wp_safe_remote_request( $this->get_delivery_url(), $http_args ); + + $duration = NumberUtil::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->get_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 ) { + switch ( $resource ) { + case 'coupon': + case 'customer': + case 'order': + case 'product': + // 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(); + } + + $version = str_replace( 'wp_api_', '', $this->get_api_version() ); + $payload = wc()->api->get_endpoint_data( "/wc/{$version}/{$resource}s/{$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; + } + + /** + * Build the payload data for the webhook. + * + * @since 2.2.0 + * @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(), wc_get_webhook_rest_api_versions(), 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->get_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.0 + * @param string $payload Payload data to hash. + * @return string + */ + public function generate_signature( $payload ) { + $hash_algo = apply_filters( 'woocommerce_webhook_hash_algorithm', 'sha256', $payload, $this->get_id() ); + + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + return base64_encode( hash_hmac( $hash_algo, $payload, wp_specialchars_decode( $this->get_secret(), ENT_QUOTES ), true ) ); + } + + /** + * Generate a new unique hash as a delivery id based on current time and wehbook id. + * Return the hash for inclusion in the webhook request. + * + * @since 2.2.0 + * @return string + */ + public function get_new_delivery_id() { + // Since we no longer use comments to store delivery logs, we generate a unique hash instead based on current time and webhook ID. + return wp_hash( $this->get_id() . strtotime( 'now' ) ); + } + + /** + * Log the delivery request/response. + * + * @since 2.2.0 + * @param string $delivery_id Previously created hash. + * @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 ) { + $logger = wc_get_logger(); + $message = array( + 'Webhook Delivery' => array( + 'Delivery ID' => $delivery_id, + 'Date' => date_i18n( __( 'M j, Y @ G:i', 'woocommerce' ), strtotime( 'now' ), true ), + 'URL' => $this->get_delivery_url(), + 'Duration' => $duration, + 'Request' => array( + 'Method' => $request['method'], + 'Headers' => array_merge( + array( + 'User-Agent' => $request['user-agent'], + ), + $request['headers'] + ), + ), + '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 ); + } + + $message['Webhook Delivery']['Response'] = array( + 'Code' => $response_code, + 'Message' => $response_message, + 'Headers' => $response_headers, + 'Body' => $response_body, + ); + + if ( ! Constants::is_true( 'WP_DEBUG' ) ) { + $message['Webhook Delivery']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on. This is to avoid the storing of personal data in the logs.'; + $message['Webhook Delivery']['Response']['Body'] = 'Webhook body is not logged unless WP_DEBUG mode is turned on. This is to avoid the storing of personal data in the logs.'; + } + + $logger->info( + wc_print_r( $message, true ), + array( + 'source' => 'webhooks-delivery', + ) + ); + + // Track failures. + // Check for a success, which is a 2xx, 301 or 302 Response Code. + if ( intval( $response_code ) >= 200 && intval( $response_code ) < 303 ) { + $this->set_failure_count( 0 ); + $this->save(); + } else { + $this->failed_delivery(); + } + } + + /** + * 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.0 + */ + private function failed_delivery() { + $failures = $this->get_failure_count(); + + if ( $failures > apply_filters( 'woocommerce_max_webhook_delivery_failures', 5 ) ) { + $this->set_status( 'disabled' ); + + do_action( 'woocommerce_webhook_disabled_due_delivery_failures', $this->get_id() ); + } else { + $this->set_failure_count( ++$failures ); + } + + $this->save(); + } + + /** + * Get the delivery logs for this webhook. + * + * @since 3.3.0 + * @return string + */ + public function get_delivery_logs() { + return esc_url( add_query_arg( 'log_file', wc_get_log_file_name( 'webhooks-delivery' ), admin_url( 'admin.php?page=wc-status&tab=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 + * @deprecated 3.3.0 + * @param int $delivery_id Delivery ID. + * @return void + */ + public function get_delivery_log( $delivery_id ) { + wc_deprecated_function( 'WC_Webhook::get_delivery_log', '3.3' ); + } + + /** + * Send a test ping to the delivery URL, sent when the webhook is first created. + * + * @since 2.2.0 + * @return bool|WP_Error + */ + public function deliver_ping() { + $args = array( + 'user-agent' => sprintf( 'WooCommerce/%s Hookshot (WordPress/%s)', Constants::get_constant( 'WC_VERSION' ), $GLOBALS['wp_version'] ), + 'body' => 'webhook_id=' . $this->get_id(), + ); + + $test = wp_safe_remote_post( $this->get_delivery_url(), $args ); + $response_code = wp_remote_retrieve_response_code( $test ); + + if ( is_wp_error( $test ) ) { + /* translators: error message */ + return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL cannot be reached: %s', 'woocommerce' ), $test->get_error_message() ) ); + } + + if ( 200 !== $response_code ) { + /* translators: error message */ + return new WP_Error( 'error', sprintf( __( 'Error: Delivery URL returned response code: %s', 'woocommerce' ), absint( $response_code ) ) ); + } + + $this->set_pending_delivery( false ); + $this->save(); + + return true; + } + + /* + |-------------------------------------------------------------------------- + | Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get the friendly name for the webhook. + * + * @since 2.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return string + */ + public function get_name( $context = 'view' ) { + return apply_filters( 'woocommerce_webhook_name', $this->get_prop( 'name', $context ), $this->get_id() ); + } + + /** + * 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.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return string status + */ + public function get_status( $context = 'view' ) { + return apply_filters( 'woocommerce_webhook_status', $this->get_prop( 'status', $context ), $this->get_id() ); + } + + /** + * Get webhook created date. + * + * @since 3.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_date_created( $context = 'view' ) { + return $this->get_prop( 'date_created', $context ); + } + + /** + * Get webhook modified date. + * + * @since 3.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return WC_DateTime|null Object if the date is set or null if there is no date. + */ + public function get_date_modified( $context = 'view' ) { + return $this->get_prop( 'date_modified', $context ); + } + + /** + * Get the secret used for generating the HMAC-SHA256 signature. + * + * @since 2.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return string + */ + public function get_secret( $context = 'view' ) { + return apply_filters( 'woocommerce_webhook_secret', $this->get_prop( 'secret', $context ), $this->get_id() ); + } + + /** + * Get the webhook topic, e.g. `order.created`. + * + * @since 2.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return string + */ + public function get_topic( $context = 'view' ) { + return apply_filters( 'woocommerce_webhook_topic', $this->get_prop( 'topic', $context ), $this->get_id() ); + } + + /** + * Get the delivery URL. + * + * @since 2.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return string + */ + public function get_delivery_url( $context = 'view' ) { + return apply_filters( 'woocommerce_webhook_delivery_url', $this->get_prop( 'delivery_url', $context ), $this->get_id() ); + } + + /** + * Get the user ID for this webhook. + * + * @since 2.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return int + */ + public function get_user_id( $context = 'view' ) { + return $this->get_prop( 'user_id', $context ); + } + + /** + * API version. + * + * @since 3.0.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return string + */ + public function get_api_version( $context = 'view' ) { + $version = $this->get_prop( 'api_version', $context ); + + return 0 < $version ? 'wp_api_v' . $version : 'legacy_v3'; + } + + /** + * Get the failure count. + * + * @since 2.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return int + */ + public function get_failure_count( $context = 'view' ) { + return $this->get_prop( 'failure_count', $context ); + } + + /** + * Get pending delivery. + * + * @since 3.2.0 + * @param string $context What the value is for. + * Valid values are 'view' and 'edit'. + * @return bool + */ + public function get_pending_delivery( $context = 'view' ) { + return $this->get_prop( 'pending_delivery', $context ); + } + + /* + |-------------------------------------------------------------------------- + | Setters + |-------------------------------------------------------------------------- + */ + + /** + * Set webhook name. + * + * @since 3.2.0 + * @param string $name Webhook name. + */ + public function set_name( $name ) { + $this->set_prop( 'name', $name ); + } + + /** + * Set webhook created date. + * + * @since 3.2.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. + * If the DateTime string has no timezone or offset, + * WordPress site timezone will be assumed. + * Null if their is no date. + */ + public function set_date_created( $date = null ) { + $this->set_date_prop( 'date_created', $date ); + } + + /** + * Set webhook modified date. + * + * @since 3.2.0 + * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. + * If the DateTime string has no timezone or offset, + * WordPress site timezone will be assumed. + * Null if their is no date. + */ + public function set_date_modified( $date = null ) { + $this->set_date_prop( 'date_modified', $date ); + } + + /** + * Set status. + * + * @since 3.2.0 + * @param string $status Status. + */ + public function set_status( $status ) { + if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) { + $status = 'disabled'; + } + + $this->set_prop( 'status', $status ); + } + + /** + * Set the secret used for generating the HMAC-SHA256 signature. + * + * @since 2.2.0 + * @param string $secret Secret. + */ + public function set_secret( $secret ) { + $this->set_prop( 'secret', $secret ); + } + + /** + * Set the webhook topic and associated hooks. + * The topic resource & event are also saved separately. + * + * @since 2.2.0 + * @param string $topic Webhook topic. + */ + public function set_topic( $topic ) { + $topic = wc_clean( $topic ); + + if ( ! wc_is_webhook_valid_topic( $topic ) ) { + $topic = ''; + } + + $this->set_prop( 'topic', $topic ); + } + + /** + * Set the delivery URL. + * + * @since 2.2.0 + * @param string $url Delivery URL. + */ + public function set_delivery_url( $url ) { + $this->set_prop( 'delivery_url', esc_url_raw( $url, array( 'http', 'https' ) ) ); + } + + /** + * Set user ID. + * + * @since 3.2.0 + * @param int $user_id User ID. + */ + public function set_user_id( $user_id ) { + $this->set_prop( 'user_id', (int) $user_id ); + } + + /** + * Set API version. + * + * @since 3.0.0 + * @param int|string $version REST API version. + */ + public function set_api_version( $version ) { + if ( ! is_numeric( $version ) ) { + $version = $this->data_store->get_api_version_number( $version ); + } + + $this->set_prop( 'api_version', (int) $version ); + } + + /** + * Set pending delivery. + * + * @since 3.2.0 + * @param bool $pending_delivery Set true if is pending for delivery. + */ + public function set_pending_delivery( $pending_delivery ) { + $this->set_prop( 'pending_delivery', (bool) $pending_delivery ); + } + + /** + * Set failure count. + * + * @since 3.2.0 + * @param bool $failure_count Total of failures. + */ + public function set_failure_count( $failure_count ) { + $this->set_prop( 'failure_count', intval( $failure_count ) ); + } + + /* + |-------------------------------------------------------------------------- + | Non-CRUD Getters + |-------------------------------------------------------------------------- + */ + + /** + * Get the associated hook names for a topic. + * + * @since 2.2.0 + * @param string $topic Topic name. + * @return array + */ + private function get_topic_hooks( $topic ) { + $topic_hooks = array( + 'coupon.created' => array( + 'woocommerce_process_shop_coupon_meta', + 'woocommerce_new_coupon', + ), + 'coupon.updated' => array( + 'woocommerce_process_shop_coupon_meta', + 'woocommerce_update_coupon', + ), + 'coupon.deleted' => array( + 'wp_trash_post', + ), + 'coupon.restored' => array( + 'untrashed_post', + ), + 'customer.created' => array( + 'user_register', + 'woocommerce_created_customer', + 'woocommerce_new_customer', + ), + 'customer.updated' => array( + 'profile_update', + 'woocommerce_update_customer', + ), + 'customer.deleted' => array( + 'delete_user', + ), + 'order.created' => array( + 'woocommerce_new_order', + ), + 'order.updated' => array( + 'woocommerce_update_order', + 'woocommerce_order_refunded', + ), + 'order.deleted' => array( + 'wp_trash_post', + ), + 'order.restored' => array( + 'untrashed_post', + ), + 'product.created' => array( + 'woocommerce_process_product_meta', + 'woocommerce_new_product', + 'woocommerce_new_product_variation', + ), + 'product.updated' => array( + 'woocommerce_process_product_meta', + 'woocommerce_update_product', + 'woocommerce_update_product_variation', + ), + '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(); + } + + /** + * Get the hook names for the webhook. + * + * @since 2.2.0 + * @return array + */ + public function get_hooks() { + if ( 'action' === $this->get_resource() ) { + $hooks = array( $this->get_event() ); + } else { + $hooks = $this->get_topic_hooks( $this->get_topic() ); + } + + return apply_filters( 'woocommerce_webhook_hooks', $hooks, $this->get_id() ); + } + + /** + * Get the resource for the webhook, e.g. `order`. + * + * @since 2.2.0 + * @return string + */ + public function get_resource() { + $topic = explode( '.', $this->get_topic() ); + + return apply_filters( 'woocommerce_webhook_resource', $topic[0], $this->get_id() ); + } + + /** + * Get the event for the webhook, e.g. `created`. + * + * @since 2.2.0 + * @return string + */ + public function get_event() { + $topic = explode( '.', $this->get_topic() ); + + return apply_filters( 'woocommerce_webhook_event', isset( $topic[1] ) ? $topic[1] : '', $this->get_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; + } +} diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php new file mode 100644 index 0000000..add860b --- /dev/null +++ b/includes/class-woocommerce.php @@ -0,0 +1,1010 @@ +$key(); + } + } + + /** + * WooCommerce Constructor. + */ + public function __construct() { + $this->define_constants(); + $this->define_tables(); + $this->includes(); + $this->init_hooks(); + } + + /** + * When WP has loaded all plugins, trigger the `woocommerce_loaded` hook. + * + * This ensures `woocommerce_loaded` is called only after all other plugins + * are loaded, to avoid issues caused by plugin directory naming changing + * the load order. See #21524 for details. + * + * @since 3.6.0 + */ + public function on_plugins_loaded() { + do_action( 'woocommerce_loaded' ); + } + + /** + * Hook into actions and filters. + * + * @since 2.3 + */ + private function init_hooks() { + register_activation_hook( WC_PLUGIN_FILE, array( 'WC_Install', 'install' ) ); + register_shutdown_function( array( $this, 'log_errors' ) ); + + add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ), -1 ); + add_action( 'admin_notices', array( $this, 'build_dependencies_notice' ) ); + add_action( 'after_setup_theme', array( $this, 'setup_environment' ) ); + add_action( 'after_setup_theme', array( $this, 'include_template_functions' ), 11 ); + add_action( 'init', array( $this, 'init' ), 0 ); + add_action( 'init', array( 'WC_Shortcodes', 'init' ) ); + add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) ); + add_action( 'init', array( $this, 'add_image_sizes' ) ); + add_action( 'init', array( $this, 'load_rest_api' ) ); + add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 ); + add_action( 'activated_plugin', array( $this, 'activated_plugin' ) ); + add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) ); + add_action( 'woocommerce_installed', array( $this, 'add_woocommerce_inbox_variant' ) ); + add_action( 'woocommerce_updated', array( $this, 'add_woocommerce_inbox_variant' ) ); + + // These classes set up hooks on instantiation. + wc_get_container()->get( DownloadPermissionsAdjuster::class ); + wc_get_container()->get( AssignDefaultCategory::class ); + wc_get_container()->get( DataRegenerator::class ); + wc_get_container()->get( LookupDataStore::class ); + wc_get_container()->get( RestockRefundedItemsAdjuster::class ); + } + + /** + * Add woocommerce_inbox_variant for the Remote Inbox Notification. + * + * P2 post can be found at https://wp.me/paJDYF-1uJ. + */ + public function add_woocommerce_inbox_variant() { + $config_name = 'woocommerce_inbox_variant_assignment'; + if ( false === get_option( $config_name, false ) ) { + update_option( $config_name, wp_rand( 1, 12 ) ); + } + } + /** + * Ensures fatal errors are logged so they can be picked up in the status report. + * + * @since 3.2.0 + */ + public function log_errors() { + $error = error_get_last(); + if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ), true ) ) { + $logger = wc_get_logger(); + $logger->critical( + /* translators: 1: error message 2: file name and path 3: line number */ + sprintf( __( '%1$s in %2$s on line %3$s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) . PHP_EOL, + array( + 'source' => 'fatal-errors', + ) + ); + do_action( 'woocommerce_shutdown_error', $error ); + } + } + + /** + * Define WC Constants. + */ + private function define_constants() { + $upload_dir = wp_upload_dir( null, false ); + + $this->define( 'WC_ABSPATH', dirname( WC_PLUGIN_FILE ) . '/' ); + $this->define( 'WC_PLUGIN_BASENAME', plugin_basename( WC_PLUGIN_FILE ) ); + $this->define( 'WC_VERSION', $this->version ); + $this->define( 'WOOCOMMERCE_VERSION', $this->version ); + $this->define( 'WC_ROUNDING_PRECISION', 6 ); + $this->define( 'WC_DISCOUNT_ROUNDING_MODE', 2 ); + $this->define( 'WC_TAX_ROUNDING_MODE', 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? 2 : 1 ); + $this->define( 'WC_DELIMITER', '|' ); + $this->define( 'WC_LOG_DIR', $upload_dir['basedir'] . '/wc-logs/' ); + $this->define( 'WC_SESSION_CACHE_GROUP', 'wc_session_id' ); + $this->define( 'WC_TEMPLATE_DEBUG_MODE', false ); + $this->define( 'WC_NOTICE_MIN_PHP_VERSION', '7.2' ); + $this->define( 'WC_NOTICE_MIN_WP_VERSION', '5.2' ); + $this->define( 'WC_PHP_MIN_REQUIREMENTS_NOTICE', 'wp_php_min_requirements_' . WC_NOTICE_MIN_PHP_VERSION . '_' . WC_NOTICE_MIN_WP_VERSION ); + /** Define if we're checking against major, minor or no versions in the following places: + * - plugin screen in WP Admin (displaying extra warning when updating to new major versions) + * - System Status Report ('Installed version not tested with active version of WooCommerce' warning) + * - core update screen in WP Admin (displaying extra warning when updating to new major versions) + * - enable/disable automated updates in the plugin screen in WP Admin (if there are any plugins + * that don't declare compatibility, the auto-update is disabled) + * + * We dropped SemVer before WC 5.0, so all versions are backwards compatible now, thus no more check needed. + * The SSR in the name is preserved for bw compatibility, as this was initially used in System Status Report. + */ + $this->define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'none' ); + + } + + /** + * Register custom tables within $wpdb object. + */ + private function define_tables() { + global $wpdb; + + // List of tables without prefixes. + $tables = array( + 'payment_tokenmeta' => 'woocommerce_payment_tokenmeta', + 'order_itemmeta' => 'woocommerce_order_itemmeta', + 'wc_product_meta_lookup' => 'wc_product_meta_lookup', + 'wc_tax_rate_classes' => 'wc_tax_rate_classes', + 'wc_reserved_stock' => 'wc_reserved_stock', + ); + + foreach ( $tables as $name => $table ) { + $wpdb->$name = $wpdb->prefix . $table; + $wpdb->tables[] = $table; + } + } + + /** + * Define constant if not already set. + * + * @param string $name Constant name. + * @param string|bool $value Constant value. + */ + private function define( $name, $value ) { + if ( ! defined( $name ) ) { + define( $name, $value ); + } + } + + /** + * Returns true if the request is a non-legacy REST API request. + * + * Legacy REST requests should still run some extra code for backwards compatibility. + * + * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061. + * + * @return bool + */ + public function is_rest_api_request() { + if ( empty( $_SERVER['REQUEST_URI'] ) ) { + return false; + } + + $rest_prefix = trailingslashit( rest_get_url_prefix() ); + $is_rest_api_request = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request ); + } + + /** + * Load REST API. + */ + public function load_rest_api() { + \Automattic\WooCommerce\RestApi\Server::instance()->init(); + } + + /** + * What type of request is this? + * + * @param string $type admin, ajax, cron or frontend. + * @return bool + */ + private function is_request( $type ) { + switch ( $type ) { + case 'admin': + return is_admin(); + case 'ajax': + return defined( 'DOING_AJAX' ); + case 'cron': + return defined( 'DOING_CRON' ); + case 'frontend': + return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' ) && ! $this->is_rest_api_request(); + } + } + + /** + * Include required core files used in admin and on the frontend. + */ + public function includes() { + /** + * Class autoloader. + */ + include_once WC_ABSPATH . 'includes/class-wc-autoloader.php'; + + /** + * Interfaces. + */ + include_once WC_ABSPATH . 'includes/interfaces/class-wc-abstract-order-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-coupon-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-customer-download-log-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-object-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-product-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-item-type-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-order-refund-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-payment-token-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-product-variable-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-shipping-zone-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-logger-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-log-handler-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-webhooks-data-store-interface.php'; + include_once WC_ABSPATH . 'includes/interfaces/class-wc-queue-interface.php'; + + /** + * Core traits. + */ + include_once WC_ABSPATH . 'includes/traits/trait-wc-item-totals.php'; + + /** + * Abstract classes. + */ + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-data.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-object-query.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-token.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-product.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-order.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-settings-api.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-shipping-method.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-payment-gateway.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-integration.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-log-handler.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-deprecated-hooks.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-session.php'; + include_once WC_ABSPATH . 'includes/abstracts/abstract-wc-privacy.php'; + + /** + * Core classes. + */ + include_once WC_ABSPATH . 'includes/wc-core-functions.php'; + include_once WC_ABSPATH . 'includes/class-wc-datetime.php'; + include_once WC_ABSPATH . 'includes/class-wc-post-types.php'; + include_once WC_ABSPATH . 'includes/class-wc-install.php'; + include_once WC_ABSPATH . 'includes/class-wc-geolocation.php'; + include_once WC_ABSPATH . 'includes/class-wc-download-handler.php'; + include_once WC_ABSPATH . 'includes/class-wc-comments.php'; + include_once WC_ABSPATH . 'includes/class-wc-post-data.php'; + include_once WC_ABSPATH . 'includes/class-wc-ajax.php'; + include_once WC_ABSPATH . 'includes/class-wc-emails.php'; + include_once WC_ABSPATH . 'includes/class-wc-data-exception.php'; + include_once WC_ABSPATH . 'includes/class-wc-query.php'; + include_once WC_ABSPATH . 'includes/class-wc-meta-data.php'; + include_once WC_ABSPATH . 'includes/class-wc-order-factory.php'; + include_once WC_ABSPATH . 'includes/class-wc-order-query.php'; + include_once WC_ABSPATH . 'includes/class-wc-product-factory.php'; + include_once WC_ABSPATH . 'includes/class-wc-product-query.php'; + include_once WC_ABSPATH . 'includes/class-wc-payment-tokens.php'; + include_once WC_ABSPATH . 'includes/class-wc-shipping-zone.php'; + include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-cc.php'; + include_once WC_ABSPATH . 'includes/gateways/class-wc-payment-gateway-echeck.php'; + include_once WC_ABSPATH . 'includes/class-wc-countries.php'; + include_once WC_ABSPATH . 'includes/class-wc-integrations.php'; + include_once WC_ABSPATH . 'includes/class-wc-cache-helper.php'; + include_once WC_ABSPATH . 'includes/class-wc-https.php'; + include_once WC_ABSPATH . 'includes/class-wc-deprecated-action-hooks.php'; + include_once WC_ABSPATH . 'includes/class-wc-deprecated-filter-hooks.php'; + include_once WC_ABSPATH . 'includes/class-wc-background-emailer.php'; + include_once WC_ABSPATH . 'includes/class-wc-discounts.php'; + include_once WC_ABSPATH . 'includes/class-wc-cart-totals.php'; + include_once WC_ABSPATH . 'includes/customizer/class-wc-shop-customizer.php'; + include_once WC_ABSPATH . 'includes/class-wc-regenerate-images.php'; + include_once WC_ABSPATH . 'includes/class-wc-privacy.php'; + include_once WC_ABSPATH . 'includes/class-wc-structured-data.php'; + include_once WC_ABSPATH . 'includes/class-wc-shortcodes.php'; + include_once WC_ABSPATH . 'includes/class-wc-logger.php'; + include_once WC_ABSPATH . 'includes/queue/class-wc-action-queue.php'; + include_once WC_ABSPATH . 'includes/queue/class-wc-queue.php'; + include_once WC_ABSPATH . 'includes/admin/marketplace-suggestions/class-wc-marketplace-updater.php'; + include_once WC_ABSPATH . 'includes/blocks/class-wc-blocks-utils.php'; + + /** + * Data stores - used to store and retrieve CRUD object data from the database. + */ + include_once WC_ABSPATH . 'includes/class-wc-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-data-store-wp.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-coupon-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-grouped-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variable-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-product-variation-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-item-type-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-coupon-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-fee-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-product-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-shipping-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-item-tax-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-payment-token-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-data-store-session.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-customer-download-log-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-shipping-zone-data-store.php'; + include_once WC_ABSPATH . 'includes/data-stores/abstract-wc-order-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-order-refund-data-store-cpt.php'; + include_once WC_ABSPATH . 'includes/data-stores/class-wc-webhook-data-store.php'; + + /** + * REST API. + */ + include_once WC_ABSPATH . 'includes/legacy/class-wc-legacy-api.php'; + include_once WC_ABSPATH . 'includes/class-wc-api.php'; + include_once WC_ABSPATH . 'includes/class-wc-rest-authentication.php'; + include_once WC_ABSPATH . 'includes/class-wc-rest-exception.php'; + include_once WC_ABSPATH . 'includes/class-wc-auth.php'; + include_once WC_ABSPATH . 'includes/class-wc-register-wp-admin-settings.php'; + + /** + * WCCOM Site. + */ + include_once WC_ABSPATH . 'includes/wccom-site/class-wc-wccom-site.php'; + + /** + * Libraries and packages. + */ + include_once WC_ABSPATH . 'packages/action-scheduler/action-scheduler.php'; + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + include_once WC_ABSPATH . 'includes/class-wc-cli.php'; + } + + if ( $this->is_request( 'admin' ) ) { + include_once WC_ABSPATH . 'includes/admin/class-wc-admin.php'; + } + + if ( $this->is_request( 'frontend' ) ) { + $this->frontend_includes(); + } + + if ( $this->is_request( 'cron' ) && 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) ) { + include_once WC_ABSPATH . 'includes/class-wc-tracker.php'; + } + + $this->theme_support_includes(); + $this->query = new WC_Query(); + $this->api = new WC_API(); + $this->api->init(); + } + + /** + * Include classes for theme support. + * + * @since 3.3.0 + */ + private function theme_support_includes() { + if ( wc_is_wp_default_theme_active() ) { + switch ( get_template() ) { + case 'twentyten': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-ten.php'; + break; + case 'twentyeleven': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-eleven.php'; + break; + case 'twentytwelve': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twelve.php'; + break; + case 'twentythirteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-thirteen.php'; + break; + case 'twentyfourteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fourteen.php'; + break; + case 'twentyfifteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-fifteen.php'; + break; + case 'twentysixteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-sixteen.php'; + break; + case 'twentyseventeen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-seventeen.php'; + break; + case 'twentynineteen': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-nineteen.php'; + break; + case 'twentytwenty': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty.php'; + break; + case 'twentytwentyone': + include_once WC_ABSPATH . 'includes/theme-support/class-wc-twenty-twenty-one.php'; + break; + } + } + } + + /** + * Include required frontend files. + */ + public function frontend_includes() { + include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; + include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; + include_once WC_ABSPATH . 'includes/wc-template-hooks.php'; + include_once WC_ABSPATH . 'includes/class-wc-template-loader.php'; + include_once WC_ABSPATH . 'includes/class-wc-frontend-scripts.php'; + include_once WC_ABSPATH . 'includes/class-wc-form-handler.php'; + include_once WC_ABSPATH . 'includes/class-wc-cart.php'; + include_once WC_ABSPATH . 'includes/class-wc-tax.php'; + include_once WC_ABSPATH . 'includes/class-wc-shipping-zones.php'; + include_once WC_ABSPATH . 'includes/class-wc-customer.php'; + include_once WC_ABSPATH . 'includes/class-wc-embed.php'; + include_once WC_ABSPATH . 'includes/class-wc-session-handler.php'; + } + + /** + * Function used to Init WooCommerce Template Functions - This makes them pluggable by plugins and themes. + */ + public function include_template_functions() { + include_once WC_ABSPATH . 'includes/wc-template-functions.php'; + } + + /** + * Init WooCommerce when WordPress Initialises. + */ + public function init() { + // Before init action. + do_action( 'before_woocommerce_init' ); + + // Set up localisation. + $this->load_plugin_textdomain(); + + // Load class instances. + $this->product_factory = new WC_Product_Factory(); + $this->order_factory = new WC_Order_Factory(); + $this->countries = new WC_Countries(); + $this->integrations = new WC_Integrations(); + $this->structured_data = new WC_Structured_Data(); + $this->deprecated_hook_handlers['actions'] = new WC_Deprecated_Action_Hooks(); + $this->deprecated_hook_handlers['filters'] = new WC_Deprecated_Filter_Hooks(); + + // Classes/actions loaded for the frontend and for ajax requests. + if ( $this->is_request( 'frontend' ) ) { + wc_load_cart(); + } + + $this->load_webhooks(); + + // Init action. + do_action( 'woocommerce_init' ); + } + + /** + * Load Localisation files. + * + * Note: the first-loaded translation file overrides any following ones if the same translation is present. + * + * Locales found in: + * - WP_LANG_DIR/woocommerce/woocommerce-LOCALE.mo + * - WP_LANG_DIR/plugins/woocommerce-LOCALE.mo + */ + public function load_plugin_textdomain() { + $locale = determine_locale(); + $locale = apply_filters( 'plugin_locale', $locale, 'woocommerce' ); + + unload_textdomain( 'woocommerce' ); + load_textdomain( 'woocommerce', WP_LANG_DIR . '/woocommerce/woocommerce-' . $locale . '.mo' ); + load_plugin_textdomain( 'woocommerce', false, plugin_basename( dirname( WC_PLUGIN_FILE ) ) . '/i18n/languages' ); + } + + /** + * Ensure theme and server variable compatibility and setup image sizes. + */ + public function setup_environment() { + /** + * WC_TEMPLATE_PATH constant. + * + * @deprecated 2.2 Use WC()->template_path() instead. + */ + $this->define( 'WC_TEMPLATE_PATH', $this->template_path() ); + + $this->add_thumbnail_support(); + } + + /** + * Ensure post thumbnail support is turned on. + */ + private function add_thumbnail_support() { + if ( ! current_theme_supports( 'post-thumbnails' ) ) { + add_theme_support( 'post-thumbnails' ); + } + add_post_type_support( 'product', 'thumbnail' ); + } + + /** + * Add WC Image sizes to WP. + * + * As of 3.3, image sizes can be registered via themes using add_theme_support for woocommerce + * and defining an array of args. If these are not defined, we will use defaults. This is + * handled in wc_get_image_size function. + * + * 3.3 sizes: + * + * woocommerce_thumbnail - Used in product listings. We assume these work for a 3 column grid layout. + * woocommerce_single - Used on single product pages for the main image. + * + * @since 2.3 + */ + public function add_image_sizes() { + $thumbnail = wc_get_image_size( 'thumbnail' ); + $single = wc_get_image_size( 'single' ); + $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); + + add_image_size( 'woocommerce_thumbnail', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); + add_image_size( 'woocommerce_single', $single['width'], $single['height'], $single['crop'] ); + add_image_size( 'woocommerce_gallery_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); + + /** + * Legacy image sizes. + * + * @deprecated 3.3.0 These sizes will be removed in 4.6.0. + */ + add_image_size( 'shop_catalog', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] ); + add_image_size( 'shop_single', $single['width'], $single['height'], $single['crop'] ); + add_image_size( 'shop_thumbnail', $gallery_thumbnail['width'], $gallery_thumbnail['height'], $gallery_thumbnail['crop'] ); + } + + /** + * Get the plugin url. + * + * @return string + */ + public function plugin_url() { + return untrailingslashit( plugins_url( '/', WC_PLUGIN_FILE ) ); + } + + /** + * Get the plugin path. + * + * @return string + */ + public function plugin_path() { + return untrailingslashit( plugin_dir_path( WC_PLUGIN_FILE ) ); + } + + /** + * Get the template path. + * + * @return string + */ + public function template_path() { + return apply_filters( 'woocommerce_template_path', 'woocommerce/' ); + } + + /** + * Get Ajax URL. + * + * @return string + */ + public function ajax_url() { + return admin_url( 'admin-ajax.php', 'relative' ); + } + + /** + * Return the WC API URL for a given request. + * + * @param string $request Requested endpoint. + * @param bool|null $ssl If should use SSL, null if should auto detect. Default: null. + * @return string + */ + public function api_request_url( $request, $ssl = null ) { + if ( is_null( $ssl ) ) { + $scheme = wp_parse_url( home_url(), PHP_URL_SCHEME ); + } elseif ( $ssl ) { + $scheme = 'https'; + } else { + $scheme = 'http'; + } + + if ( strstr( get_option( 'permalink_structure' ), '/index.php/' ) ) { + $api_request_url = trailingslashit( home_url( '/index.php/wc-api/' . $request, $scheme ) ); + } elseif ( get_option( 'permalink_structure' ) ) { + $api_request_url = trailingslashit( home_url( '/wc-api/' . $request, $scheme ) ); + } else { + $api_request_url = add_query_arg( 'wc-api', $request, trailingslashit( home_url( '', $scheme ) ) ); + } + + return esc_url_raw( apply_filters( 'woocommerce_api_request_url', $api_request_url, $request, $ssl ) ); + } + + /** + * Load & enqueue active webhooks. + * + * @since 2.2 + */ + private function load_webhooks() { + + if ( ! is_blog_installed() ) { + return; + } + + /** + * Hook: woocommerce_load_webhooks_limit. + * + * @since 3.6.0 + * @param int $limit Used to limit how many webhooks are loaded. Default: no limit. + */ + $limit = apply_filters( 'woocommerce_load_webhooks_limit', null ); + + wc_load_webhooks( 'active', $limit ); + } + + /** + * Initialize the customer and cart objects and setup customer saving on shutdown. + * + * @since 3.6.4 + * @return void + */ + public function initialize_cart() { + // Cart needs customer info. + if ( is_null( $this->customer ) || ! $this->customer instanceof WC_Customer ) { + $this->customer = new WC_Customer( get_current_user_id(), true ); + // Customer should be saved during shutdown. + add_action( 'shutdown', array( $this->customer, 'save' ), 10 ); + } + if ( is_null( $this->cart ) || ! $this->cart instanceof WC_Cart ) { + $this->cart = new WC_Cart(); + } + } + + /** + * Initialize the session class. + * + * @since 3.6.4 + * @return void + */ + public function initialize_session() { + // Session class, handles session data for users - can be overwritten if custom handler is needed. + $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); + if ( is_null( $this->session ) || ! $this->session instanceof $session_class ) { + $this->session = new $session_class(); + $this->session->init(); + } + } + + /** + * Set tablenames inside WPDB object. + */ + public function wpdb_table_fix() { + $this->define_tables(); + } + + /** + * Ran when any plugin is activated. + * + * @since 3.6.0 + * @param string $filename The filename of the activated plugin. + */ + public function activated_plugin( $filename ) { + include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; + + if ( '/woocommerce.php' === substr( $filename, -16 ) ) { + set_transient( 'woocommerce_activated_plugin', $filename ); + } + + WC_Helper::activated_plugin( $filename ); + } + + /** + * Ran when any plugin is deactivated. + * + * @since 3.6.0 + * @param string $filename The filename of the deactivated plugin. + */ + public function deactivated_plugin( $filename ) { + include_once dirname( __FILE__ ) . '/admin/helper/class-wc-helper.php'; + + WC_Helper::deactivated_plugin( $filename ); + } + + /** + * Get queue instance. + * + * @return WC_Queue_Interface + */ + public function queue() { + return WC_Queue::instance(); + } + + /** + * Get Checkout Class. + * + * @return WC_Checkout + */ + public function checkout() { + return WC_Checkout::instance(); + } + + /** + * Get gateways class. + * + * @return WC_Payment_Gateways + */ + public function payment_gateways() { + return WC_Payment_Gateways::instance(); + } + + /** + * Get shipping class. + * + * @return WC_Shipping + */ + public function shipping() { + return WC_Shipping::instance(); + } + + /** + * Email Class. + * + * @return WC_Emails + */ + public function mailer() { + return WC_Emails::instance(); + } + + /** + * Check if plugin assets are built and minified + * + * @return bool + */ + public function build_dependencies_satisfied() { + // Check if we have compiled CSS. + if ( ! file_exists( WC()->plugin_path() . '/assets/css/admin.css' ) ) { + return false; + } + + // Check if we have minified JS. + if ( ! file_exists( WC()->plugin_path() . '/assets/js/admin/woocommerce_admin.min.js' ) ) { + return false; + } + + return true; + } + + /** + * Output a admin notice when build dependencies not met. + * + * @return void + */ + public function build_dependencies_notice() { + if ( $this->build_dependencies_satisfied() ) { + return; + } + + $message_one = __( 'You have installed a development version of WooCommerce which requires files to be built and minified. From the plugin directory, run grunt assets to build and minify assets.', 'woocommerce' ); + $message_two = sprintf( + /* translators: 1: URL of WordPress.org Repository 2: URL of the GitHub Repository release page */ + __( 'Or you can download a pre-built version of the plugin from the WordPress.org repository or by visiting the releases page in the GitHub repository.', 'woocommerce' ), + 'https://wordpress.org/plugins/woocommerce/', + 'https://github.com/woocommerce/woocommerce/releases' + ); + printf( '

    %s %s

    ', $message_one, $message_two ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Is the WooCommerce Admin actively included in the WooCommerce core? + * Based on presence of a basic WC Admin function. + * + * @return boolean + */ + public function is_wc_admin_active() { + return function_exists( 'wc_admin_url' ); + } + + /** + * Call a user function. This should be used to execute any non-idempotent function, especially + * those in the `includes` directory or provided by WordPress. + * + * This method can be useful for unit tests, since functions called using this method + * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks. + * + * @param string $function_name The function to execute. + * @param mixed ...$parameters The parameters to pass to the function. + * + * @return mixed The result from the function. + * + * @since 4.4 + */ + public function call_function( $function_name, ...$parameters ) { + return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters ); + } + + /** + * Call a static method in a class. This should be used to execute any non-idempotent method in classes + * from the `includes` directory. + * + * This method can be useful for unit tests, since methods called using this method + * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks. + * + * @param string $class_name The name of the class containing the method. + * @param string $method_name The name of the method. + * @param mixed ...$parameters The parameters to pass to the method. + * + * @return mixed The result from the method. + * + * @since 4.4 + */ + public function call_static( $class_name, $method_name, ...$parameters ) { + return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters ); + } + + /** + * Gets an instance of a given legacy class. + * This must not be used to get instances of classes in the `src` directory. + * + * This method can be useful for unit tests, since objects obtained using this method + * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks. + * + * @param string $class_name The name of the class to get an instance for. + * @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method. + * + * @return object The instance of the class. + * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class. + * + * @since 4.4 + */ + public function get_instance_of( string $class_name, ...$args ) { + return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args ); + } +} diff --git a/includes/cli/class-wc-cli-rest-command.php b/includes/cli/class-wc-cli-rest-command.php new file mode 100644 index 0000000..f4e2239 --- /dev/null +++ b/includes/cli/class-wc-cli-rest-command.php @@ -0,0 +1,467 @@ + desc). + * + * @var array + */ + 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, true ) ) { + $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 List of supported IDs. + */ + public function set_supported_ids( $supported_ids = array() ) { + $this->supported_ids = $supported_ids; + } + + /** + * Returns 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 WP-CLI positional arguments. + * @param array $assoc_args WP-CLI associative arguments. + */ + 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 WP-CLI positional arguments. + * @param array $assoc_args WP-CLI associative arguments. + */ + public function delete_item( $args, $assoc_args ) { + list( $status, $body ) = $this->do_request( 'DELETE', $this->get_filled_route( $args ), $assoc_args ); + $object_id = isset( $body['id'] ) ? $body['id'] : ''; + if ( ! $object_id && isset( $body['slug'] ) ) { + $object_id = $body['slug']; + } + + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'porcelain' ) ) { + WP_CLI::line( $object_id ); + } else { + if ( empty( $assoc_args['force'] ) ) { + WP_CLI::success( __( 'Trashed', 'woocommerce' ) . " {$this->name} {$object_id}" ); + } else { + WP_CLI::success( __( 'Deleted', 'woocommerce' ) . " {$this->name} {$object_id}." ); + } + } + } + + /** + * Get a single item. + * + * @subcommand get + * + * @param array $args WP-CLI positional arguments. + * @param array $assoc_args WP-CLI associative arguments. + */ + 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 ( empty( $assoc_args['format'] ) ) { + $assoc_args['format'] = 'table'; + } + + if ( 'headers' === $assoc_args['format'] ) { + echo wp_json_encode( $headers ); + } elseif ( 'body' === $assoc_args['format'] ) { + echo wp_json_encode( $body ); + } elseif ( 'envelope' === $assoc_args['format'] ) { + echo wp_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 WP-CLI positional arguments. + * @param array $assoc_args WP-CLI associative arguments. + */ + public function list_items( $args, $assoc_args ) { + if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { + $method = 'HEAD'; + } else { + $method = 'GET'; + } + + if ( ! isset( $assoc_args['per_page'] ) || empty( $assoc_args['per_page'] ) ) { + $assoc_args['per_page'] = '100'; + } + + 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'] ) ) { + $assoc_args['format'] = 'table'; + } + + if ( ! empty( $assoc_args['format'] ) && 'count' === $assoc_args['format'] ) { + echo (int) $headers['X-WP-Total']; + } elseif ( 'headers' === $assoc_args['format'] ) { + echo wp_json_encode( $headers ); + } elseif ( 'body' === $assoc_args['format'] ) { + echo wp_json_encode( $body ); + } elseif ( 'envelope' === $assoc_args['format'] ) { + echo wp_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 WP-CLI positional arguments. + * @param array $assoc_args WP-CLI associative arguments. + */ + 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 Request method. Examples: 'POST', 'PUT', 'DELETE' or 'GET'. + * @param string $route Resource route. + * @param array $assoc_args Associative arguments passed to the originating WP-CLI command. + * + * @return array + */ + private function do_request( $method, $route, $assoc_args ) { + wc_maybe_define_constant( 'REST_REQUEST', true ); + + $request = new WP_REST_Request( $method, $route ); + if ( in_array( $method, array( 'POST', 'PUT' ), true ) ) { + $request->set_body_params( $assoc_args ); + } else { + foreach ( $assoc_args as $key => $value ) { + $request->set_param( $key, $value ); + } + } + if ( Constants::is_true( 'SAVEQUERIES' ) ) { + $original_queries = is_array( $GLOBALS['wpdb']->queries ) ? array_keys( $GLOBALS['wpdb']->queries ) : array(); + } + $response = rest_do_request( $request ); + if ( Constants::is_true( 'SAVEQUERIES' ) ) { + $performed_queries = array(); + foreach ( (array) $GLOBALS['wpdb']->queries as $key => $query ) { + if ( in_array( $key, $original_queries, true ) ) { + 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 = NumberUtil::round( $query[1], 6 ); + $slow_query_message .= <<as_error(); + + if ( $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 Scope under which the request is made. Determines fields present in response. + * @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'], true ) ) { + $fields[] = $key; + } + } + return $fields; + } + + /** + * Get the route for this resource + * + * @param array $args Positional arguments passed to the originating WP-CLI command. + * @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 ( 'id' !== $id_name && strpos( $route, '<' . $id_name . '>' ) !== false && ! empty( $args ) ) { + $route = str_replace( array( '(?P<' . $id_name . '>[\d]+)', '(?P<' . $id_name . '>\w[\w\s\-]*)' ), $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 ); + } + + /** + * Reduce an item to specific fields. + * + * @param array $item Item to reduce. + * @param array $fields Fields to keep. + * @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, true ) ) { + 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 Array that will be scanned for JSON encoded values. + * + * @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 0000000..0804fb9 --- /dev/null +++ b/includes/cli/class-wc-cli-runner.php @@ -0,0 +1,254 @@ +[\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. + * + * @var string + */ + 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'] ) ) { + /* translators: %s: Route to a given WC-API endpoint */ + 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, true ) ) { + 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 available from a route. + * + * @param string $rest_command WC-API command. + * @param string $route Path to route endpoint. + * @param array $route_data Command data. + * @param array $command_args WP-CLI command arguments. + */ + 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( + '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' ), + 'instance_id' => __( 'Instance ID.', 'woocommerce' ), + 'id' => __( 'The ID for the resource.', 'woocommerce' ), + 'slug' => __( 'The slug for the resource.', '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'], true ) && $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; + } + } + + foreach ( $endpoint_args as $name => $args ) { + if ( ! in_array( $name, $positional_args, true ) || strpos( $route, '<' . $id_name . '>' ) === false ) { + $arg_regs[] = array( + 'name' => $name, + 'type' => 'assoc', + 'description' => ! empty( $args['description'] ) ? $args['description'] : '', + 'optional' => empty( $args['required'] ), + ); + } + } + + foreach ( $arg_regs as $arg_reg ) { + $synopsis[] = $arg_reg; + } + + if ( in_array( $command, array( 'list', 'get' ), true ) ) { + $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' ), true ) ) { + $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() { + wc_maybe_define_constant( '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 0000000..d2d3bd1 --- /dev/null +++ b/includes/cli/class-wc-cli-tool-command.php @@ -0,0 +1,107 @@ +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/v2/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/v2/system_status/tools'; + } + + $before_invoke = null; + if ( empty( $command_args['when'] ) && WP_CLI::get_config( 'debug' ) ) { + $before_invoke = function() { + wc_maybe_define_constant( '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-tracker-command.php b/includes/cli/class-wc-cli-tracker-command.php new file mode 100644 index 0000000..d5b8155 --- /dev/null +++ b/includes/cli/class-wc-cli-tracker-command.php @@ -0,0 +1,55 @@ +] + * : Render output in a particular format, see WP_CLI\Formatter for details. + * + * @see \WP_CLI\Formatter + * @see WC_Tracker::get_tracking_data() + * @param array $args WP-CLI positional arguments. + * @param array $assoc_args WP-CLI associative arguments. + */ + public static function show_tracker_snapshot( $args, $assoc_args ) { + $snapshot_data = WC_Tracker::get_tracking_data(); + + $formatter = new \WP_CLI\Formatter( + $assoc_args, + array_keys( $snapshot_data ) + ); + + $formatter->display_items( array( $snapshot_data ) ); + } +} diff --git a/includes/cli/class-wc-cli-update-command.php b/includes/cli/class-wc-cli-update-command.php new file mode 100644 index 0000000..25473b0 --- /dev/null +++ b/includes/cli/class-wc-cli-update-command.php @@ -0,0 +1,81 @@ +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; + $callbacks = WC_Install::get_db_update_callbacks(); + $callbacks_to_run = array(); + + foreach ( $callbacks as $version => $update_callbacks ) { + if ( version_compare( $current_db_version, $version, '<' ) ) { + foreach ( $update_callbacks as $update_callback ) { + $callbacks_to_run[] = $update_callback; + } + } + } + + if ( empty( $callbacks_to_run ) ) { + // Ensure DB version is set to the current WC version to match WP-Admin update routine. + WC_Install::update_db_version(); + /* translators: %s Database version number */ + WP_CLI::success( sprintf( __( 'No updates required. Database version is %s', 'woocommerce' ), get_option( 'woocommerce_db_version' ) ) ); + return; + } + + /* translators: 1: Number of database updates 2: List of update callbacks */ + WP_CLI::log( sprintf( __( 'Found %1$d updates (%2$s)', 'woocommerce' ), count( $callbacks_to_run ), implode( ', ', $callbacks_to_run ) ) ); + + $progress = \WP_CLI\Utils\make_progress_bar( __( 'Updating database', 'woocommerce' ), count( $callbacks_to_run ) ); // phpcs:ignore PHPCompatibility.LanguageConstructs.NewLanguageConstructs.t_ns_separatorFound + + foreach ( $callbacks_to_run as $update_callback ) { + call_user_func( $update_callback ); + $result = false; + while ( $result ) { + $result = (bool) call_user_func( $update_callback ); + } + $update_count ++; + $progress->tick(); + } + + $progress->finish(); + + WC_Admin_Notices::remove_notice( 'update', true ); + + /* translators: 1: Number of database updates performed 2: Database version number */ + WP_CLI::success( sprintf( __( '%1$d update functions completed. Database version is %2$s', 'woocommerce' ), absint( $update_count ), get_option( 'woocommerce_db_version' ) ) ); + } +} diff --git a/includes/customizer/class-wc-customizer-control-cropping.php b/includes/customizer/class-wc-customizer-control-cropping.php new file mode 100644 index 0000000..675f698 --- /dev/null +++ b/includes/customizer/class-wc-customizer-control-cropping.php @@ -0,0 +1,64 @@ +choices ) ) { + return; + } + + $value = $this->value( 'cropping' ); + $custom_width = $this->value( 'custom_width' ); + $custom_height = $this->value( 'custom_height' ); + ?> + + + label ); ?> + + + description ) ) : ?> + description ); ?> + + +
      + choices as $key => $radio ) : ?> +
    • + link( 'cropping' ); ?> /> + + + + + link( 'custom_width' ); ?> /> : link( 'custom_height' ); ?> /> + + +
    • + +
    + add_panel( + 'woocommerce', + array( + 'priority' => 200, + 'capability' => 'manage_woocommerce', + 'theme_supports' => '', + 'title' => __( 'WooCommerce', 'woocommerce' ), + ) + ); + + $this->add_store_notice_section( $wp_customize ); + $this->add_product_catalog_section( $wp_customize ); + $this->add_product_images_section( $wp_customize ); + $this->add_checkout_section( $wp_customize ); + } + + /** + * Frontend CSS styles. + */ + public function add_frontend_scripts() { + if ( ! is_customize_preview() || ! is_store_notice_showing() ) { + return; + } + + $css = '.woocommerce-store-notice, p.demo_store { display: block !important; }'; + wp_add_inline_style( 'customize-preview', $css ); + } + + /** + * CSS styles to improve our form. + */ + public function add_styles() { + ?> + + + + __( 'Default sorting (custom ordering + name)', 'woocommerce' ), + 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), + 'rating' => __( 'Average rating', 'woocommerce' ), + 'date' => __( 'Sort by most recent', 'woocommerce' ), + 'price' => __( 'Sort by price (asc)', 'woocommerce' ), + 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), + ) + ); + + return array_key_exists( $value, $options ) ? $value : 'menu_order'; + } + + /** + * Store notice section. + * + * @param WP_Customize_Manager $wp_customize Theme Customizer object. + */ + private function add_store_notice_section( $wp_customize ) { + $wp_customize->add_section( + 'woocommerce_store_notice', + array( + 'title' => __( 'Store Notice', 'woocommerce' ), + 'priority' => 10, + 'panel' => 'woocommerce', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_demo_store', + array( + 'default' => 'no', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wc_bool_to_string', + 'sanitize_js_callback' => 'wc_string_to_bool', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_demo_store_notice', + array( + 'default' => __( 'This is a demo store for testing purposes — no orders shall be fulfilled.', 'woocommerce' ), + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wp_kses_post', + 'transport' => 'postMessage', + ) + ); + + $wp_customize->add_control( + 'woocommerce_demo_store_notice', + array( + 'label' => __( 'Store notice', 'woocommerce' ), + 'description' => __( 'If enabled, this text will be shown site-wide. You can use it to show events or promotions to visitors!', 'woocommerce' ), + 'section' => 'woocommerce_store_notice', + 'settings' => 'woocommerce_demo_store_notice', + 'type' => 'textarea', + ) + ); + + $wp_customize->add_control( + 'woocommerce_demo_store', + array( + 'label' => __( 'Enable store notice', 'woocommerce' ), + 'section' => 'woocommerce_store_notice', + 'settings' => 'woocommerce_demo_store', + 'type' => 'checkbox', + ) + ); + + if ( isset( $wp_customize->selective_refresh ) ) { + $wp_customize->selective_refresh->add_partial( + 'woocommerce_demo_store_notice', + array( + 'selector' => '.woocommerce-store-notice', + 'container_inclusive' => true, + 'render_callback' => 'woocommerce_demo_store', + ) + ); + } + } + + /** + * Product catalog section. + * + * @param WP_Customize_Manager $wp_customize Theme Customizer object. + */ + public function add_product_catalog_section( $wp_customize ) { + $wp_customize->add_section( + 'woocommerce_product_catalog', + array( + 'title' => __( 'Product Catalog', 'woocommerce' ), + 'priority' => 10, + 'panel' => 'woocommerce', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_shop_page_display', + array( + 'default' => '', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => array( $this, 'sanitize_archive_display' ), + ) + ); + + $wp_customize->add_control( + 'woocommerce_shop_page_display', + array( + 'label' => __( 'Shop page display', 'woocommerce' ), + 'description' => __( 'Choose what to display on the main shop page.', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_shop_page_display', + 'type' => 'select', + 'choices' => array( + '' => __( 'Show products', 'woocommerce' ), + 'subcategories' => __( 'Show categories', 'woocommerce' ), + 'both' => __( 'Show categories & products', 'woocommerce' ), + ), + ) + ); + + $wp_customize->add_setting( + 'woocommerce_category_archive_display', + array( + 'default' => '', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => array( $this, 'sanitize_archive_display' ), + ) + ); + + $wp_customize->add_control( + 'woocommerce_category_archive_display', + array( + 'label' => __( 'Category display', 'woocommerce' ), + 'description' => __( 'Choose what to display on product category pages.', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_category_archive_display', + 'type' => 'select', + 'choices' => array( + '' => __( 'Show products', 'woocommerce' ), + 'subcategories' => __( 'Show subcategories', 'woocommerce' ), + 'both' => __( 'Show subcategories & products', 'woocommerce' ), + ), + ) + ); + + $wp_customize->add_setting( + 'woocommerce_default_catalog_orderby', + array( + 'default' => 'menu_order', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => array( $this, 'sanitize_default_catalog_orderby' ), + ) + ); + + $wp_customize->add_control( + 'woocommerce_default_catalog_orderby', + array( + 'label' => __( 'Default product sorting', 'woocommerce' ), + 'description' => __( 'How should products be sorted in the catalog by default?', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_default_catalog_orderby', + 'type' => 'select', + 'choices' => apply_filters( + 'woocommerce_default_catalog_orderby_options', + array( + 'menu_order' => __( 'Default sorting (custom ordering + name)', 'woocommerce' ), + 'popularity' => __( 'Popularity (sales)', 'woocommerce' ), + 'rating' => __( 'Average rating', 'woocommerce' ), + 'date' => __( 'Sort by most recent', 'woocommerce' ), + 'price' => __( 'Sort by price (asc)', 'woocommerce' ), + 'price-desc' => __( 'Sort by price (desc)', 'woocommerce' ), + ) + ), + ) + ); + + // The following settings should be hidden if the theme is declaring the values. + if ( has_filter( 'loop_shop_columns' ) ) { + return; + } + + $wp_customize->add_setting( + 'woocommerce_catalog_columns', + array( + 'default' => 4, + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + 'woocommerce_catalog_columns', + array( + 'label' => __( 'Products per row', 'woocommerce' ), + 'description' => __( 'How many products should be shown per row?', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_catalog_columns', + 'type' => 'number', + 'input_attrs' => array( + 'min' => wc_get_theme_support( 'product_grid::min_columns', 1 ), + 'max' => wc_get_theme_support( 'product_grid::max_columns', '' ), + 'step' => 1, + ), + ) + ); + + // Only add this setting if something else isn't managing the number of products per page. + if ( ! has_filter( 'loop_shop_per_page' ) ) { + $wp_customize->add_setting( + 'woocommerce_catalog_rows', + array( + 'default' => 4, + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + } + + $wp_customize->add_control( + 'woocommerce_catalog_rows', + array( + 'label' => __( 'Rows per page', 'woocommerce' ), + 'description' => __( 'How many rows of products should be shown per page?', 'woocommerce' ), + 'section' => 'woocommerce_product_catalog', + 'settings' => 'woocommerce_catalog_rows', + 'type' => 'number', + 'input_attrs' => array( + 'min' => wc_get_theme_support( 'product_grid::min_rows', 1 ), + 'max' => wc_get_theme_support( 'product_grid::max_rows', '' ), + 'step' => 1, + ), + ) + ); + } + + /** + * Product images section. + * + * @param WP_Customize_Manager $wp_customize Theme Customizer object. + */ + private function add_product_images_section( $wp_customize ) { + if ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) { + $regen_description = ''; // Nothing to report; Jetpack will handle magically. + } elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && ! is_multisite() ) { + $regen_description = __( 'After publishing your changes, new image sizes will be generated automatically.', 'woocommerce' ); + } elseif ( apply_filters( 'woocommerce_background_image_regeneration', true ) && is_multisite() ) { + /* translators: 1: tools URL 2: regen thumbs url */ + $regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you regenerate thumbnails. You can do this from the tools section in WooCommerce or by using a plugin such as Regenerate Thumbnails.', 'woocommerce' ), admin_url( 'admin.php?page=wc-status&tab=tools' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' ); + } else { + /* translators: %s: regen thumbs url */ + $regen_description = sprintf( __( 'After publishing your changes, new image sizes may not be shown until you Regenerate Thumbnails.', 'woocommerce' ), 'https://en-gb.wordpress.org/plugins/regenerate-thumbnails/' ); + } + + $wp_customize->add_section( + 'woocommerce_product_images', + array( + 'title' => __( 'Product Images', 'woocommerce' ), + 'description' => $regen_description, + 'priority' => 20, + 'panel' => 'woocommerce', + ) + ); + + if ( ! wc_get_theme_support( 'single_image_width' ) ) { + $wp_customize->add_setting( + 'woocommerce_single_image_width', + array( + 'default' => 600, + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + 'woocommerce_single_image_width', + array( + 'label' => __( 'Main image width', 'woocommerce' ), + 'description' => __( 'Image size used for the main image on single product pages. These images will remain uncropped.', 'woocommerce' ), + 'section' => 'woocommerce_product_images', + 'settings' => 'woocommerce_single_image_width', + 'type' => 'number', + 'input_attrs' => array( + 'min' => 0, + 'step' => 1, + ), + ) + ); + } + + if ( ! wc_get_theme_support( 'thumbnail_image_width' ) ) { + $wp_customize->add_setting( + 'woocommerce_thumbnail_image_width', + array( + 'default' => 300, + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + 'woocommerce_thumbnail_image_width', + array( + 'label' => __( 'Thumbnail width', 'woocommerce' ), + 'description' => __( 'Image size used for products in the catalog.', 'woocommerce' ), + 'section' => 'woocommerce_product_images', + 'settings' => 'woocommerce_thumbnail_image_width', + 'type' => 'number', + 'input_attrs' => array( + 'min' => 0, + 'step' => 1, + ), + ) + ); + } + + include_once WC_ABSPATH . 'includes/customizer/class-wc-customizer-control-cropping.php'; + + $wp_customize->add_setting( + 'woocommerce_thumbnail_cropping', + array( + 'default' => '1:1', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wc_clean', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_thumbnail_cropping_custom_width', + array( + 'default' => '4', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_thumbnail_cropping_custom_height', + array( + 'default' => '3', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'absint', + 'sanitize_js_callback' => 'absint', + ) + ); + + $wp_customize->add_control( + new WC_Customizer_Control_Cropping( + $wp_customize, + 'woocommerce_thumbnail_cropping', + array( + 'section' => 'woocommerce_product_images', + 'settings' => array( + 'cropping' => 'woocommerce_thumbnail_cropping', + 'custom_width' => 'woocommerce_thumbnail_cropping_custom_width', + 'custom_height' => 'woocommerce_thumbnail_cropping_custom_height', + ), + 'label' => __( 'Thumbnail cropping', 'woocommerce' ), + 'choices' => array( + '1:1' => array( + 'label' => __( '1:1', 'woocommerce' ), + 'description' => __( 'Images will be cropped into a square', 'woocommerce' ), + ), + 'custom' => array( + 'label' => __( 'Custom', 'woocommerce' ), + 'description' => __( 'Images will be cropped to a custom aspect ratio', 'woocommerce' ), + ), + 'uncropped' => array( + 'label' => __( 'Uncropped', 'woocommerce' ), + 'description' => __( 'Images will display using the aspect ratio in which they were uploaded', 'woocommerce' ), + ), + ), + ) + ) + ); + } + + /** + * Checkout section. + * + * @param WP_Customize_Manager $wp_customize Theme Customizer object. + */ + public function add_checkout_section( $wp_customize ) { + $wp_customize->add_section( + 'woocommerce_checkout', + array( + 'title' => __( 'Checkout', 'woocommerce' ), + 'priority' => 20, + 'panel' => 'woocommerce', + 'description' => __( 'These options let you change the appearance of the WooCommerce checkout.', 'woocommerce' ), + ) + ); + + // Checkout field controls. + $fields = array( + 'company' => __( 'Company name', 'woocommerce' ), + 'address_2' => __( 'Address line 2', 'woocommerce' ), + 'phone' => __( 'Phone', 'woocommerce' ), + ); + foreach ( $fields as $field => $label ) { + $wp_customize->add_setting( + 'woocommerce_checkout_' . $field . '_field', + array( + 'default' => 'phone' === $field ? 'required' : 'optional', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => array( $this, 'sanitize_checkout_field_display' ), + ) + ); + $wp_customize->add_control( + 'woocommerce_checkout_' . $field . '_field', + array( + /* Translators: %s field name. */ + 'label' => sprintf( __( '%s field', 'woocommerce' ), $label ), + 'section' => 'woocommerce_checkout', + 'settings' => 'woocommerce_checkout_' . $field . '_field', + 'type' => 'select', + 'choices' => array( + 'hidden' => __( 'Hidden', 'woocommerce' ), + 'optional' => __( 'Optional', 'woocommerce' ), + 'required' => __( 'Required', 'woocommerce' ), + ), + ) + ); + } + + // Register settings. + $wp_customize->add_setting( + 'woocommerce_checkout_highlight_required_fields', + array( + 'default' => 'yes', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wc_bool_to_string', + 'sanitize_js_callback' => 'wc_string_to_bool', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_checkout_terms_and_conditions_checkbox_text', + array( + /* translators: %s terms and conditions page name and link */ + 'default' => sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ), + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wp_kses_post', + 'transport' => 'postMessage', + ) + ); + + $wp_customize->add_setting( + 'woocommerce_checkout_privacy_policy_text', + array( + /* translators: %s privacy policy page name and link */ + 'default' => sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ), + 'type' => 'option', + 'capability' => 'manage_woocommerce', + 'sanitize_callback' => 'wp_kses_post', + 'transport' => 'postMessage', + ) + ); + + // Register controls. + $wp_customize->add_control( + 'woocommerce_checkout_highlight_required_fields', + array( + 'label' => __( 'Highlight required fields with an asterisk', 'woocommerce' ), + 'section' => 'woocommerce_checkout', + 'settings' => 'woocommerce_checkout_highlight_required_fields', + 'type' => 'checkbox', + ) + ); + + if ( current_user_can( 'manage_privacy_options' ) ) { + $choose_pages = array( + 'wp_page_for_privacy_policy' => __( 'Privacy policy', 'woocommerce' ), + 'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ), + ); + } else { + $choose_pages = array( + 'woocommerce_terms_page_id' => __( 'Terms and conditions', 'woocommerce' ), + ); + } + $pages = get_pages( + array( + 'post_type' => 'page', + 'post_status' => 'publish,private,draft', + 'child_of' => 0, + 'parent' => -1, + 'exclude' => array( + wc_get_page_id( 'cart' ), + wc_get_page_id( 'checkout' ), + wc_get_page_id( 'myaccount' ), + ), + 'sort_order' => 'asc', + 'sort_column' => 'post_title', + ) + ); + $page_choices = array( '' => __( 'No page set', 'woocommerce' ) ) + array_combine( array_map( 'strval', wp_list_pluck( $pages, 'ID' ) ), wp_list_pluck( $pages, 'post_title' ) ); + + foreach ( $choose_pages as $id => $name ) { + $wp_customize->add_setting( + $id, + array( + 'default' => '', + 'type' => 'option', + 'capability' => 'manage_woocommerce', + ) + ); + $wp_customize->add_control( + $id, + array( + /* Translators: %s: page name. */ + 'label' => sprintf( __( '%s page', 'woocommerce' ), $name ), + 'section' => 'woocommerce_checkout', + 'settings' => $id, + 'type' => 'select', + 'choices' => $page_choices, + ) + ); + } + + $wp_customize->add_control( + 'woocommerce_checkout_privacy_policy_text', + array( + 'label' => __( 'Privacy policy', 'woocommerce' ), + 'description' => __( 'Optionally add some text about your store privacy policy to show during checkout.', 'woocommerce' ), + 'section' => 'woocommerce_checkout', + 'settings' => 'woocommerce_checkout_privacy_policy_text', + 'active_callback' => array( $this, 'has_privacy_policy_page_id' ), + 'type' => 'textarea', + ) + ); + + $wp_customize->add_control( + 'woocommerce_checkout_terms_and_conditions_checkbox_text', + array( + 'label' => __( 'Terms and conditions', 'woocommerce' ), + 'description' => __( 'Optionally add some text for the terms checkbox that customers must accept.', 'woocommerce' ), + 'section' => 'woocommerce_checkout', + 'settings' => 'woocommerce_checkout_terms_and_conditions_checkbox_text', + 'active_callback' => array( $this, 'has_terms_and_conditions_page_id' ), + 'type' => 'text', + ) + ); + + if ( isset( $wp_customize->selective_refresh ) ) { + $wp_customize->selective_refresh->add_partial( + 'woocommerce_checkout_privacy_policy_text', + array( + 'selector' => '.woocommerce-privacy-policy-text', + 'container_inclusive' => true, + 'render_callback' => 'wc_checkout_privacy_policy_text', + ) + ); + $wp_customize->selective_refresh->add_partial( + 'woocommerce_checkout_terms_and_conditions_checkbox_text', + array( + 'selector' => '.woocommerce-terms-and-conditions-checkbox-text', + 'container_inclusive' => false, + 'render_callback' => 'wc_terms_and_conditions_checkbox_text', + ) + ); + } + } + + /** + * Sanitize field display. + * + * @param string $value '', 'subcategories', or 'both'. + * @return string + */ + public function sanitize_checkout_field_display( $value ) { + $options = array( 'hidden', 'optional', 'required' ); + return in_array( $value, $options, true ) ? $value : ''; + } + + /** + * Whether or not a page has been chose for the privacy policy. + * + * @return bool + */ + public function has_privacy_policy_page_id() { + return wc_privacy_policy_page_id() > 0; + } + + /** + * Whether or not a page has been chose for the terms and conditions. + * + * @return bool + */ + public function has_terms_and_conditions_page_id() { + return wc_terms_and_conditions_page_id() > 0; + } +} + +new WC_Shop_Customizer(); diff --git a/includes/data-stores/abstract-wc-order-data-store-cpt.php b/includes/data-stores/abstract-wc-order-data-store-cpt.php new file mode 100644 index 0000000..ce37852 --- /dev/null +++ b/includes/data-stores/abstract-wc-order-data-store-cpt.php @@ -0,0 +1,438 @@ +set_version( Constants::get_constant( 'WC_VERSION' ) ); + $order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() ); + if ( ! $order->get_date_created( 'edit' ) ) { + $order->set_date_created( time() ); + } + + $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' => $this->get_post_status( $order ), + 'ping_status' => 'closed', + 'post_author' => 1, + 'post_title' => $this->get_post_title(), + 'post_password' => $this->get_order_key( $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_Order $order Order object. + * + * @throws Exception If passed order is invalid. + */ + public function read( &$order ) { + $order->set_defaults(); + $post_object = get_post( $order->get_id() ); + if ( ! $order->get_id() || ! $post_object || ! in_array( $post_object->post_type, wc_get_order_types(), true ) ) { + throw new Exception( __( 'Invalid order.', 'woocommerce' ) ); + } + + $order->set_props( + array( + 'parent_id' => $post_object->post_parent, + 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), + 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), + 'status' => $post_object->post_status, + ) + ); + + $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( (float) get_post_meta( $order->get_id(), '_cart_discount', true ) - (float) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) ); + } + } + + /** + * Method to update an order in the database. + * + * @param WC_Order $order Order object. + */ + public function update( &$order ) { + $order->save_meta_data(); + $order->set_version( Constants::get_constant( 'WC_VERSION' ) ); + + if ( null === $order->get_date_created( 'edit' ) ) { + $order->set_date_created( time() ); + } + + $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' => $this->get_post_status( $order ), + '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 Order object. + * @param array $args Array of args to pass to the delete method. + * + * @return void + */ + 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 + |-------------------------------------------------------------------------- + */ + + /** + * Get the status to save to the post object. + * + * Plugins extending the order classes can override this to change the stored status/add prefixes etc. + * + * @since 3.6.0 + * @param WC_order $order Order object. + * @return string + */ + protected function get_post_status( $order ) { + $order_status = $order->get_status( 'edit' ); + + if ( ! $order_status ) { + $order_status = apply_filters( 'woocommerce_default_order_status', 'pending' ); + } + + $post_status = $order_status; + $valid_statuses = get_post_stati(); + + // Add a wc- prefix to the status, but exclude some core statuses which should not be prefixed. + // @todo In the future this should only happen based on `wc_is_order_status`, but in order to + // preserve back-compatibility this happens to all statuses except a select few. A doing_it_wrong + // Notice will be needed here, followed by future removal. + if ( ! in_array( $post_status, array( 'auto-draft', 'draft', 'trash' ), true ) && in_array( 'wc-' . $post_status, $valid_statuses, true ) ) { + $post_status = 'wc-' . $post_status; + } + + return $post_status; + } + + /** + * Excerpt for post. + * + * @param WC_order $order Order object. + * @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 + } + + /** + * Get order key. + * + * @since 4.3.0 + * @param WC_order $order Order object. + * @return string + */ + protected function get_order_key( $order ) { + return wc_generate_order_key(); + } + + /** + * Read order data. Can be overridden by child classes to load other props. + * + * @param WC_Order $order Order object. + * @param object $post_object Post object. + * @since 3.0.0 + */ + protected function read_order_data( &$order, $post_object ) { + $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 WC_Order $order Order object. + * @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' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + + if ( 'prices_include_tax' === $prop ) { + $value = $value ? 'yes' : 'no'; + } + + $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); + + if ( $updated ) { + $updated_props[] = $prop; + } + } + + do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); + } + + /** + * Clear any caches. + * + * @param WC_Order $order Order object. + * @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 Order object. + * @param string $type Order item type. + * @return array + */ + public function read_items( $order, $type ) { + global $wpdb; + + // Get from cache if available. + $items = 0 < $order->get_id() ? wp_cache_get( 'order-items-' . $order->get_id(), 'orders' ) : false; + + if ( false === $items ) { + $items = $wpdb->get_results( + $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() ) + ); + foreach ( $items as $item ) { + wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' ); + } + if ( 0 < $order->get_id() ) { + 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 $order Order object. + * @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 $order Order object. + * @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 $order Order object. + * @param array $token_ids Payment 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 0000000..0aa25a6 --- /dev/null +++ b/includes/data-stores/abstract-wc-order-item-type-data-store.php @@ -0,0 +1,166 @@ +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 Order item object. + */ + 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 Order item object. + * @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() ); + $this->clear_cache( $item ); + } + } + + /** + * Read a order item from the database. + * + * @since 3.0.0 + * + * @param WC_Order_Item $item Order item object. + * + * @throws Exception If invalid order item. + */ + 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 Order item object. + */ + public function save_item_data( &$item ) {} + + /** + * Clear meta cache. + * + * @param WC_Order_Item $item Order item object. + */ + public function clear_cache( &$item ) { + wp_cache_delete( 'item-' . $item->get_id(), 'order-items' ); + wp_cache_delete( 'order-items-' . $item->get_order_id(), 'orders' ); + wp_cache_delete( $item->get_id(), $this->meta_type . '_meta' ); + } +} 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 0000000..f0bcbd7 --- /dev/null +++ b/includes/data-stores/class-wc-coupon-data-store-cpt.php @@ -0,0 +1,729 @@ +get_date_created( 'edit' ) ) { + $coupon->set_date_created( time() ); + } + + $coupon_id = wp_insert_post( + apply_filters( + 'woocommerce_new_coupon_data', + array( + 'post_type' => 'shop_coupon', + 'post_status' => 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $coupon->get_code( 'edit' ), + 'post_content' => '', + 'post_excerpt' => $coupon->get_description( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created()->getTimestamp() ), + ) + ), + true + ); + + if ( $coupon_id ) { + $coupon->set_id( $coupon_id ); + $this->update_post_meta( $coupon ); + $coupon->save_meta_data(); + $coupon->apply_changes(); + delete_transient( 'rest_api_coupons_type_count' ); + do_action( 'woocommerce_new_coupon', $coupon_id, $coupon ); + } + } + + /** + * Method to read a coupon. + * + * @since 3.0.0 + * + * @param WC_Coupon $coupon Coupon object. + * + * @throws Exception If invalid coupon. + */ + public function read( &$coupon ) { + $coupon->set_defaults(); + + $post_object = get_post( $coupon->get_id() ); + + if ( ! $coupon->get_id() || ! $post_object || 'shop_coupon' !== $post_object->post_type ) { + throw new Exception( __( 'Invalid coupon.', 'woocommerce' ) ); + } + + $coupon_id = $coupon->get_id(); + $coupon->set_props( + array( + 'code' => $post_object->post_title, + 'description' => $post_object->post_excerpt, + 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), + 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), + 'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine. + 'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ), + 'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ), + 'usage_count' => get_post_meta( $coupon_id, 'usage_count', true ), + 'individual_use' => 'yes' === get_post_meta( $coupon_id, 'individual_use', true ), + 'product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'product_ids', true ) ) ), + 'excluded_product_ids' => array_filter( (array) explode( ',', get_post_meta( $coupon_id, 'exclude_product_ids', true ) ) ), + 'usage_limit' => get_post_meta( $coupon_id, 'usage_limit', true ), + 'usage_limit_per_user' => get_post_meta( $coupon_id, 'usage_limit_per_user', true ), + 'limit_usage_to_x_items' => 0 < get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) ? get_post_meta( $coupon_id, 'limit_usage_to_x_items', true ) : null, + 'free_shipping' => 'yes' === get_post_meta( $coupon_id, 'free_shipping', true ), + 'product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'product_categories', true ) ), + 'excluded_product_categories' => array_filter( (array) get_post_meta( $coupon_id, 'exclude_product_categories', true ) ), + 'exclude_sale_items' => 'yes' === get_post_meta( $coupon_id, 'exclude_sale_items', true ), + 'minimum_amount' => get_post_meta( $coupon_id, 'minimum_amount', true ), + 'maximum_amount' => get_post_meta( $coupon_id, 'maximum_amount', true ), + 'email_restrictions' => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ), + 'used_by' => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ), + ) + ); + $coupon->read_meta_data(); + $coupon->set_object_read( true ); + do_action( 'woocommerce_coupon_loaded', $coupon ); + } + + /** + * Updates a coupon in the database. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + */ + public function update( &$coupon ) { + $coupon->save_meta_data(); + $changes = $coupon->get_changes(); + + if ( array_intersect( array( 'code', 'description', 'date_created', 'date_modified' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_title' => $coupon->get_code( 'edit' ), + 'post_excerpt' => $coupon->get_description( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $coupon->get_date_created( 'edit' )->getTimestamp() ), + 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), + 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $coupon->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), + ); + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $coupon->get_id() ) ); + clean_post_cache( $coupon->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $coupon->get_id() ), $post_data ) ); + } + $coupon->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + } + $this->update_post_meta( $coupon ); + $coupon->apply_changes(); + delete_transient( 'rest_api_coupons_type_count' ); + do_action( 'woocommerce_update_coupon', $coupon->get_id(), $coupon ); + } + + /** + * Deletes a coupon from the database. + * + * @since 3.0.0 + * + * @param WC_Coupon $coupon Coupon object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$coupon, $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'force_delete' => false, + ) + ); + + $id = $coupon->get_id(); + + if ( ! $id ) { + return; + } + + if ( $args['force_delete'] ) { + wp_delete_post( $id ); + + wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $coupon->get_code(), 'coupons' ); + + $coupon->set_id( 0 ); + do_action( 'woocommerce_delete_coupon', $id ); + } else { + wp_trash_post( $id ); + do_action( 'woocommerce_trash_coupon', $id ); + } + } + + /** + * Helper method that updates all the post meta for a coupon based on it's settings in the WC_Coupon class. + * + * @param WC_Coupon $coupon Coupon object. + * @since 3.0.0 + */ + private function update_post_meta( &$coupon ) { + $meta_key_to_props = array( + 'discount_type' => 'discount_type', + 'coupon_amount' => 'amount', + 'individual_use' => 'individual_use', + 'product_ids' => 'product_ids', + 'exclude_product_ids' => 'excluded_product_ids', + 'usage_limit' => 'usage_limit', + 'usage_limit_per_user' => 'usage_limit_per_user', + 'limit_usage_to_x_items' => 'limit_usage_to_x_items', + 'usage_count' => 'usage_count', + 'date_expires' => 'date_expires', + 'free_shipping' => 'free_shipping', + 'product_categories' => 'product_categories', + 'exclude_product_categories' => 'excluded_product_categories', + 'exclude_sale_items' => 'exclude_sale_items', + 'minimum_amount' => 'minimum_amount', + 'maximum_amount' => 'maximum_amount', + 'customer_email' => 'email_restrictions', + ); + + $props_to_update = $this->get_props_to_update( $coupon, $meta_key_to_props ); + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $coupon->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + switch ( $prop ) { + case 'individual_use': + case 'free_shipping': + case 'exclude_sale_items': + $value = wc_bool_to_string( $value ); + break; + case 'product_ids': + case 'excluded_product_ids': + $value = implode( ',', array_filter( array_map( 'intval', $value ) ) ); + break; + case 'product_categories': + case 'excluded_product_categories': + $value = array_filter( array_map( 'intval', $value ) ); + break; + case 'email_restrictions': + $value = array_filter( array_map( 'sanitize_email', $value ) ); + break; + case 'date_expires': + $value = $value ? $value->getTimestamp() : null; + break; + } + + $updated = $this->update_or_delete_post_meta( $coupon, $meta_key, $value ); + + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + do_action( 'woocommerce_coupon_object_updated_props', $coupon, $this->updated_props ); + } + + /** + * Increase usage count for current coupon. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + * @param string $used_by Either user ID or billing email. + * @param WC_Order $order (Optional) If passed, clears the hold record associated with order. + + * @return int New usage count. + */ + public function increase_usage_count( &$coupon, $used_by = '', $order = null ) { + $coupon_held_key_for_user = ''; + if ( $order instanceof WC_Order ) { + $coupon_held_key_for_user = $order->get_data_store()->get_coupon_held_keys_for_users( $order, $coupon->get_id() ); + } + + $new_count = $this->update_usage_count_meta( $coupon, 'increase' ); + + if ( $used_by ) { + $this->add_coupon_used_by( $coupon, $used_by, $coupon_held_key_for_user ); + $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); + } + + do_action( 'woocommerce_increase_coupon_usage_count', $coupon, $new_count, $used_by ); + + return $new_count; + } + + /** + * Helper function to add a `_used_by` record to track coupons used by the user. + * + * @param WC_Coupon $coupon Coupon object. + * @param string $used_by Either user ID or billing email. + * @param string $coupon_held_key (Optional) Update meta key to `_used_by` instead of adding a new record. + */ + private function add_coupon_used_by( $coupon, $used_by, $coupon_held_key ) { + global $wpdb; + if ( $coupon_held_key && '' !== $coupon_held_key ) { + // Looks like we added a tentative record for this coupon getting used. + // Lets change the tentative record to a permanent one. + $result = $wpdb->query( + $wpdb->prepare( + " + UPDATE $wpdb->postmeta SET meta_key = %s, meta_value = %s WHERE meta_key = %s LIMIT 1", + '_used_by', + $used_by, + $coupon_held_key + ) + ); + if ( ! $result ) { + // If no rows were updated, then insert a `_used_by` row manually to maintain consistency. + add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); + } + } else { + add_post_meta( $coupon->get_id(), '_used_by', strtolower( $used_by ) ); + } + } + + /** + * Decrease usage count for current coupon. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + * @param string $used_by Either user ID or billing email. + * @return int New usage count. + */ + public function decrease_usage_count( &$coupon, $used_by = '' ) { + global $wpdb; + $new_count = $this->update_usage_count_meta( $coupon, 'decrease' ); + if ( $used_by ) { + /** + * We're doing this the long way because `delete_post_meta( $id, $key, $value )` deletes. + * all instances where the key and value match, and we only want to delete one. + */ + $meta_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_id FROM $wpdb->postmeta WHERE meta_key = '_used_by' AND meta_value = %s AND post_id = %d LIMIT 1;", + $used_by, + $coupon->get_id() + ) + ); + if ( $meta_id ) { + delete_metadata_by_mid( 'post', $meta_id ); + $coupon->set_used_by( (array) get_post_meta( $coupon->get_id(), '_used_by' ) ); + } + } + + do_action( 'woocommerce_decrease_coupon_usage_count', $coupon, $new_count, $used_by ); + + return $new_count; + } + + /** + * Increase or decrease the usage count for a coupon by 1. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + * @param string $operation 'increase' or 'decrease'. + * @return int New usage count + */ + private function update_usage_count_meta( &$coupon, $operation = 'increase' ) { + global $wpdb; + $id = $coupon->get_id(); + $operator = ( 'increase' === $operation ) ? '+' : '-'; + + add_post_meta( $id, 'usage_count', $coupon->get_usage_count( 'edit' ), true ); + $wpdb->query( + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "UPDATE $wpdb->postmeta SET meta_value = meta_value {$operator} 1 WHERE meta_key = 'usage_count' AND post_id = %d;", + $id + ) + ); + + // Get the latest value direct from the DB, instead of possibly the WP meta cache. + return (int) $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = 'usage_count' AND post_id = %d;", $id ) ); + } + + /** + * Returns tentative usage count for coupon. + * + * @param int $coupon_id Coupon ID. + * + * @return int Tentative usage count. + */ + public function get_tentative_usage_count( $coupon_id ) { + global $wpdb; + return $wpdb->get_var( + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $this->get_tentative_usage_query( $coupon_id ) + ); + } + + /** + * Get the number of uses for a coupon by user ID. + * + * @since 3.0.0 + * @param WC_Coupon $coupon Coupon object. + * @param int $user_id User ID. + * @return int + */ + public function get_usage_by_user_id( &$coupon, $user_id ) { + global $wpdb; + $usage_count = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %d;", + $coupon->get_id(), + $user_id + ) + ); + $tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $user_id ) ); + return $tentative_usage_count + $usage_count; + } + + /** + * Get the number of uses for a coupon by email address + * + * @since 3.6.4 + * @param WC_Coupon $coupon Coupon object. + * @param string $email Email address. + * @return int + */ + public function get_usage_by_email( &$coupon, $email ) { + global $wpdb; + $usage_count = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value = %s;", + $coupon->get_id(), + $email + ) + ); + $tentative_usage_count = $this->get_tentative_usages_for_user( $coupon->get_id(), array( $email ) ); + return $tentative_usage_count + $usage_count; + } + + /** + * Get tentative coupon usages for user. + * + * @param int $coupon_id Coupon ID. + * @param array $user_aliases Array of user aliases to check tentative usages for. + * + * @return string|null + */ + public function get_tentative_usages_for_user( $coupon_id, $user_aliases ) { + global $wpdb; + return $wpdb->get_var( + $this->get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) + ); // WPCS: unprepared SQL ok. + + } + + /** + * Get held time for resources before cancelling the order. Use 60 minutes as sane default. + * Note that the filter `woocommerce_coupon_hold_minutes` only support minutes because it's getting used elsewhere as well, however this function returns in seconds. + * + * @return int + */ + private function get_tentative_held_time() { + return apply_filters( 'woocommerce_coupon_hold_minutes', ( (int) get_option( 'woocommerce_hold_stock_minutes', 60 ) ) ) * 60; + } + + /** + * Check and records coupon usage tentatively for short period of time so that counts validation is correct. Returns early if there is no limit defined for the coupon. + * + * @param WC_Coupon $coupon Coupon object. + * + * @return bool|int|string|null Returns meta key if coupon was held, null if returned early. + */ + public function check_and_hold_coupon( $coupon ) { + global $wpdb; + + $usage_limit = $coupon->get_usage_limit(); + $held_time = $this->get_tentative_held_time(); + + if ( 0 >= $usage_limit || 0 >= $held_time ) { + return null; + } + + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { + return null; + } + + // Make sure we have usage_count meta key for this coupon because its required for `$query_for_usages`. + // We are not directly modifying `$query_for_usages` to allow for `usage_count` not present only keep that query simple. + if ( ! metadata_exists( 'post', $coupon->get_id(), 'usage_count' ) ) { + $coupon->set_usage_count( $coupon->get_usage_count() ); // Use `get_usage_count` here to write default value, which may changed by a filter. + $coupon->save(); + } + + $query_for_usages = $wpdb->prepare( + " + SELECT meta_value from $wpdb->postmeta + WHERE {$wpdb->postmeta}.meta_key = 'usage_count' + AND {$wpdb->postmeta}.post_id = %d + LIMIT 1 + FOR UPDATE + ", + $coupon->get_id() + ); + + $query_for_tentative_usages = $this->get_tentative_usage_query( $coupon->get_id() ); + $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); + + $coupon_usage_key = '_coupon_held_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false ); + + $insert_statement = $wpdb->prepare( + " + INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) + SELECT %d, %s, %s FROM DUAL + WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d + ", + $coupon->get_id(), + $coupon_usage_key, + '', + $usage_limit + ); // WPCS: unprepared SQL ok. + + /** + * In some cases, specifically when there is a combined index on post_id,meta_key, the insert statement above could end up in a deadlock. + * We will try to insert 3 times before giving up to recover from deadlock. + */ + for ( $count = 0; $count < 3; $count++ ) { + $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. + if ( false !== $result ) { + break; + } + } + + return $result > 0 ? $coupon_usage_key : $result; + } + + /** + * Generate query to calculate tentative usages for the coupon. + * + * @param int $coupon_id Coupon ID to get tentative usage query for. + * + * @return string Query for tentative usages. + */ + private function get_tentative_usage_query( $coupon_id ) { + global $wpdb; + return $wpdb->prepare( + " + SELECT COUNT(meta_id) FROM $wpdb->postmeta + WHERE {$wpdb->postmeta}.meta_key like %s + AND {$wpdb->postmeta}.meta_key > %s + AND {$wpdb->postmeta}.post_id = %d + FOR UPDATE + ", + array( + '_coupon_held_%', + '_coupon_held_' . time(), + $coupon_id, + ) + ); // WPCS: unprepared SQL ok. + } + + /** + * Check and records coupon usage tentatively for passed user aliases for short period of time so that counts validation is correct. Returns early if there is no limit per user for the coupon. + * + * @param WC_Coupon $coupon Coupon object. + * @param array $user_aliases Emails or Ids to check for user. + * @param string $user_alias Email/ID to use as `used_by` value. + * + * @return null|false|int + */ + public function check_and_hold_coupon_for_user( $coupon, $user_aliases, $user_alias ) { + global $wpdb; + $limit_per_user = $coupon->get_usage_limit_per_user(); + $held_time = $this->get_tentative_held_time(); + + if ( 0 >= $limit_per_user || 0 >= $held_time ) { + // This coupon do not have any restriction for usage per customer. No need to check further, lets bail. + return null; + } + + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', true ) ) { + return null; + } + + $format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) ); + + $query_for_usages = $wpdb->prepare( + " + SELECT COUNT(*) FROM $wpdb->postmeta + WHERE {$wpdb->postmeta}.meta_key = '_used_by' + AND {$wpdb->postmeta}.meta_value IN ('$format') + AND {$wpdb->postmeta}.post_id = %d + FOR UPDATE + ", + array_merge( + $user_aliases, + array( $coupon->get_id() ) + ) + ); // WPCS: unprepared SQL ok. + + $query_for_tentative_usages = $this->get_tentative_usage_query_for_user( $coupon->get_id(), $user_aliases ); + $db_timestamp = $wpdb->get_var( 'SELECT UNIX_TIMESTAMP() FROM DUAL' ); + + $coupon_used_by_meta_key = '_maybe_used_by_' . ( (int) $db_timestamp + $held_time ) . '_' . wp_generate_password( 6, false ); + $insert_statement = $wpdb->prepare( + " + INSERT INTO $wpdb->postmeta ( post_id, meta_key, meta_value ) + SELECT %d, %s, %s FROM DUAL + WHERE ( $query_for_usages ) + ( $query_for_tentative_usages ) < %d + ", + $coupon->get_id(), + $coupon_used_by_meta_key, + $user_alias, + $limit_per_user + ); // WPCS: unprepared SQL ok. + + // This query can potentially be deadlocked if a combined index on post_id and meta_key is present and there is + // high concurrency, in which case DB will abort the query which has done less work to resolve deadlock. + // We will try up to 3 times before giving up. + for ( $count = 0; $count < 3; $count++ ) { + $result = $wpdb->query( $insert_statement ); // WPCS: unprepared SQL ok. + if ( false !== $result ) { + break; + } + } + + return $result > 0 ? $coupon_used_by_meta_key : $result; + } + + /** + * Generate query to calculate tentative usages for the coupon by the user. + * + * @param int $coupon_id Coupon ID. + * @param array $user_aliases List of user aliases to check for usages. + * + * @return string Tentative usages query. + */ + private function get_tentative_usage_query_for_user( $coupon_id, $user_aliases ) { + global $wpdb; + + $format = implode( "','", array_fill( 0, count( $user_aliases ), '%s' ) ); + + // Note that if you are debugging, `_maybe_used_by_%` will be converted to `_maybe_used_by_{...very long str...}` to very long string. This is expected, and is automatically corrected while running the insert query. + return $wpdb->prepare( + " + SELECT COUNT( meta_id ) FROM $wpdb->postmeta + WHERE {$wpdb->postmeta}.meta_key like %s + AND {$wpdb->postmeta}.meta_key > %s + AND {$wpdb->postmeta}.post_id = %d + AND {$wpdb->postmeta}.meta_value IN ('$format') + FOR UPDATE + ", + array_merge( + array( + '_maybe_used_by_%', + '_maybe_used_by_' . time(), + $coupon_id, + ), + $user_aliases + ) + ); // WPCS: unprepared SQL ok. + } + + /** + * Return a coupon code for a specific ID. + * + * @since 3.0.0 + * @param int $id Coupon ID. + * @return string Coupon Code + */ + public function get_code_by_id( $id ) { + global $wpdb; + return $wpdb->get_var( + $wpdb->prepare( + "SELECT post_title + FROM $wpdb->posts + WHERE ID = %d + AND post_type = 'shop_coupon' + AND post_status = 'publish'", + $id + ) + ); + } + + /** + * Return an array of IDs for for a specific coupon code. + * Can return multiple to check for existence. + * + * @since 3.0.0 + * @param string $code Coupon code. + * @return array Array of IDs. + */ + public function get_ids_by_code( $code ) { + global $wpdb; + return $wpdb->get_col( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC", + wc_sanitize_coupon_code( $code ) + ) + ); + } +} diff --git a/includes/data-stores/class-wc-customer-data-store-session.php b/includes/data-stores/class-wc-customer-data-store-session.php new file mode 100644 index 0000000..048e093 --- /dev/null +++ b/includes/data-stores/class-wc-customer-data-store-session.php @@ -0,0 +1,199 @@ +save_to_session( $customer ); + } + + /** + * Simply update the session. + * + * @param WC_Customer $customer Customer object. + */ + public function update( &$customer ) { + $this->save_to_session( $customer ); + } + + /** + * Saves all customer data to the session. + * + * @param WC_Customer $customer Customer object. + */ + public function save_to_session( $customer ) { + $data = array(); + foreach ( $this->session_keys as $session_key ) { + $function_key = $session_key; + if ( 'billing_' === substr( $session_key, 0, 8 ) ) { + $session_key = str_replace( 'billing_', '', $session_key ); + } + $data[ $session_key ] = (string) $customer->{"get_$function_key"}( 'edit' ); + } + WC()->session->set( 'customer', $data ); + } + + /** + * Read customer data from the session unless the user has logged in, in + * which case the stored ID will differ from the actual ID. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + */ + public function read( &$customer ) { + $data = (array) WC()->session->get( 'customer' ); + + /** + * There is a valid session if $data is not empty, and the ID matches the logged in user ID. + * + * If the user object has been updated since the session was created (based on date_modified) we should not load the session - data should be reloaded. + */ + if ( isset( $data['id'], $data['date_modified'] ) && $data['id'] === (string) $customer->get_id() && $data['date_modified'] === (string) $customer->get_date_modified( 'edit' ) ) { + foreach ( $this->session_keys as $session_key ) { + if ( in_array( $session_key, array( 'id', 'date_modified' ), true ) ) { + continue; + } + $function_key = $session_key; + if ( 'billing_' === substr( $session_key, 0, 8 ) ) { + $session_key = str_replace( 'billing_', '', $session_key ); + } + if ( isset( $data[ $session_key ] ) && is_callable( array( $customer, "set_{$function_key}" ) ) ) { + $customer->{"set_{$function_key}"}( wp_unslash( $data[ $session_key ] ) ); + } + } + } + $this->set_defaults( $customer ); + $customer->set_object_read( true ); + } + + /** + * Load default values if props are unset. + * + * @param WC_Customer $customer Customer object. + */ + protected function set_defaults( &$customer ) { + try { + $default = wc_get_customer_default_location(); + $has_shipping_address = $customer->has_shipping_address(); + + if ( ! $customer->get_billing_country() ) { + $customer->set_billing_country( $default['country'] ); + } + + if ( ! $customer->get_shipping_country() && ! $has_shipping_address ) { + $customer->set_shipping_country( $customer->get_billing_country() ); + } + + if ( ! $customer->get_billing_state() ) { + $customer->set_billing_state( $default['state'] ); + } + + if ( ! $customer->get_shipping_state() && ! $has_shipping_address ) { + $customer->set_shipping_state( $customer->get_billing_state() ); + } + + if ( ! $customer->get_billing_email() && is_user_logged_in() ) { + $current_user = wp_get_current_user(); + $customer->set_billing_email( $current_user->user_email ); + } + } catch ( WC_Data_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch + } + } + + /** + * Deletes a customer from the database. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$customer, $args = array() ) { + WC()->session->set( 'customer', null ); + } + + /** + * Gets the customers last order. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return WC_Order|false + */ + public function get_last_order( &$customer ) { + return false; + } + + /** + * Return the number of orders this customer has. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return integer + */ + public function get_order_count( &$customer ) { + return 0; + } + + /** + * Return how much money this customer has spent. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return float + */ + public function get_total_spent( &$customer ) { + return 0; + } +} diff --git a/includes/data-stores/class-wc-customer-data-store.php b/includes/data-stores/class-wc-customer-data-store.php new file mode 100644 index 0000000..7bbfe83 --- /dev/null +++ b/includes/data-stores/class-wc-customer-data-store.php @@ -0,0 +1,529 @@ +prefix ? $wpdb->prefix : 'wp_'; + + return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) + && 0 !== strpos( $meta->meta_key, '_woocommerce_persistent_cart' ) + && 0 !== strpos( $meta->meta_key, 'closedpostboxes_' ) + && 0 !== strpos( $meta->meta_key, 'metaboxhidden_' ) + && 0 !== strpos( $meta->meta_key, 'manageedit-' ) + && ! strstr( $meta->meta_key, $table_prefix ) + && 0 !== stripos( $meta->meta_key, 'wp_' ); + } + + /** + * Method to create a new customer in the database. + * + * @since 3.0.0 + * + * @param WC_Customer $customer Customer object. + * + * @throws WC_Data_Exception If unable to create new customer. + */ + public function create( &$customer ) { + $id = wc_create_new_customer( $customer->get_email(), $customer->get_username(), $customer->get_password() ); + + if ( is_wp_error( $id ) ) { + throw new WC_Data_Exception( $id->get_error_code(), $id->get_error_message() ); + } + + $customer->set_id( $id ); + $this->update_user_meta( $customer ); + + // Prevent wp_update_user calls in the same request and customer trigger the 'Notice of Password Changed' email. + $customer->set_password( '' ); + + wp_update_user( + apply_filters( + 'woocommerce_update_customer_args', + array( + 'ID' => $customer->get_id(), + 'role' => $customer->get_role(), + 'display_name' => $customer->get_display_name(), + ), + $customer + ) + ); + $wp_user = new WP_User( $customer->get_id() ); + $customer->set_date_created( $wp_user->user_registered ); + $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); + $customer->save_meta_data(); + $customer->apply_changes(); + do_action( 'woocommerce_new_customer', $customer->get_id(), $customer ); + } + + /** + * Method to read a customer object. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @throws Exception If invalid customer. + */ + public function read( &$customer ) { + $user_object = $customer->get_id() ? get_user_by( 'id', $customer->get_id() ) : false; + + // User object is required. + if ( ! $user_object || empty( $user_object->ID ) ) { + throw new Exception( __( 'Invalid customer.', 'woocommerce' ) ); + } + + $customer_id = $customer->get_id(); + + // Load meta but exclude deprecated props and parent keys. + $user_meta = array_diff_key( + array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ), + array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ), + array_change_key_case( (array) $user_object->data ) + ); + + $customer->set_props( $user_meta ); + $customer->set_props( + array( + 'is_paying_customer' => get_user_meta( $customer_id, 'paying_customer', true ), + 'email' => $user_object->user_email, + 'username' => $user_object->user_login, + 'display_name' => $user_object->display_name, + 'date_created' => $user_object->user_registered, // Mysql string in local format. + 'date_modified' => get_user_meta( $customer_id, 'last_update', true ), + 'role' => ! empty( $user_object->roles[0] ) ? $user_object->roles[0] : 'customer', + ) + ); + $customer->read_meta_data(); + $customer->set_object_read( true ); + do_action( 'woocommerce_customer_loaded', $customer ); + } + + /** + * Updates a customer in the database. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + */ + public function update( &$customer ) { + wp_update_user( + apply_filters( + 'woocommerce_update_customer_args', + array( + 'ID' => $customer->get_id(), + 'user_email' => $customer->get_email(), + 'display_name' => $customer->get_display_name(), + ), + $customer + ) + ); + + // Only update password if a new one was set with set_password. + if ( $customer->get_password() ) { + wp_update_user( + array( + 'ID' => $customer->get_id(), + 'user_pass' => $customer->get_password(), + ) + ); + $customer->set_password( '' ); + } + + $this->update_user_meta( $customer ); + $customer->set_date_modified( get_user_meta( $customer->get_id(), 'last_update', true ) ); + $customer->save_meta_data(); + $customer->apply_changes(); + do_action( 'woocommerce_update_customer', $customer->get_id(), $customer ); + } + + /** + * Deletes a customer from the database. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$customer, $args = array() ) { + if ( ! $customer->get_id() ) { + return; + } + + $args = wp_parse_args( + $args, + array( + 'reassign' => 0, + ) + ); + + $id = $customer->get_id(); + wp_delete_user( $id, $args['reassign'] ); + + do_action( 'woocommerce_delete_customer', $id ); + } + + /** + * Helper method that updates all the meta for a customer. Used for update & create. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + */ + private function update_user_meta( $customer ) { + $updated_props = array(); + $changed_props = $customer->get_changes(); + + $meta_key_to_props = array( + 'paying_customer' => 'is_paying_customer', + 'first_name' => 'first_name', + 'last_name' => 'last_name', + ); + + foreach ( $meta_key_to_props as $meta_key => $prop ) { + if ( ! array_key_exists( $prop, $changed_props ) ) { + continue; + } + + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + $billing_address_props = array( + 'billing_first_name' => 'billing_first_name', + 'billing_last_name' => 'billing_last_name', + 'billing_company' => 'billing_company', + 'billing_address_1' => 'billing_address_1', + 'billing_address_2' => 'billing_address_2', + 'billing_city' => 'billing_city', + 'billing_state' => 'billing_state', + 'billing_postcode' => 'billing_postcode', + 'billing_country' => 'billing_country', + 'billing_email' => 'billing_email', + 'billing_phone' => 'billing_phone', + ); + + foreach ( $billing_address_props as $meta_key => $prop ) { + $prop_key = substr( $prop, 8 ); + + if ( ! isset( $changed_props['billing'] ) || ! array_key_exists( $prop_key, $changed_props['billing'] ) ) { + continue; + } + + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + $shipping_address_props = array( + 'shipping_first_name' => 'shipping_first_name', + 'shipping_last_name' => 'shipping_last_name', + 'shipping_company' => 'shipping_company', + 'shipping_address_1' => 'shipping_address_1', + 'shipping_address_2' => 'shipping_address_2', + 'shipping_city' => 'shipping_city', + 'shipping_state' => 'shipping_state', + 'shipping_postcode' => 'shipping_postcode', + 'shipping_country' => 'shipping_country', + 'shipping_phone' => 'shipping_phone', + ); + + foreach ( $shipping_address_props as $meta_key => $prop ) { + $prop_key = substr( $prop, 9 ); + + if ( ! isset( $changed_props['shipping'] ) || ! array_key_exists( $prop_key, $changed_props['shipping'] ) ) { + continue; + } + + if ( update_user_meta( $customer->get_id(), $meta_key, $customer->{"get_$prop"}( 'edit' ) ) ) { + $updated_props[] = $prop; + } + } + + do_action( 'woocommerce_customer_object_updated_props', $customer, $updated_props ); + } + + /** + * Gets the customers last order. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return WC_Order|false + */ + public function get_last_order( &$customer ) { + $last_order = apply_filters( + 'woocommerce_customer_get_last_order', + get_user_meta( $customer->get_id(), '_last_order', true ), + $customer + ); + + if ( '' === $last_order ) { + global $wpdb; + + $last_order = $wpdb->get_var( + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + "SELECT posts.ID + FROM $wpdb->posts AS posts + LEFT JOIN {$wpdb->postmeta} AS meta on posts.ID = meta.post_id + WHERE meta.meta_key = '_customer_user' + AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + ORDER BY posts.ID DESC" + // phpcs:enable + ); + update_user_meta( $customer->get_id(), '_last_order', $last_order ); + } + + if ( ! $last_order ) { + return false; + } + + return wc_get_order( absint( $last_order ) ); + } + + /** + * Return the number of orders this customer has. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return integer + */ + public function get_order_count( &$customer ) { + $count = apply_filters( + 'woocommerce_customer_get_order_count', + get_user_meta( $customer->get_id(), '_order_count', true ), + $customer + ); + + if ( '' === $count ) { + global $wpdb; + + $count = $wpdb->get_var( + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + "SELECT COUNT(*) + FROM $wpdb->posts as posts + LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id + WHERE meta.meta_key = '_customer_user' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + AND meta_value = '" . esc_sql( $customer->get_id() ) . "'" + // phpcs:enable + ); + update_user_meta( $customer->get_id(), '_order_count', $count ); + } + + return absint( $count ); + } + + /** + * Return how much money this customer has spent. + * + * @since 3.0.0 + * @param WC_Customer $customer Customer object. + * @return float + */ + public function get_total_spent( &$customer ) { + $spent = apply_filters( + 'woocommerce_customer_get_total_spent', + get_user_meta( $customer->get_id(), '_money_spent', true ), + $customer + ); + + if ( '' === $spent ) { + global $wpdb; + + $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + $spent = $wpdb->get_var( + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + apply_filters( + 'woocommerce_customer_get_total_spent_query', + "SELECT SUM(meta2.meta_value) + FROM $wpdb->posts as posts + LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id + LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id + WHERE meta.meta_key = '_customer_user' + AND meta.meta_value = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) + AND meta2.meta_key = '_order_total'", + $customer + ) + // phpcs:enable + ); + + if ( ! $spent ) { + $spent = 0; + } + update_user_meta( $customer->get_id(), '_money_spent', $spent ); + } + + return wc_format_decimal( $spent, 2 ); + } + + /** + * Search customers and return customer IDs. + * + * @param string $term Search term. + * @param int|string $limit Limit search results. + * @since 3.0.7 + * + * @return array + */ + public function search_customers( $term, $limit = '' ) { + $results = apply_filters( 'woocommerce_customer_pre_search_customers', false, $term, $limit ); + if ( is_array( $results ) ) { + return $results; + } + + $query = new WP_User_Query( + apply_filters( + 'woocommerce_customer_search_customers', + array( + 'search' => '*' . esc_attr( $term ) . '*', + 'search_columns' => array( 'user_login', 'user_url', 'user_email', 'user_nicename', 'display_name' ), + 'fields' => 'ID', + 'number' => $limit, + ), + $term, + $limit, + 'main_query' + ) + ); + + $query2 = new WP_User_Query( + apply_filters( + 'woocommerce_customer_search_customers', + array( + 'fields' => 'ID', + 'number' => $limit, + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'first_name', + 'value' => $term, + 'compare' => 'LIKE', + ), + array( + 'key' => 'last_name', + 'value' => $term, + 'compare' => 'LIKE', + ), + ), + ), + $term, + $limit, + 'meta_query' + ) + ); + + $results = wp_parse_id_list( array_merge( (array) $query->get_results(), (array) $query2->get_results() ) ); + + if ( $limit && count( $results ) > $limit ) { + $results = array_slice( $results, 0, $limit ); + } + + return $results; + } + + /** + * Get all user ids who have `billing_email` set to any of the email passed in array. + * + * @param array $emails List of emails to check against. + * + * @return array + */ + public function get_user_ids_for_billing_email( $emails ) { + $emails = array_unique( array_map( 'strtolower', array_map( 'sanitize_email', $emails ) ) ); + $users_query = new WP_User_Query( + array( + 'fields' => 'ID', + 'meta_query' => array( + array( + 'key' => 'billing_email', + 'value' => $emails, + 'compare' => 'IN', + ), + ), + ) + ); + return array_unique( $users_query->get_results() ); + } +} diff --git a/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 0000000..f715fc6 --- /dev/null +++ b/includes/data-stores/class-wc-customer-download-data-store.php @@ -0,0 +1,521 @@ +insert_new_download_permission( $data ); + + do_action( 'woocommerce_grant_product_download_access', $data ); + + return $id; + } + + /** + * Create download permission for a user. + * + * @param WC_Customer_Download $download WC_Customer_Download object. + */ + public function create( &$download ) { + global $wpdb; + + // Always set a access granted date. + if ( is_null( $download->get_access_granted( 'edit' ) ) ) { + $download->set_access_granted( time() ); + } + + $data = array(); + foreach ( self::DOWNLOAD_PERMISSION_DB_FIELDS as $db_field_name ) { + $value = call_user_func( array( $download, 'get_' . $db_field_name ), 'edit' ); + $data[ $db_field_name ] = $value; + } + + $inserted_id = $this->insert_new_download_permission( $data ); + if ( $inserted_id ) { + $download->set_id( $inserted_id ); + $download->apply_changes(); + } + + do_action( 'woocommerce_grant_product_download_access', $data ); + } + + /** + * Create download permission for a user, from an array of data. + * Assumes that all the keys in the passed data are valid. + * + * @param array $data Data to create the permission for. + * @return int The database id of the created permission, or false if the permission creation failed. + */ + private function insert_new_download_permission( $data ) { + global $wpdb; + + // Always set a access granted date. + if ( ! isset( $data['access_granted'] ) ) { + $data['access_granted'] = time(); + } + + $data['access_granted'] = $this->adjust_date_for_db( $data['access_granted'] ); + + if ( isset( $data['access_expires'] ) ) { + $data['access_expires'] = $this->adjust_date_for_db( $data['access_expires'] ); + } + + $format = array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%s', + ); + + $result = $wpdb->insert( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + apply_filters( 'woocommerce_downloadable_file_permission_data', $data ), + apply_filters( 'woocommerce_downloadable_file_permission_format', $format, $data ) + ); + + return $result ? $wpdb->insert_id : false; + } + + /** + * Adjust a date value to be inserted in the database. + * + * @param mixed $date The date value. Can be a WC_DateTime, a timestamp, or anything else that "date" recognizes. + * @return string The date converted to 'Y-m-d' format. + * @throws Exception The passed value can't be converted to a date. + */ + private function adjust_date_for_db( $date ) { + if ( 'WC_DateTime' === get_class( $date ) ) { + $date = $date->getTimestamp(); + } + + $adjusted_date = date( 'Y-m-d', $date ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + + if ( $adjusted_date ) { + return $adjusted_date; + } + + $msg = sprintf( __( "I don't know how to get a date from a %s", 'woocommerce' ), is_object( $date ) ? get_class( $date ) : gettype( $date ) ); + throw new Exception( $msg ); + } + + /** + * Method to read a download permission from the database. + * + * @param WC_Customer_Download $download WC_Customer_Download object. + * + * @throws Exception Throw exception if invalid download is passed. + */ + public function read( &$download ) { + global $wpdb; + + if ( ! $download->get_id() ) { + throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); + } + + $download->set_defaults(); + $raw_download = $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", + $download->get_id() + ) + ); + + if ( ! $raw_download ) { + throw new Exception( __( 'Invalid download.', 'woocommerce' ) ); + } + + $download->set_props( + array( + 'download_id' => $raw_download->download_id, + 'product_id' => $raw_download->product_id, + 'user_id' => $raw_download->user_id, + 'user_email' => $raw_download->user_email, + 'order_id' => $raw_download->order_id, + 'order_key' => $raw_download->order_key, + 'downloads_remaining' => $raw_download->downloads_remaining, + 'access_granted' => strtotime( $raw_download->access_granted ), + 'download_count' => $raw_download->download_count, + 'access_expires' => is_null( $raw_download->access_expires ) ? null : strtotime( $raw_download->access_expires ), + ) + ); + $download->set_object_read( true ); + } + + /** + * Method to update a download in the database. + * + * @param WC_Customer_Download $download WC_Customer_Download object. + */ + public function update( &$download ) { + global $wpdb; + + $data = array( + 'download_id' => $download->get_download_id( 'edit' ), + 'product_id' => $download->get_product_id( 'edit' ), + 'user_id' => $download->get_user_id( 'edit' ), + 'user_email' => $download->get_user_email( 'edit' ), + 'order_id' => $download->get_order_id( 'edit' ), + 'order_key' => $download->get_order_key( 'edit' ), + 'downloads_remaining' => $download->get_downloads_remaining( 'edit' ), + // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + 'access_granted' => date( 'Y-m-d', $download->get_access_granted( 'edit' )->getTimestamp() ), + 'download_count' => $download->get_download_count( 'edit' ), + // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + 'access_expires' => ! is_null( $download->get_access_expires( 'edit' ) ) ? date( 'Y-m-d', $download->get_access_expires( 'edit' )->getTimestamp() ) : null, + ); + + $format = array( + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%s', + '%d', + '%s', + ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + $data, + array( + 'permission_id' => $download->get_id(), + ), + $format + ); + $download->apply_changes(); + } + + /** + * Method to delete a download permission from the database. + * + * @param WC_Customer_Download $download WC_Customer_Download object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$download, $args = array() ) { + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE permission_id = %d", + $download->get_id() + ) + ); + + $download->set_id( 0 ); + } + + /** + * Method to delete a download permission from the database by ID. + * + * @param int $id permission_id of the download to be deleted. + */ + public function delete_by_id( $id ) { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE permission_id = %d", + $id + ) + ); + } + + /** + * Method to delete a download permission from the database by order ID. + * + * @param int $id Order ID of the downloads that will be deleted. + */ + public function delete_by_order_id( $id ) { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE order_id = %d", + $id + ) + ); + } + + /** + * Method to delete a download permission from the database by download ID. + * + * @param int $id download_id of the downloads that will be deleted. + */ + public function delete_by_download_id( $id ) { + global $wpdb; + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE download_id = %s", + $id + ) + ); + } + + /** + * Method to delete a download permission from the database by user ID. + * + * @since 3.4.0 + * @param int $id user ID of the downloads that will be deleted. + * @return bool True if deleted rows. + */ + public function delete_by_user_id( $id ) { + global $wpdb; + return (bool) $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE user_id = %d", + $id + ) + ); + } + + /** + * Method to delete a download permission from the database by user email. + * + * @since 3.4.0 + * @param string $email email of the downloads that will be deleted. + * @return bool True if deleted rows. + */ + public function delete_by_user_email( $email ) { + global $wpdb; + return (bool) $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE user_email = %s", + $email + ) + ); + } + + /** + * Get a download object. + * + * @param array $data From the DB. + * @return WC_Customer_Download + */ + private function get_download( $data ) { + return new WC_Customer_Download( $data ); + } + + /** + * Get array of download ids by specified args. + * + * @param array $args Arguments to filter downloads. $args['return'] accepts the following values: 'objects' (default), 'ids' or a comma separeted list of fields (for example: 'order_id,user_id,user_email'). + * @return array Can be an array of permission_ids, an array of WC_Customer_Download objects or an array of arrays containing specified fields depending on the value of $args['return']. + */ + public function get_downloads( $args = array() ) { + global $wpdb; + + $args = wp_parse_args( + $args, + array( + 'user_email' => '', + 'user_id' => '', + 'order_id' => '', + 'order_key' => '', + 'product_id' => '', + 'download_id' => '', + 'orderby' => 'permission_id', + 'order' => 'ASC', + 'limit' => -1, + 'page' => 1, + 'return' => 'objects', + ) + ); + + $valid_fields = array( 'permission_id', 'download_id', 'product_id', 'order_id', 'order_key', 'user_email', 'user_id', 'downloads_remaining', 'access_granted', 'access_expires', 'download_count' ); + $get_results_output = ARRAY_A; + + if ( 'ids' === $args['return'] ) { + $fields = 'permission_id'; + } elseif ( 'objects' === $args['return'] ) { + $fields = '*'; + $get_results_output = OBJECT; + } else { + $fields = explode( ',', (string) $args['return'] ); + $fields = implode( ', ', array_intersect( $fields, $valid_fields ) ); + } + + $query = array(); + $query[] = "SELECT {$fields} FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE 1=1"; + + if ( $args['user_email'] ) { + $query[] = $wpdb->prepare( 'AND user_email = %s', sanitize_email( $args['user_email'] ) ); + } + + if ( $args['user_id'] ) { + $query[] = $wpdb->prepare( 'AND user_id = %d', absint( $args['user_id'] ) ); + } + + if ( $args['order_id'] ) { + $query[] = $wpdb->prepare( 'AND order_id = %d', $args['order_id'] ); + } + + if ( $args['order_key'] ) { + $query[] = $wpdb->prepare( 'AND order_key = %s', $args['order_key'] ); + } + + if ( $args['product_id'] ) { + $query[] = $wpdb->prepare( 'AND product_id = %d', $args['product_id'] ); + } + + if ( $args['download_id'] ) { + $query[] = $wpdb->prepare( 'AND download_id = %s', $args['download_id'] ); + } + + $orderby = in_array( $args['orderby'], $valid_fields, true ) ? $args['orderby'] : 'permission_id'; + $order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC'; + $orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" ); + $query[] = "ORDER BY {$orderby_sql}"; + + if ( 0 < $args['limit'] ) { + $query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( implode( ' ', $query ), $get_results_output ); + + switch ( $args['return'] ) { + case 'ids': + return wp_list_pluck( $results, 'permission_id' ); + case 'objects': + return array_map( array( $this, 'get_download' ), $results ); + default: + return $results; + } + } + + /** + * Update download ids if the hash changes. + * + * @deprecated 3.3.0 Download id is now a static UUID and should not be changed based on file hash. + * + * @param int $product_id Product ID. + * @param string $old_id Old download_id. + * @param string $new_id New download_id. + */ + public function update_download_id( $product_id, $old_id, $new_id ) { + global $wpdb; + + wc_deprecated_function( __METHOD__, '3.3' ); + + $wpdb->update( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + array( + 'download_id' => $new_id, + ), + array( + 'download_id' => $old_id, + 'product_id' => $product_id, + ) + ); + } + + /** + * Get a customers downloads. + * + * @param int $customer_id Customer ID. + * @return array + */ + public function get_downloads_for_customer( $customer_id ) { + global $wpdb; + + return $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions as permissions + WHERE user_id = %d + AND permissions.order_id > 0 + AND + ( + permissions.downloads_remaining > 0 + OR permissions.downloads_remaining = '' + ) + AND + ( + permissions.access_expires IS NULL + OR permissions.access_expires >= %s + OR permissions.access_expires = '0000-00-00 00:00:00' + ) + ORDER BY permissions.order_id, permissions.product_id, permissions.permission_id;", + $customer_id, + date( 'Y-m-d', current_time( 'timestamp' ) ) // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + ) + ); + } + + /** + * Update user prop for downloads based on order id. + * + * @param int $order_id Order ID. + * @param int $customer_id Customer ID. + * @param string $email Customer email address. + */ + public function update_user_by_order_id( $order_id, $customer_id, $email ) { + global $wpdb; + $wpdb->update( + $wpdb->prefix . 'woocommerce_downloadable_product_permissions', + array( + 'user_id' => $customer_id, + 'user_email' => $email, + ), + array( + 'order_id' => $order_id, + ), + array( + '%d', + '%s', + ), + array( + '%d', + ) + ); + } +} diff --git a/includes/data-stores/class-wc-customer-download-log-data-store.php b/includes/data-stores/class-wc-customer-download-log-data-store.php new file mode 100644 index 0000000..12b59a5 --- /dev/null +++ b/includes/data-stores/class-wc-customer-download-log-data-store.php @@ -0,0 +1,239 @@ +get_timestamp( 'edit' ) ) ) { + $download_log->set_timestamp( time() ); + } + + $data = array( + 'timestamp' => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ), + 'permission_id' => $download_log->get_permission_id( 'edit' ), + 'user_id' => $download_log->get_user_id( 'edit' ), + 'user_ip_address' => $download_log->get_user_ip_address( 'edit' ), + ); + + $format = array( + '%s', + '%s', + '%s', + '%s', + ); + + $result = $wpdb->insert( + $wpdb->prefix . self::get_table_name(), + apply_filters( 'woocommerce_downloadable_product_download_log_insert_data', $data ), + apply_filters( 'woocommerce_downloadable_product_download_log_insert_format', $format, $data ) + ); + + do_action( 'woocommerce_downloadable_product_download_log_insert', $data ); + + if ( $result ) { + $download_log->set_id( $wpdb->insert_id ); + $download_log->apply_changes(); + } else { + wp_die( esc_html__( 'Unable to insert download log entry in database.', 'woocommerce' ) ); + } + } + + /** + * Method to read a download log from the database. + * + * @param WC_Customer_Download_Log $download_log Download log object. + * @throws Exception Exception when read is not possible. + */ + public function read( &$download_log ) { + global $wpdb; + + $download_log->set_defaults(); + + // Ensure we have an id to pull from the DB. + if ( ! $download_log->get_id() ) { + throw new Exception( __( 'Invalid download log: no ID.', 'woocommerce' ) ); + } + + $table = $wpdb->prefix . self::get_table_name(); + + // Query the DB for the download log. + $raw_download_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE download_log_id = %d", $download_log->get_id() ) ); // WPCS: unprepared SQL ok. + + if ( ! $raw_download_log ) { + throw new Exception( __( 'Invalid download log: not found.', 'woocommerce' ) ); + } + + $download_log->set_props( + array( + 'timestamp' => strtotime( $raw_download_log->timestamp ), + 'permission_id' => $raw_download_log->permission_id, + 'user_id' => $raw_download_log->user_id, + 'user_ip_address' => $raw_download_log->user_ip_address, + ) + ); + + $download_log->set_object_read( true ); + } + + /** + * Method to update a download log in the database. + * + * @param WC_Customer_Download_Log $download_log Download log object. + */ + public function update( &$download_log ) { + global $wpdb; + + $data = array( + 'timestamp' => date( 'Y-m-d H:i:s', $download_log->get_timestamp( 'edit' )->getTimestamp() ), + 'permission_id' => $download_log->get_permission_id( 'edit' ), + 'user_id' => $download_log->get_user_id( 'edit' ), + 'user_ip_address' => $download_log->get_user_ip_address( 'edit' ), + ); + + $format = array( + '%s', + '%s', + '%s', + '%s', + ); + + $wpdb->update( + $wpdb->prefix . self::get_table_name(), + $data, + array( + 'download_log_id' => $download_log->get_id(), + ), + $format + ); + $download_log->apply_changes(); + } + + /** + * Get a download log object. + * + * @param array $data From the DB. + * @return WC_Customer_Download_Log + */ + private function get_download_log( $data ) { + return new WC_Customer_Download_Log( $data ); + } + + /** + * Get array of download log ids by specified args. + * + * @param array $args Arguments to define download logs to retrieve. + * @return array + */ + public function get_download_logs( $args = array() ) { + global $wpdb; + + $args = wp_parse_args( + $args, + array( + 'permission_id' => '', + 'user_id' => '', + 'user_ip_address' => '', + 'orderby' => 'download_log_id', + 'order' => 'ASC', + 'limit' => -1, + 'page' => 1, + 'return' => 'objects', + ) + ); + + $query = array(); + $table = $wpdb->prefix . self::get_table_name(); + $query[] = "SELECT * FROM {$table} WHERE 1=1"; + + if ( $args['permission_id'] ) { + $query[] = $wpdb->prepare( 'AND permission_id = %d', $args['permission_id'] ); + } + + if ( $args['user_id'] ) { + $query[] = $wpdb->prepare( 'AND user_id = %d', $args['user_id'] ); + } + + if ( $args['user_ip_address'] ) { + $query[] = $wpdb->prepare( 'AND user_ip_address = %s', $args['user_ip_address'] ); + } + + $allowed_orders = array( 'download_log_id', 'timestamp', 'permission_id', 'user_id' ); + $orderby = in_array( $args['orderby'], $allowed_orders, true ) ? $args['orderby'] : 'download_log_id'; + $order = 'DESC' === strtoupper( $args['order'] ) ? 'DESC' : 'ASC'; + $orderby_sql = sanitize_sql_orderby( "{$orderby} {$order}" ); + $query[] = "ORDER BY {$orderby_sql}"; + + if ( 0 < $args['limit'] ) { + $query[] = $wpdb->prepare( 'LIMIT %d, %d', absint( $args['limit'] ) * absint( $args['page'] - 1 ), absint( $args['limit'] ) ); + } + + $raw_download_logs = $wpdb->get_results( implode( ' ', $query ) ); // WPCS: unprepared SQL ok. + + switch ( $args['return'] ) { + case 'ids': + return wp_list_pluck( $raw_download_logs, 'download_log_id' ); + default: + return array_map( array( $this, 'get_download_log' ), $raw_download_logs ); + } + } + + /** + * Get download logs for a given download permission. + * + * @param int $permission_id Permission to get logs for. + * @return array + */ + public function get_download_logs_for_permission( $permission_id ) { + // If no permission_id is passed, return an empty array. + if ( empty( $permission_id ) ) { + return array(); + } + + return $this->get_download_logs( + array( + 'permission_id' => $permission_id, + ) + ); + } + + /** + * Method to delete download logs for a given permission ID. + * + * @since 3.4.0 + * @param int $id download_id of the downloads that will be deleted. + */ + public function delete_by_permission_id( $id ) { + global $wpdb; + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE permission_id = %d", $id ) ); + } +} 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 0000000..c94907b --- /dev/null +++ b/includes/data-stores/class-wc-data-store-wp.php @@ -0,0 +1,658 @@ +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 $object WC_Data object. + * @return array + */ + public function read_meta( &$object ) { + global $wpdb; + $db_info = $this->get_db_info(); + $raw_meta_data = $wpdb->get_results( + $wpdb->prepare( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "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']}", + // phpcs:enable + $object->get_id() + ) + ); + return $this->filter_raw_meta_data( $object, $raw_meta_data ); + } + + /** + * Helper method to filter internal meta keys from all meta data rows for the object. + * + * @since 4.7.0 + * + * @param WC_Data $object WC_Data object. + * @param array $raw_meta_data Array of std object of meta data to be filtered. + * + * @return mixed|void + */ + public function filter_raw_meta_data( &$object, $raw_meta_data ) { + $this->internal_meta_keys = array_merge( array_map( array( $this, 'prefix_key' ), $object->get_data_keys() ), $this->internal_meta_keys ); + $meta_data = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) ); + return apply_filters( "woocommerce_data_store_wp_{$this->meta_type}_read_meta", $meta_data, $object, $this ); + } + + /** + * Deletes meta based on meta ID. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data object. + * @param stdClass $meta (containing at least ->id). + */ + 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 $object WC_Data object. + * @param stdClass $meta (containing ->key and ->value). + * @return int meta ID + */ + public function add_meta( &$object, $meta ) { + return add_metadata( $this->meta_type, $object->get_id(), wp_slash( $meta->key ), is_string( $meta->value ) ? wp_slash( $meta->value ) : $meta->value, false ); + } + + /** + * Update meta. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data object. + * @param stdClass $meta (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' ), true ) ) { + $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 Prefix to be added to meta keys. + * @return string + */ + protected function prefix_key( $key ) { + return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key; + } + + /** + * Callback to remove unwanted meta data. + * + * @param object $meta Meta object to check if it should be excluded or not. + * @return bool + */ + protected function exclude_internal_meta_keys( $meta ) { + return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 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; + } + + /** + * Update meta data in, or delete it from, the database. + * + * Avoids storing meta when it's either an empty string or empty array. + * Other empty values such as numeric 0 and null should still be stored. + * Data-stores can force meta to exist using `must_exist_meta_keys`. + * + * Note: WordPress `get_metadata` function returns an empty string when meta data does not exist. + * + * @param WC_Data $object The WP_Data object (WC_Coupon for coupons, etc). + * @param string $meta_key Meta key to update. + * @param mixed $meta_value Value to save. + * + * @since 3.6.0 Added to prevent empty meta being stored unless required. + * + * @return bool True if updated/deleted. + */ + protected function update_or_delete_post_meta( $object, $meta_key, $meta_value ) { + if ( in_array( $meta_value, array( array(), '' ), true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) { + $updated = delete_post_meta( $object->get_id(), $meta_key ); + } else { + $updated = update_post_meta( $object->get_id(), $meta_key, $meta_value ); + } + + return (bool) $updated; + } + + /** + * 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(), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + ); + + 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, true ) ) { + // Check for existing values if wildcard is used. + if ( '*' === $value ) { + $wp_query_args['meta_query'][] = array( + array( + 'key' => '_' . $key, + 'compare' => 'EXISTS', + ), + array( + 'key' => '_' . $key, + 'value' => '', + 'compare' => '!=', + ), + ); + } else { + $wp_query_args['meta_query'][] = array( + 'key' => '_' . $key, + 'value' => $value, + 'compare' => is_array( $value ) ? 'IN' : '=', + ); + } + } else { // Other vars get mapped to wp_query args or just left alone. + $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. + * + * @since 3.2.0 + * @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 + */ + public 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; + } elseif ( is_numeric( $query_var ) ) { // Specific time query with a timestamp. + $dates[] = new WC_DateTime( "@{$query_var}", new DateTimeZone( 'UTC' ) ); + } elseif ( preg_match( $query_parse_regex, $query_var, $sections ) ) { // Query with operators and possible range of dates. + 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, true ) ? $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'; + } + } else { // Specific time query with a string. + $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 ) { + if ( 'day' === $precision ) { + /** + * 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 + */ + $query_arg[ $comparison ]['year'] = $dates[ $index ]->date( 'Y' ); + $query_arg[ $comparison ]['month'] = $dates[ $index ]->date( 'n' ); + $query_arg[ $comparison ]['day'] = $dates[ $index ]->date( 'j' ); + } else { + /** + * WordPress doesn't support 'hour'/'second'/'minute' in array format 'before'/'after' queries, + * so we have to use a string query. + */ + $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(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + // Meta dates are stored as timestamps in the db. + // Check against beginning/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; + } + + /** + * Return list of internal meta keys. + * + * @since 3.2.0 + * @return array + */ + public function get_internal_meta_keys() { + return $this->internal_meta_keys; + } + + /** + * Check if the terms are suitable for searching. + * + * Uses an array of stopwords (terms) that are excluded from the separate + * term matching when searching for posts. The list of English stopwords is + * the approximate search engines list, and is translatable. + * + * @since 3.4.0 + * @param array $terms Terms to check. + * @return array Terms that are not stopwords. + */ + protected function get_valid_search_terms( $terms ) { + $valid_terms = array(); + $stopwords = $this->get_search_stopwords(); + + foreach ( $terms as $term ) { + // keep before/after spaces when term is for exact match, otherwise trim quotes and spaces. + if ( preg_match( '/^".+"$/', $term ) ) { + $term = trim( $term, "\"'" ); + } else { + $term = trim( $term, "\"' " ); + } + + // Avoid single A-Z and single dashes. + if ( empty( $term ) || ( 1 === strlen( $term ) && preg_match( '/^[a-z\-]$/i', $term ) ) ) { + continue; + } + + if ( in_array( wc_strtolower( $term ), $stopwords, true ) ) { + continue; + } + + $valid_terms[] = $term; + } + + return $valid_terms; + } + + /** + * Retrieve stopwords used when parsing search terms. + * + * @since 3.4.0 + * @return array Stopwords. + */ + protected function get_search_stopwords() { + // Translators: This is a comma-separated list of very common words that should be excluded from a search, like a, an, and the. These are usually called "stopwords". You should not simply translate these individual words into your language. Instead, look for and provide commonly accepted stopwords in your language. + $stopwords = array_map( + 'wc_strtolower', + array_map( + 'trim', + explode( + ',', + _x( + 'about,an,are,as,at,be,by,com,for,from,how,in,is,it,of,on,or,that,the,this,to,was,what,when,where,who,will,with,www', + 'Comma-separated list of search stopwords in your language', + 'woocommerce' + ) + ) + ) + ); + + return apply_filters( 'wp_search_stopwords', $stopwords ); + } + + /** + * Get data to save to a lookup table. + * + * @since 3.6.0 + * @param int $id ID of object to update. + * @param string $table Lookup table name. + * @return array + */ + protected function get_data_for_lookup_table( $id, $table ) { + return array(); + } + + /** + * Get primary key name for lookup table. + * + * @since 3.6.0 + * @param string $table Lookup table name. + * @return string + */ + protected function get_primary_key_for_lookup_table( $table ) { + return ''; + } + + /** + * Update a lookup table for an object. + * + * @since 3.6.0 + * @param int $id ID of object to update. + * @param string $table Lookup table name. + * + * @return NULL + */ + protected function update_lookup_table( $id, $table ) { + global $wpdb; + + $id = absint( $id ); + $table = sanitize_key( $table ); + + if ( empty( $id ) || empty( $table ) ) { + return false; + } + + $existing_data = wp_cache_get( 'lookup_table', 'object_' . $id ); + $update_data = $this->get_data_for_lookup_table( $id, $table ); + + if ( ! empty( $update_data ) && $update_data !== $existing_data ) { + $wpdb->replace( + $wpdb->$table, + $update_data + ); + wp_cache_set( 'lookup_table', $update_data, 'object_' . $id ); + } + } + + /** + * Delete lookup table data for an ID. + * + * @since 3.6.0 + * @param int $id ID of object to update. + * @param string $table Lookup table name. + */ + public function delete_from_lookup_table( $id, $table ) { + global $wpdb; + + $id = absint( $id ); + $table = sanitize_key( $table ); + + if ( empty( $id ) || empty( $table ) ) { + return false; + } + + $pk = $this->get_primary_key_for_lookup_table( $table ); + + $wpdb->delete( + $wpdb->$table, + array( + $pk => $id, + ) + ); + wp_cache_delete( 'lookup_table', 'object_' . $id ); + } + + /** + * Converts a WP post date string into a timestamp. + * + * @since 4.8.0 + * + * @param string $time_string The WP post date string. + * @return int|null The date string converted to a timestamp or null. + */ + protected function string_to_timestamp( $time_string ) { + return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null; + } +} 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 0000000..bc05529 --- /dev/null +++ b/includes/data-stores/class-wc-order-data-store-cpt.php @@ -0,0 +1,1117 @@ +get_order_key() ) { + $order->set_order_key( wc_generate_order_key() ); + } + parent::create( $order ); + do_action( 'woocommerce_new_order', $order->get_id(), $order ); + } + + /** + * Read order data. Can be overridden by child classes to load other props. + * + * @param WC_Order $order Order object. + * @param object $post_object Post object. + * @since 3.0.0 + */ + protected function read_order_data( &$order, $post_object ) { + parent::read_order_data( $order, $post_object ); + $id = $order->get_id(); + $date_completed = get_post_meta( $id, '_date_completed', true ); + $date_paid = get_post_meta( $id, '_date_paid', true ); + + if ( ! $date_completed ) { + $date_completed = get_post_meta( $id, '_completed_date', true ); + } + + if ( ! $date_paid ) { + $date_paid = get_post_meta( $id, '_paid_date', true ); + } + + $order->set_props( + array( + 'order_key' => get_post_meta( $id, '_order_key', true ), + 'customer_id' => get_post_meta( $id, '_customer_user', true ), + 'billing_first_name' => get_post_meta( $id, '_billing_first_name', true ), + 'billing_last_name' => get_post_meta( $id, '_billing_last_name', true ), + 'billing_company' => get_post_meta( $id, '_billing_company', true ), + 'billing_address_1' => get_post_meta( $id, '_billing_address_1', true ), + 'billing_address_2' => get_post_meta( $id, '_billing_address_2', true ), + 'billing_city' => get_post_meta( $id, '_billing_city', true ), + 'billing_state' => get_post_meta( $id, '_billing_state', true ), + 'billing_postcode' => get_post_meta( $id, '_billing_postcode', true ), + 'billing_country' => get_post_meta( $id, '_billing_country', true ), + 'billing_email' => get_post_meta( $id, '_billing_email', true ), + 'billing_phone' => get_post_meta( $id, '_billing_phone', true ), + 'shipping_first_name' => get_post_meta( $id, '_shipping_first_name', true ), + 'shipping_last_name' => get_post_meta( $id, '_shipping_last_name', true ), + 'shipping_company' => get_post_meta( $id, '_shipping_company', true ), + 'shipping_address_1' => get_post_meta( $id, '_shipping_address_1', true ), + 'shipping_address_2' => get_post_meta( $id, '_shipping_address_2', true ), + 'shipping_city' => get_post_meta( $id, '_shipping_city', true ), + 'shipping_state' => get_post_meta( $id, '_shipping_state', true ), + 'shipping_postcode' => get_post_meta( $id, '_shipping_postcode', true ), + 'shipping_country' => get_post_meta( $id, '_shipping_country', true ), + 'shipping_phone' => get_post_meta( $id, '_shipping_phone', true ), + 'payment_method' => get_post_meta( $id, '_payment_method', true ), + 'payment_method_title' => get_post_meta( $id, '_payment_method_title', true ), + 'transaction_id' => get_post_meta( $id, '_transaction_id', true ), + 'customer_ip_address' => get_post_meta( $id, '_customer_ip_address', true ), + 'customer_user_agent' => get_post_meta( $id, '_customer_user_agent', true ), + 'created_via' => get_post_meta( $id, '_created_via', true ), + 'date_completed' => $date_completed, + 'date_paid' => $date_paid, + 'cart_hash' => get_post_meta( $id, '_cart_hash', true ), + 'customer_note' => $post_object->post_excerpt, + ) + ); + } + + /** + * Method to update an order in the database. + * + * @param WC_Order $order Order object. + */ + public function update( &$order ) { + // Before updating, ensure date paid is set if missing. + if ( ! $order->get_date_paid( 'edit' ) && version_compare( $order->get_version( 'edit' ), '3.0', '<' ) && $order->has_status( apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) ) ) { + $order->set_date_paid( $order->get_date_created( 'edit' ) ); + } + + // Also grab the current status so we can compare. + $previous_status = get_post_status( $order->get_id() ); + + // Update the order. + parent::update( $order ); + + // Fire a hook depending on the status - this should be considered a creation if it was previously draft status. + $new_status = $order->get_status( 'edit' ); + + if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft' ), true ) ) { + do_action( 'woocommerce_new_order', $order->get_id(), $order ); + } else { + do_action( 'woocommerce_update_order', $order->get_id(), $order ); + } + } + + /** + * Helper method that updates all the post meta for an order based on it's settings in the WC_Order class. + * + * @param WC_Order $order Order object. + * @since 3.0.0 + */ + protected function update_post_meta( &$order ) { + $updated_props = array(); + $id = $order->get_id(); + $meta_key_to_props = array( + '_order_key' => 'order_key', + '_customer_user' => 'customer_id', + '_payment_method' => 'payment_method', + '_payment_method_title' => 'payment_method_title', + '_transaction_id' => 'transaction_id', + '_customer_ip_address' => 'customer_ip_address', + '_customer_user_agent' => 'customer_user_agent', + '_created_via' => 'created_via', + '_date_completed' => 'date_completed', + '_date_paid' => 'date_paid', + '_cart_hash' => 'cart_hash', + ); + + $props_to_update = $this->get_props_to_update( $order, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + switch ( $prop ) { + case 'date_paid': + case 'date_completed': + $value = ! is_null( $value ) ? $value->getTimestamp() : ''; + break; + } + + $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); + + if ( $updated ) { + $updated_props[] = $prop; + } + } + + $address_props = array( + 'billing' => array( + '_billing_first_name' => 'billing_first_name', + '_billing_last_name' => 'billing_last_name', + '_billing_company' => 'billing_company', + '_billing_address_1' => 'billing_address_1', + '_billing_address_2' => 'billing_address_2', + '_billing_city' => 'billing_city', + '_billing_state' => 'billing_state', + '_billing_postcode' => 'billing_postcode', + '_billing_country' => 'billing_country', + '_billing_email' => 'billing_email', + '_billing_phone' => 'billing_phone', + ), + 'shipping' => array( + '_shipping_first_name' => 'shipping_first_name', + '_shipping_last_name' => 'shipping_last_name', + '_shipping_company' => 'shipping_company', + '_shipping_address_1' => 'shipping_address_1', + '_shipping_address_2' => 'shipping_address_2', + '_shipping_city' => 'shipping_city', + '_shipping_state' => 'shipping_state', + '_shipping_postcode' => 'shipping_postcode', + '_shipping_country' => 'shipping_country', + '_shipping_phone' => 'shipping_phone', + ), + ); + + foreach ( $address_props as $props_key => $props ) { + $props_to_update = $this->get_props_to_update( $order, $props ); + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + $updated = $this->update_or_delete_post_meta( $order, $meta_key, $value ); + + if ( $updated ) { + $updated_props[] = $prop; + $updated_props[] = $props_key; + } + } + } + + parent::update_post_meta( $order ); + + // If address changed, store concatenated version to make searches faster. + if ( in_array( 'billing', $updated_props, true ) || ! metadata_exists( 'post', $id, '_billing_address_index' ) ) { + update_post_meta( $id, '_billing_address_index', implode( ' ', $order->get_address( 'billing' ) ) ); + } + if ( in_array( 'shipping', $updated_props, true ) || ! metadata_exists( 'post', $id, '_shipping_address_index' ) ) { + update_post_meta( $id, '_shipping_address_index', implode( ' ', $order->get_address( 'shipping' ) ) ); + } + + // Legacy date handling. @todo remove in 4.0. + if ( in_array( 'date_paid', $updated_props, true ) ) { + $value = $order->get_date_paid( 'edit' ); + // In 2.6.x date_paid was stored as _paid_date in local mysql format. + update_post_meta( $id, '_paid_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); + } + + if ( in_array( 'date_completed', $updated_props, true ) ) { + $value = $order->get_date_completed( 'edit' ); + // In 2.6.x date_completed was stored as _completed_date in local mysql format. + update_post_meta( $id, '_completed_date', ! is_null( $value ) ? $value->date( 'Y-m-d H:i:s' ) : '' ); + } + + // If customer changed, update any downloadable permissions. + if ( in_array( 'customer_id', $updated_props ) || in_array( 'billing_email', $updated_props ) ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->update_user_by_order_id( $id, $order->get_customer_id(), $order->get_billing_email() ); + } + + // Mark user account as active. + if ( in_array( 'customer_id', $updated_props, true ) ) { + wc_update_user_last_active( $order->get_customer_id() ); + } + + do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); + } + + /** + * Excerpt for post. + * + * @param WC_Order $order Order object. + * @return string + */ + protected function get_post_excerpt( $order ) { + return $order->get_customer_note(); + } + + /** + * Get order key. + * + * @since 4.3.0 + * @param WC_order $order Order object. + * @return string + */ + protected function get_order_key( $order ) { + if ( '' !== $order->get_order_key() ) { + return $order->get_order_key(); + } + + return parent::get_order_key( $order ); + } + + /** + * Get amount already refunded. + * + * @param WC_Order $order Order object. + * @return float + */ + public function get_total_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( postmeta.meta_value ) + FROM $wpdb->postmeta AS postmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + WHERE postmeta.meta_key = '_refund_amount' + AND postmeta.post_id = posts.ID", + $order->get_id() + ) + ); + + return floatval( $total ); + } + + /** + * Get the total tax refunded. + * + * @param WC_Order $order Order object. + * @return float + */ + public function get_total_tax_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( order_itemmeta.meta_value ) + FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' ) + WHERE order_itemmeta.order_item_id = order_items.order_item_id + AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount')", + $order->get_id() + ) + ); + + return abs( $total ); + } + + /** + * Get the total shipping refunded. + * + * @param WC_Order $order Order object. + * @return float + */ + public function get_total_shipping_refunded( $order ) { + global $wpdb; + + $total = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( order_itemmeta.meta_value ) + FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' ) + WHERE order_itemmeta.order_item_id = order_items.order_item_id + AND order_itemmeta.meta_key IN ('cost')", + $order->get_id() + ) + ); + + return abs( $total ); + } + + /** + * Finds an Order ID based on an order key. + * + * @param string $order_key An order key has generated by. + * @return int The ID of an order, or 0 if the order could not be found + */ + public function get_order_id_by_order_key( $order_key ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->prefix}postmeta WHERE meta_key = '_order_key' AND meta_value = %s", $order_key ) ); + } + + /** + * Return count of orders with a specific status. + * + * @param string $status Order status. Function wc_get_order_statuses() returns a list of valid statuses. + * @return int + */ + public function get_order_count( $status ) { + global $wpdb; + return absint( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT( * ) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", $status ) ) ); + } + + /** + * Get all orders matching the passed in args. + * + * @deprecated 3.1.0 - Use wc_get_orders instead. + * @see wc_get_orders() + * + * @param array $args List of args passed to wc_get_orders(). + * + * @return array|object + */ + public function get_orders( $args = array() ) { + wc_deprecated_function( 'WC_Order_Data_Store_CPT::get_orders', '3.1.0', 'Use wc_get_orders instead.' ); + return wc_get_orders( $args ); + } + + /** + * Generate meta query for wc_get_orders. + * + * @param array $values List of customers ids or emails. + * @param string $relation 'or' or 'and' relation used to build the WP meta_query. + * @return array + */ + private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) { + $meta_query = array( + 'relation' => strtoupper( $relation ), + 'customer_emails' => array( + 'key' => '_billing_email', + 'value' => array(), + 'compare' => 'IN', + ), + 'customer_ids' => array( + 'key' => '_customer_user', + 'value' => array(), + 'compare' => 'IN', + ), + ); + foreach ( $values as $value ) { + if ( is_array( $value ) ) { + $query_part = $this->get_orders_generate_customer_meta_query( $value, 'and' ); + if ( is_wp_error( $query_part ) ) { + return $query_part; + } + $meta_query[] = $query_part; + } elseif ( is_email( $value ) ) { + $meta_query['customer_emails']['value'][] = sanitize_email( $value ); + } elseif ( is_numeric( $value ) ) { + $meta_query['customer_ids']['value'][] = strval( absint( $value ) ); + } else { + return new WP_Error( 'woocommerce_query_invalid', __( 'Invalid customer query.', 'woocommerce' ), $values ); + } + } + + if ( empty( $meta_query['customer_emails']['value'] ) ) { + unset( $meta_query['customer_emails'] ); + unset( $meta_query['relation'] ); + } + + if ( empty( $meta_query['customer_ids']['value'] ) ) { + unset( $meta_query['customer_ids'] ); + unset( $meta_query['relation'] ); + } + + return $meta_query; + } + + /** + * Get unpaid orders after a certain date, + * + * @param int $date Timestamp. + * @return array + */ + public function get_unpaid_orders( $date ) { + global $wpdb; + + $unpaid_orders = $wpdb->get_col( + $wpdb->prepare( + // @codingStandardsIgnoreStart + "SELECT posts.ID + FROM {$wpdb->posts} AS posts + WHERE posts.post_type IN ('" . implode( "','", wc_get_order_types() ) . "') + AND posts.post_status = 'wc-pending' + AND posts.post_modified < %s", + // @codingStandardsIgnoreEnd + gmdate( 'Y-m-d H:i:s', absint( $date ) ) + ) + ); + + return $unpaid_orders; + } + + /** + * Search order data for a term and return ids. + * + * @param string $term Searched term. + * @return array of ids + */ + public function search_orders( $term ) { + global $wpdb; + + /** + * Searches on meta data can be slow - this lets you choose what fields to search. + * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster. + * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed). + * + * @var array + */ + $search_fields = array_map( + 'wc_clean', + apply_filters( + 'woocommerce_shop_order_search_fields', + array( + '_billing_address_index', + '_shipping_address_index', + '_billing_last_name', + '_billing_email', + ) + ) + ); + $order_ids = array(); + + if ( is_numeric( $term ) ) { + $order_ids[] = absint( $term ); + } + + if ( ! empty( $search_fields ) ) { + $order_ids = array_unique( + array_merge( + $order_ids, + $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT p1.post_id FROM {$wpdb->postmeta} p1 WHERE p1.meta_value LIKE %s AND p1.meta_key IN ('" . implode( "','", array_map( 'esc_sql', $search_fields ) ) . "')", // @codingStandardsIgnoreLine + '%' . $wpdb->esc_like( wc_clean( $term ) ) . '%' + ) + ), + $wpdb->get_col( + $wpdb->prepare( + "SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items as order_items + WHERE order_item_name LIKE %s", + '%' . $wpdb->esc_like( wc_clean( $term ) ) . '%' + ) + ) + ) + ); + } + + return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields ); + } + + /** + * Gets information about whether permissions were generated yet. + * + * @param WC_Order|int $order Order ID or order object. + * @return bool + */ + public function get_download_permissions_granted( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) ); + } + + /** + * Stores information about whether permissions were generated yet. + * + * @param WC_Order|int $order Order ID or order object. + * @param bool $set True or false. + */ + public function set_download_permissions_granted( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) ); + } + + /** + * Gets information about whether sales were recorded. + * + * @param WC_Order|int $order Order ID or order object. + * @return bool + */ + public function get_recorded_sales( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) ); + } + + /** + * Stores information about whether sales were recorded. + * + * @param WC_Order|int $order Order ID or order object. + * @param bool $set True or false. + */ + public function set_recorded_sales( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) ); + } + + /** + * Gets information about whether coupon counts were updated. + * + * @param WC_Order|int $order Order ID or order object. + * @return bool + */ + public function get_recorded_coupon_usage_counts( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) ); + } + + /** + * Stores information about whether coupon counts were updated. + * + * @param WC_Order|int $order Order ID or order object. + * @param bool $set True or false. + */ + public function set_recorded_coupon_usage_counts( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) ); + } + + /** + * Return array of coupon_code => meta_key for coupon which have usage limit and have tentative keys. + * Pass $coupon_id if key for only one of the coupon is needed. + * + * @param WC_Order $order Order object. + * @param int $coupon_id If passed, will return held key for that coupon. + * + * @return array|string Key value pair for coupon code and meta key name. If $coupon_id is passed, returns meta_key for only that coupon. + */ + public function get_coupon_held_keys( $order, $coupon_id = null ) { + $held_keys = $order->get_meta( '_coupon_held_keys' ); + if ( $coupon_id ) { + return isset( $held_keys[ $coupon_id ] ) ? $held_keys[ $coupon_id ] : null; + } + return $held_keys; + } + + /** + * Return array of coupon_code => meta_key for coupon which have usage limit per customer and have tentative keys. + * + * @param WC_Order $order Order object. + * @param int $coupon_id If passed, will return held key for that coupon. + * + * @return mixed + */ + public function get_coupon_held_keys_for_users( $order, $coupon_id = null ) { + $held_keys_for_user = $order->get_meta( '_coupon_held_keys_for_users' ); + if ( $coupon_id ) { + return isset( $held_keys_for_user[ $coupon_id ] ) ? $held_keys_for_user[ $coupon_id ] : null; + } + return $held_keys_for_user; + } + + /** + * Add/Update list of meta keys that are currently being used by this order to hold a coupon. + * This is used to figure out what all meta entries we should delete when order is cancelled/completed. + * + * @param WC_Order $order Order object. + * @param array $held_keys Array of coupon_code => meta_key. + * @param array $held_keys_for_user Array of coupon_code => meta_key for held coupon for user. + * + * @return mixed + */ + public function set_coupon_held_keys( $order, $held_keys, $held_keys_for_user ) { + if ( is_array( $held_keys ) && 0 < count( $held_keys ) ) { + $order->update_meta_data( '_coupon_held_keys', $held_keys ); + } + if ( is_array( $held_keys_for_user ) && 0 < count( $held_keys_for_user ) ) { + $order->update_meta_data( '_coupon_held_keys_for_users', $held_keys_for_user ); + } + } + + /** + * Release all coupons held by this order. + * + * @param WC_Order $order Current order object. + * @param bool $save Whether to delete keys from DB right away. Could be useful to pass `false` if you are building a bulk request. + */ + public function release_held_coupons( $order, $save = true ) { + $coupon_held_keys = $this->get_coupon_held_keys( $order ); + if ( is_array( $coupon_held_keys ) ) { + foreach ( $coupon_held_keys as $coupon_id => $meta_key ) { + delete_post_meta( $coupon_id, $meta_key ); + } + } + $order->delete_meta_data( '_coupon_held_keys' ); + + $coupon_held_keys_for_users = $this->get_coupon_held_keys_for_users( $order ); + if ( is_array( $coupon_held_keys_for_users ) ) { + foreach ( $coupon_held_keys_for_users as $coupon_id => $meta_key ) { + delete_post_meta( $coupon_id, $meta_key ); + } + } + $order->delete_meta_data( '_coupon_held_keys_for_users' ); + + if ( $save ) { + $order->save_meta_data(); + } + + } + + /** + * Gets information about whether stock was reduced. + * + * @param WC_Order|int $order Order ID or order object. + * @return bool + */ + public function get_stock_reduced( $order ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) ); + } + + /** + * Stores information about whether stock was reduced. + * + * @param WC_Order|int $order Order ID or order object. + * @param bool $set True or false. + */ + public function set_stock_reduced( $order, $set ) { + $order_id = WC_Order_Factory::get_order_id( $order ); + update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) ); + } + + /** + * Get the order type based on Order ID. + * + * @since 3.0.0 + * @param int|WP_Post $order Order | Order id. + * + * @return string + */ + public function get_order_type( $order ) { + return get_post_type( $order ); + } + + /** + * Get valid WP_Query args from a WC_Order_Query's query variables. + * + * @since 3.1.0 + * @param array $query_vars query vars from a WC_Order_Query. + * @return array + */ + protected function get_wp_query_args( $query_vars ) { + + // Map query vars to ones that get_wp_query_args or WP_Query recognize. + $key_mapping = array( + 'customer_id' => 'customer_user', + 'status' => 'post_status', + 'currency' => 'order_currency', + 'version' => 'order_version', + 'discount_total' => 'cart_discount', + 'discount_tax' => 'cart_discount_tax', + 'shipping_total' => 'order_shipping', + 'shipping_tax' => 'order_shipping_tax', + 'cart_tax' => 'order_tax', + 'total' => 'order_total', + 'page' => 'paged', + ); + + foreach ( $key_mapping as $query_key => $db_key ) { + if ( isset( $query_vars[ $query_key ] ) ) { + $query_vars[ $db_key ] = $query_vars[ $query_key ]; + unset( $query_vars[ $query_key ] ); + } + } + + // Add the 'wc-' prefix to status if needed. + if ( ! empty( $query_vars['post_status'] ) ) { + if ( is_array( $query_vars['post_status'] ) ) { + foreach ( $query_vars['post_status'] as &$status ) { + $status = wc_is_order_status( 'wc-' . $status ) ? 'wc-' . $status : $status; + } + } else { + $query_vars['post_status'] = wc_is_order_status( 'wc-' . $query_vars['post_status'] ) ? 'wc-' . $query_vars['post_status'] : $query_vars['post_status']; + } + } + + $wp_query_args = parent::get_wp_query_args( $query_vars ); + + if ( ! isset( $wp_query_args['date_query'] ) ) { + $wp_query_args['date_query'] = array(); + } + if ( ! isset( $wp_query_args['meta_query'] ) ) { + $wp_query_args['meta_query'] = array(); + } + + $date_queries = array( + 'date_created' => 'post_date', + 'date_modified' => 'post_modified', + 'date_completed' => '_date_completed', + 'date_paid' => '_date_paid', + ); + foreach ( $date_queries as $query_var_key => $db_key ) { + if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { + + // Remove any existing meta queries for the same keys to prevent conflicts. + $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); + $meta_query_index = array_search( $db_key, $existing_queries, true ); + if ( false !== $meta_query_index ) { + unset( $wp_query_args['meta_query'][ $meta_query_index ] ); + } + + $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); + } + } + + if ( isset( $query_vars['customer'] ) && '' !== $query_vars['customer'] && array() !== $query_vars['customer'] ) { + $values = is_array( $query_vars['customer'] ) ? $query_vars['customer'] : array( $query_vars['customer'] ); + $customer_query = $this->get_orders_generate_customer_meta_query( $values ); + if ( is_wp_error( $customer_query ) ) { + $wp_query_args['errors'][] = $customer_query; + } else { + $wp_query_args['meta_query'][] = $customer_query; + } + } + + if ( isset( $query_vars['anonymized'] ) ) { + if ( $query_vars['anonymized'] ) { + $wp_query_args['meta_query'][] = array( + 'key' => '_anonymized', + 'value' => 'yes', + ); + } else { + $wp_query_args['meta_query'][] = array( + 'key' => '_anonymized', + 'compare' => 'NOT EXISTS', + ); + } + } + + if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { + $wp_query_args['no_found_rows'] = true; + } + + return apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $query_vars, $this ); + } + + /** + * Query for Orders matching specific criteria. + * + * @since 3.1.0 + * + * @param array $query_vars query vars from a WC_Order_Query. + * + * @return array|object + */ + public function query( $query_vars ) { + $args = $this->get_wp_query_args( $query_vars ); + + if ( ! empty( $args['errors'] ) ) { + $query = (object) array( + 'posts' => array(), + 'found_posts' => 0, + 'max_num_pages' => 0, + ); + } else { + $query = new WP_Query( $args ); + } + + if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) { + $orders = $query->posts; + } else { + update_post_caches( $query->posts ); // We already fetching posts, might as well hydrate some caches. + $order_ids = wp_list_pluck( $query->posts, 'ID' ); + $orders = $this->compile_orders( $order_ids, $query_vars, $query ); + } + + if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { + return (object) array( + 'orders' => $orders, + 'total' => $query->found_posts, + 'max_num_pages' => $query->max_num_pages, + ); + } + + return $orders; + } + + /** + * Compile order response and set caches as needed for order ids. + * + * @param array $order_ids List of order IDS to compile. + * @param array $query_vars Original query arguments. + * @param WP_Query $query Query object. + * + * @return array Orders. + */ + private function compile_orders( $order_ids, $query_vars, $query ) { + if ( empty( $order_ids ) ) { + return array(); + } + $orders = array(); + + // Lets do some cache hydrations so that we don't have to fetch data from DB for every order. + $this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars ); + $this->prime_refund_caches_for_order( $order_ids, $query_vars ); + $this->prime_order_item_caches_for_orders( $order_ids, $query_vars ); + + foreach ( $query->posts as $post ) { + $order = wc_get_order( $post ); + + // If the order returns false, don't add it to the list. + if ( false === $order ) { + continue; + } + + $orders[] = $order; + } + + return $orders; + } + + /** + * Prime refund cache for orders. + * + * @param array $order_ids Order Ids to prime cache for. + * @param array $query_vars Query vars for the query. + */ + private function prime_refund_caches_for_order( $order_ids, $query_vars ) { + if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) { + return; + } + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { + if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'] ) ) { + return; + } + } + $cache_keys_mapping = array(); + foreach ( $order_ids as $order_id ) { + $cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id; + } + $non_cached_ids = array(); + $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' ); + foreach ( $order_ids as $order_id ) { + if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) { + $non_cached_ids[] = $order_id; + } + } + if ( empty( $non_cached_ids ) ) { + return; + } + + $refunds = wc_get_orders( + array( + 'type' => 'shop_order_refund', + 'post_parent__in' => $non_cached_ids, + 'limit' => - 1, + ) + ); + $order_refunds = array_reduce( + $refunds, + function ( $order_refunds_array, WC_Order_Refund $refund ) { + if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) { + $order_refunds_array[ $refund->get_parent_id() ] = array(); + } + $order_refunds_array[ $refund->get_parent_id() ][] = $refund; + return $order_refunds_array; + }, + array() + ); + foreach ( $non_cached_ids as $order_id ) { + $refunds = array(); + if ( isset( $order_refunds[ $order_id ] ) ) { + $refunds = $order_refunds[ $order_id ]; + } + wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' ); + } + } + + /** + * Prime following caches: + * 1. item-$order_item_id For individual items. + * 2. order-items-$order-id For fetching items associated with an order. + * 3. order-item meta. + * + * @param array $order_ids Order Ids to prime cache for. + * @param array $query_vars Query vars for the query. + */ + private function prime_order_item_caches_for_orders( $order_ids, $query_vars ) { + global $wpdb; + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { + $line_items = array( + 'line_items', + 'shipping_lines', + 'fee_lines', + 'coupon_lines', + ); + + if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) { + return; + } + } + $cache_keys = array_map( + function ( $order_id ) { + return 'order-items-' . $order_id; + }, + $order_ids + ); + $cache_values = wc_cache_get_multiple( $cache_keys, 'orders' ); + $non_cached_ids = array(); + foreach ( $order_ids as $order_id ) { + if ( false === $cache_values[ 'order-items-' . $order_id ] ) { + $non_cached_ids[] = $order_id; + } + } + if ( empty( $non_cached_ids ) ) { + return; + } + + $non_cached_ids = esc_sql( $non_cached_ids ); + $non_cached_ids_string = implode( ',', $non_cached_ids ); + $order_items = $wpdb->get_results( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;" + ); + if ( empty( $order_items ) ) { + return; + } + + $order_items_for_all_orders = array_reduce( + $order_items, + function ( $order_items_collection, $order_item ) { + if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) { + $order_items_collection[ $order_item->order_id ] = array(); + } + $order_items_collection[ $order_item->order_id ][] = $order_item; + return $order_items_collection; + } + ); + foreach ( $order_items_for_all_orders as $order_id => $items ) { + wp_cache_set( 'order-items-' . $order_id, $items, 'orders' ); + } + foreach ( $order_items as $item ) { + wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' ); + } + $order_item_ids = wp_list_pluck( $order_items, 'order_item_id' ); + update_meta_cache( 'order_item', $order_item_ids ); + } + + /** + * Prime cache for raw meta data for orders in bulk. Difference between this and WP built-in metadata is that this method also fetches `meta_id` field which we use and cache it. + * + * @param array $order_ids Order Ids to prime cache for. + * @param array $query_vars Query vars for the query. + */ + private function prime_raw_meta_cache_for_orders( $order_ids, $query_vars ) { + global $wpdb; + + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) { + if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'] ) ) { + return; + } + } + + $cache_keys_mapping = array(); + foreach ( $order_ids as $order_id ) { + $cache_keys_mapping[ $order_id ] = WC_Order::generate_meta_cache_key( $order_id, 'orders' ); + } + $cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' ); + $non_cached_ids = array(); + foreach ( $order_ids as $order_id ) { + if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) { + $non_cached_ids[] = $order_id; + } + } + if ( empty( $non_cached_ids ) ) { + return; + } + $order_ids = esc_sql( $non_cached_ids ); + $order_ids_in = "'" . implode( "', '", $order_ids ) . "'"; + $raw_meta_data_array = $wpdb->get_results( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT post_id as object_id, meta_id, meta_key, meta_value + FROM {$wpdb->postmeta} + WHERE post_id IN ( $order_ids_in ) + ORDER BY post_id" + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + ); + $raw_meta_data_collection = array_reduce( + $raw_meta_data_array, + function ( $collection, $raw_meta_data ) { + if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) { + $collection[ $raw_meta_data->object_id ] = array(); + } + $collection[ $raw_meta_data->object_id ][] = $raw_meta_data; + return $collection; + }, + array() + ); + WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' ); + } + + /** + * Return the order type of a given item which belongs to WC_Order. + * + * @since 3.2.0 + * @param WC_Order $order Order Object. + * @param int $order_item_id Order item id. + * @return string Order Item type + */ + public function get_order_item_type( $order, $order_item_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT DISTINCT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id = %d and order_item_id = %d;", $order->get_id(), $order_item_id ) ); + } +} diff --git a/includes/data-stores/class-wc-order-item-coupon-data-store.php b/includes/data-stores/class-wc-order-item-coupon-data-store.php new file mode 100644 index 0000000..01eeb9d --- /dev/null +++ b/includes/data-stores/class-wc-order-item-coupon-data-store.php @@ -0,0 +1,62 @@ +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 Coupon order 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 0000000..46600ac --- /dev/null +++ b/includes/data-stores/class-wc-order-item-data-store.php @@ -0,0 +1,189 @@ +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', + ) + ); + + $item_id = absint( $wpdb->insert_id ); + + $this->clear_caches( $item_id, $order_id ); + + return $item_id; + } + + /** + * Update an order item. + * + * @since 3.0.0 + * @param int $item_id Item ID. + * @param array $item order_item_name or order_item_type. + * @return boolean + */ + public function update_order_item( $item_id, $item ) { + global $wpdb; + $updated = $wpdb->update( $wpdb->prefix . 'woocommerce_order_items', $item, array( 'order_item_id' => $item_id ) ); + $this->clear_caches( $item_id, null ); + return $updated; + } + + /** + * Delete an order item. + * + * @since 3.0.0 + * @param int $item_id Item ID. + */ + public function delete_order_item( $item_id ) { + // Load the order ID before the deletion, since after, it won't exist in the database. + $order_id = $this->get_order_id_by_order_item_id( $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 ) ); + + $this->clear_caches( $item_id, $order_id ); + } + + /** + * Update term meta. + * + * @since 3.0.0 + * @param int $item_id Item ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value 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, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $prev_value ); + } + + /** + * Add term meta. + * + * @since 3.0.0 + * @param int $item_id Item ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value 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, wp_slash( $meta_key ), is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $unique ); + } + + /** + * Delete term meta. + * + * @since 3.0.0 + * @param int $item_id Item ID. + * @param string $meta_key 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, is_string( $meta_value ) ? wp_slash( $meta_value ) : $meta_value, $delete_all ); + } + + /** + * Get term meta. + * + * @since 3.0.0 + * @param int $item_id Item ID. + * @param string $key Meta 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 Item ID. + * @return int + */ + public 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 Item ID. + * @return string|null Order item type or null if no order item entry found. + */ + public function get_order_item_type( $item_id ) { + global $wpdb; + $order_item_type = $wpdb->get_var( + $wpdb->prepare( + "SELECT order_item_type FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d LIMIT 1;", + $item_id + ) + ); + + return $order_item_type; + } + + /** + * Clear meta cache. + * + * @param int $item_id Item ID. + * @param int|null $order_id Order ID. If not set, it will be loaded using the item ID. + */ + protected function clear_caches( $item_id, $order_id ) { + wp_cache_delete( 'item-' . $item_id, 'order-items' ); + + if ( ! $order_id ) { + $order_id = $this->get_order_id_by_order_item_id( $item_id ); + } + if ( $order_id ) { + wp_cache_delete( 'order-items-' . $order_id, 'orders' ); + } + } +} 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 0000000..2222d06 --- /dev/null +++ b/includes/data-stores/class-wc-order-item-fee-data-store.php @@ -0,0 +1,69 @@ +get_id(); + $item->set_props( + array( + 'amount' => get_metadata( 'order_item', $id, '_fee_amount', true ), + '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 Fee order item object. + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $save_values = array( + '_fee_amount' => $item->get_amount( 'edit' ), + '_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-data-store.php b/includes/data-stores/class-wc-order-item-product-data-store.php new file mode 100644 index 0000000..2f78691 --- /dev/null +++ b/includes/data-stores/class-wc-order-item-product-data-store.php @@ -0,0 +1,97 @@ +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 Product order item object. + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $changes = $item->get_changes(); + $meta_key_to_props = array( + '_product_id' => 'product_id', + '_variation_id' => 'variation_id', + '_qty' => 'quantity', + '_tax_class' => 'tax_class', + '_line_subtotal' => 'subtotal', + '_line_subtotal_tax' => 'subtotal_tax', + '_line_total' => 'total', + '_line_tax' => 'total_tax', + '_line_tax_data' => 'taxes', + ); + $props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' ); + + foreach ( $props_to_update as $meta_key => $prop ) { + update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) ); + } + } + + /** + * Get a list of download IDs for a specific item from an order. + * + * @since 3.0.0 + * @param WC_Order_Item_Product $item Product order item object. + * @param WC_Order $order Order object. + * @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 0000000..0c4f343 --- /dev/null +++ b/includes/data-stores/class-wc-order-item-shipping-data-store.php @@ -0,0 +1,78 @@ +get_id(); + $item->set_props( + array( + 'method_id' => get_metadata( 'order_item', $id, 'method_id', true ), + 'instance_id' => get_metadata( 'order_item', $id, 'instance_id', true ), + 'total' => get_metadata( 'order_item', $id, 'cost', true ), + 'taxes' => get_metadata( 'order_item', $id, 'taxes', true ), + ) + ); + + // BW compat. + if ( '' === $item->get_instance_id() && strstr( $item->get_method_id(), ':' ) ) { + $legacy_method_id = explode( ':', $item->get_method_id() ); + $item->set_method_id( $legacy_method_id[0] ); + $item->set_instance_id( $legacy_method_id[1] ); + } + + $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 Item to save. + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $changes = $item->get_changes(); + $meta_key_to_props = array( + 'method_id' => 'method_id', + 'instance_id' => 'instance_id', + 'cost' => 'total', + 'total_tax' => 'total_tax', + 'taxes' => 'taxes', + ); + $props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' ); + + foreach ( $props_to_update as $meta_key => $prop ) { + update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) ); + } + } +} 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 0000000..ab5325a --- /dev/null +++ b/includes/data-stores/class-wc-order-item-tax-data-store.php @@ -0,0 +1,74 @@ +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 ), + 'rate_percent' => get_metadata( 'order_item', $id, 'rate_percent', 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 Tax order item object. + */ + public function save_item_data( &$item ) { + $id = $item->get_id(); + $changes = $item->get_changes(); + $meta_key_to_props = array( + 'rate_id' => 'rate_id', + 'label' => 'label', + 'compound' => 'compound', + 'tax_amount' => 'tax_total', + 'shipping_tax_amount' => 'shipping_tax_total', + 'rate_percent' => 'rate_percent', + ); + $props_to_update = $this->get_props_to_update( $item, $meta_key_to_props, 'order_item' ); + + foreach ( $props_to_update as $meta_key => $prop ) { + update_metadata( 'order_item', $id, $meta_key, $item->{"get_$prop"}( 'edit' ) ); + } + } +} 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 0000000..090d3a5 --- /dev/null +++ b/includes/data-stores/class-wc-order-refund-data-store-cpt.php @@ -0,0 +1,122 @@ +get_id(); + $parent_order_id = $order->get_parent_id(); + $refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $parent_order_id; + + if ( ! $id ) { + return; + } + + wp_delete_post( $id ); + wp_cache_delete( $refund_cache_key, 'orders' ); + $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 $refund Refund object. + * @param object $post_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 ), + 'refunded_payment' => wc_string_to_bool( get_post_meta( $id, '_refunded_payment', true ) ), + '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_Refund $refund Refund object. + * @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', + '_refunded_payment' => 'refunded_payment', + '_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() { + return sprintf( + /* translators: %s: Order date */ + __( 'Refund – %s', 'woocommerce' ), + strftime( _x( '%b %d, %Y @ %I:%M %p', 'Order date parsed by strftime', 'woocommerce' ) ) // phpcs:ignore WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText + ); + } +} 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 0000000..f7ebc18 --- /dev/null +++ b/includes/data-stores/class-wc-payment-token-data-store.php @@ -0,0 +1,372 @@ +validate() ) { + throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); + } + + global $wpdb; + if ( ! $token->is_default() && $token->get_user_id() > 0 ) { + $default_token = WC_Payment_Tokens::get_customer_default_token( $token->get_user_id() ); + if ( is_null( $default_token ) ) { + $token->set_default( true ); + } + } + + $payment_token_data = array( + 'gateway_id' => $token->get_gateway_id( 'edit' ), + 'token' => $token->get_token( 'edit' ), + 'user_id' => $token->get_user_id( 'edit' ), + 'type' => $token->get_type( 'edit' ), + ); + + $wpdb->insert( $wpdb->prefix . 'woocommerce_payment_tokens', $payment_token_data ); + $token_id = $wpdb->insert_id; + $token->set_id( $token_id ); + $this->save_extra_data( $token, true ); + $token->save_meta_data(); + $token->apply_changes(); + + // Make sure all other tokens are not set to default. + if ( $token->is_default() && $token->get_user_id() > 0 ) { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token_id ); + } + + do_action( 'woocommerce_new_payment_token', $token_id, $token ); + } + + /** + * Update a payment token. + * + * @since 3.0.0 + * + * @param WC_Payment_Token $token Payment token object. + * + * @throws Exception Throw exception if invalid or missing payment token fields. + */ + public function update( &$token ) { + if ( false === $token->validate() ) { + throw new Exception( __( 'Invalid or missing payment token fields.', 'woocommerce' ) ); + } + + global $wpdb; + + $updated_props = array(); + $core_props = array( 'gateway_id', 'token', 'user_id', 'type' ); + $changed_props = array_keys( $token->get_changes() ); + + foreach ( $changed_props as $prop ) { + if ( ! in_array( $prop, $core_props, true ) ) { + continue; + } + $updated_props[] = $prop; + $payment_token_data[ $prop ] = $token->{'get_' . $prop}( 'edit' ); + } + + if ( ! empty( $payment_token_data ) ) { + $wpdb->update( + $wpdb->prefix . 'woocommerce_payment_tokens', + $payment_token_data, + array( 'token_id' => $token->get_id() ) + ); + } + + $updated_extra_props = $this->save_extra_data( $token ); + $updated_props = array_merge( $updated_props, $updated_extra_props ); + $token->save_meta_data(); + $token->apply_changes(); + + // Make sure all other tokens are not set to default. + if ( $token->is_default() && $token->get_user_id() > 0 ) { + WC_Payment_Tokens::set_users_default( $token->get_user_id(), $token->get_id() ); + } + + do_action( 'woocommerce_payment_token_object_updated_props', $token, $updated_props ); + do_action( 'woocommerce_payment_token_updated', $token->get_id() ); + } + + /** + * Remove a payment token from the database. + * + * @since 3.0.0 + * @param WC_Payment_Token $token Payment token object. + * @param bool $force_delete Unused param. + */ + public function delete( &$token, $force_delete = false ) { + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokens', array( 'token_id' => $token->get_id() ), array( '%d' ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_payment_tokenmeta', array( 'payment_token_id' => $token->get_id() ), array( '%d' ) ); + do_action( 'woocommerce_payment_token_deleted', $token->get_id(), $token ); + } + + /** + * Read a token from the database. + * + * @since 3.0.0 + * + * @param WC_Payment_Token $token Payment token object. + * + * @throws Exception Throw exception if invalid payment token. + */ + public function read( &$token ) { + global $wpdb; + + $data = $wpdb->get_row( + $wpdb->prepare( + "SELECT token, user_id, gateway_id, is_default FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d LIMIT 1", + $token->get_id() + ) + ); + + if ( $data ) { + $token->set_props( + array( + 'token' => $data->token, + 'user_id' => $data->user_id, + 'gateway_id' => $data->gateway_id, + 'default' => $data->is_default, + ) + ); + $this->read_extra_data( $token ); + $token->read_meta_data(); + $token->set_object_read( true ); + do_action( 'woocommerce_payment_token_loaded', $token ); + } else { + throw new Exception( __( 'Invalid payment token.', 'woocommerce' ) ); + } + } + + /** + * Read extra data associated with the token (like last4 digits of a card for expiry dates). + * + * @param WC_Payment_Token $token Payment token object. + * @since 3.0.0 + */ + protected function read_extra_data( &$token ) { + foreach ( $token->get_extra_data_keys() as $key ) { + $function = 'set_' . $key; + if ( is_callable( array( $token, $function ) ) ) { + $token->{$function}( get_metadata( 'payment_token', $token->get_id(), $key, true ) ); + } + } + } + + /** + * Saves extra token data as meta. + * + * @since 3.0.0 + * @param WC_Payment_Token $token Payment token object. + * @param bool $force By default, only changed props are updated. When this param is true all props are updated. + * @return array List of updated props. + */ + protected function save_extra_data( &$token, $force = false ) { + if ( $this->extra_data_saved ) { + return array(); + } + + $updated_props = array(); + $extra_data_keys = $token->get_extra_data_keys(); + $meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : array(); + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $token, $meta_key_to_props ); + + foreach ( $extra_data_keys as $key ) { + if ( ! array_key_exists( $key, $props_to_update ) ) { + continue; + } + $function = 'get_' . $key; + if ( is_callable( array( $token, $function ) ) ) { + if ( update_metadata( 'payment_token', $token->get_id(), $key, $token->{$function}( 'edit' ) ) ) { + $updated_props[] = $key; + } + } + } + + return $updated_props; + } + + /** + * Returns an array of objects (stdObject) matching specific token criteria. + * Accepts token_id, user_id, gateway_id, and type. + * Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param array $args List of accepted args: token_id, gateway_id, user_id, type. + * @return array + */ + public function get_tokens( $args ) { + global $wpdb; + $args = wp_parse_args( + $args, + array( + 'token_id' => '', + 'user_id' => '', + 'gateway_id' => '', + 'type' => '', + ) + ); + + $sql = "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens"; + $where = array( '1=1' ); + + if ( $args['token_id'] ) { + $token_ids = array_map( 'absint', is_array( $args['token_id'] ) ? $args['token_id'] : array( $args['token_id'] ) ); + $where[] = "token_id IN ('" . implode( "','", array_map( 'esc_sql', $token_ids ) ) . "')"; + } + + if ( $args['user_id'] ) { + $where[] = $wpdb->prepare( 'user_id = %d', absint( $args['user_id'] ) ); + } + + if ( $args['gateway_id'] ) { + $gateway_ids = array( $args['gateway_id'] ); + } else { + $gateways = WC_Payment_Gateways::instance(); + $gateway_ids = $gateways->get_payment_gateway_ids(); + } + + $page = isset( $args['page'] ) ? absint( $args['page'] ) : 1; + $posts_per_page = isset( $args['limit'] ) ? absint( $args['limit'] ) : get_option( 'posts_per_page' ); + + $pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', '; + $limits = 'LIMIT ' . $pgstrt . $posts_per_page; + + $gateway_ids[] = ''; + $where[] = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')"; + + if ( $args['type'] ) { + $where[] = $wpdb->prepare( 'type = %s', $args['type'] ); + } + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $token_results = $wpdb->get_results( $sql . ' WHERE ' . implode( ' AND ', $where ) . ' ' . $limits ); + + return $token_results; + } + + /** + * Returns an stdObject of a token for a user's default token. + * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param int $user_id User ID. + * @return object + */ + public function get_users_default_token( $user_id ) { + global $wpdb; + return $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d AND is_default = 1", + $user_id + ) + ); + } + + /** + * Returns an stdObject of a token. + * Should contain the fields token_id, gateway_id, token, user_id, type, is_default. + * + * @since 3.0.0 + * @param int $token_id Token ID. + * @return object + */ + public function get_token_by_id( $token_id ) { + global $wpdb; + return $wpdb->get_row( + $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", + $token_id + ) + ); + } + + /** + * Returns metadata for a specific payment token. + * + * @since 3.0.0 + * @param int $token_id Token ID. + * @return array + */ + public function get_metadata( $token_id ) { + return get_metadata( 'payment_token', $token_id ); + } + + /** + * Get a token's type by ID. + * + * @since 3.0.0 + * @param int $token_id Token ID. + * @return string + */ + public function get_token_type_by_id( $token_id ) { + global $wpdb; + return $wpdb->get_var( + $wpdb->prepare( + "SELECT type FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE token_id = %d", + $token_id + ) + ); + } + + /** + * Update's a tokens default status in the database. Used for quickly + * looping through tokens and setting their statuses instead of creating a bunch + * of objects. + * + * @since 3.0.0 + * + * @param int $token_id Token ID. + * @param bool $status Whether given payment token is the default payment token or not. + * + * @return void + */ + public function set_default_status( $token_id, $status = true ) { + global $wpdb; + $wpdb->update( + $wpdb->prefix . 'woocommerce_payment_tokens', + array( 'is_default' => (int) $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 0000000..190749d --- /dev/null +++ b/includes/data-stores/class-wc-product-data-store-cpt.php @@ -0,0 +1,2130 @@ +get_date_created( 'edit' ) ) { + $product->set_date_created( time() ); + } + + $id = wp_insert_post( + apply_filters( + 'woocommerce_new_product_data', + array( + 'post_type' => 'product', + 'post_status' => $product->get_status() ? $product->get_status() : 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $product->get_name() ? $product->get_name() : __( 'Product', 'woocommerce' ), + 'post_content' => $product->get_description(), + 'post_excerpt' => $product->get_short_description(), + 'post_parent' => $product->get_parent_id(), + 'comment_status' => $product->get_reviews_allowed() ? 'open' : 'closed', + 'ping_status' => 'closed', + 'menu_order' => $product->get_menu_order(), + 'post_password' => $product->get_post_password( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_name' => $product->get_slug( 'edit' ), + ) + ), + true + ); + + if ( $id && ! is_wp_error( $id ) ) { + $product->set_id( $id ); + + $this->update_post_meta( $product, true ); + $this->update_terms( $product, true ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product, true ); + $this->update_version_and_type( $product ); + $this->handle_updated_props( $product ); + $this->clear_caches( $product ); + + $product->save_meta_data(); + $product->apply_changes(); + + do_action( 'woocommerce_new_product', $id, $product ); + } + } + + /** + * Method to read a product from the database. + * + * @param WC_Product $product Product object. + * @throws Exception If invalid product. + */ + public function read( &$product ) { + $product->set_defaults(); + $post_object = get_post( $product->get_id() ); + + if ( ! $product->get_id() || ! $post_object || 'product' !== $post_object->post_type ) { + throw new Exception( __( 'Invalid product.', 'woocommerce' ) ); + } + + $product->set_props( + array( + 'name' => $post_object->post_title, + 'slug' => $post_object->post_name, + 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), + 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), + 'status' => $post_object->post_status, + 'description' => $post_object->post_content, + 'short_description' => $post_object->post_excerpt, + 'parent_id' => $post_object->post_parent, + 'menu_order' => $post_object->menu_order, + 'post_password' => $post_object->post_password, + 'reviews_allowed' => 'open' === $post_object->comment_status, + ) + ); + + $this->read_attributes( $product ); + $this->read_downloads( $product ); + $this->read_visibility( $product ); + $this->read_product_data( $product ); + $this->read_extra_data( $product ); + $product->set_object_read( true ); + + do_action( 'woocommerce_product_read', $product->get_id() ); + } + + /** + * Method to update a product in the database. + * + * @param WC_Product $product Product object. + */ + public function update( &$product ) { + $product->save_meta_data(); + $changes = $product->get_changes(); + + // Only update the post when the post data changes. + if ( array_intersect( array( 'description', 'short_description', 'name', 'parent_id', 'reviews_allowed', 'status', 'menu_order', 'date_created', 'date_modified', 'slug' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_content' => $product->get_description( 'edit' ), + 'post_excerpt' => $product->get_short_description( 'edit' ), + 'post_title' => $product->get_name( 'edit' ), + 'post_parent' => $product->get_parent_id( 'edit' ), + 'comment_status' => $product->get_reviews_allowed( 'edit' ) ? 'open' : 'closed', + 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', + 'menu_order' => $product->get_menu_order( 'edit' ), + 'post_password' => $product->get_post_password( 'edit' ), + 'post_name' => $product->get_slug( 'edit' ), + 'post_type' => 'product', + ); + if ( $product->get_date_created( 'edit' ) ) { + $post_data['post_date'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ); + $post_data['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ); + } + if ( isset( $changes['date_modified'] ) && $product->get_date_modified( 'edit' ) ) { + $post_data['post_modified'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ); + $post_data['post_modified_gmt'] = gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ); + } else { + $post_data['post_modified'] = current_time( 'mysql' ); + $post_data['post_modified_gmt'] = current_time( 'mysql', 1 ); + } + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); + } + $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + + } else { // Only update post modified time to record this save event. + $GLOBALS['wpdb']->update( + $GLOBALS['wpdb']->posts, + array( + 'post_modified' => current_time( 'mysql' ), + 'post_modified_gmt' => current_time( 'mysql', 1 ), + ), + array( + 'ID' => $product->get_id(), + ) + ); + clean_post_cache( $product->get_id() ); + } + + $this->update_post_meta( $product ); + $this->update_terms( $product ); + $this->update_visibility( $product ); + $this->update_attributes( $product ); + $this->update_version_and_type( $product ); + $this->handle_updated_props( $product ); + $this->clear_caches( $product ); + + wc_get_container() + ->get( DownloadPermissionsAdjuster::class ) + ->maybe_schedule_adjust_download_permissions( $product ); + + $product->apply_changes(); + + do_action( 'woocommerce_update_product', $product->get_id(), $product ); + } + + /** + * Method to delete a product from the database. + * + * @param WC_Product $product Product object. + * @param array $args Array of args to pass to the delete method. + */ + public function delete( &$product, $args = array() ) { + $id = $product->get_id(); + $post_type = $product->is_type( 'variation' ) ? 'product_variation' : 'product'; + + $args = wp_parse_args( + $args, + array( + 'force_delete' => false, + ) + ); + + if ( ! $id ) { + return; + } + + if ( $args['force_delete'] ) { + do_action( 'woocommerce_before_delete_' . $post_type, $id ); + wp_delete_post( $id ); + $product->set_id( 0 ); + do_action( 'woocommerce_delete_' . $post_type, $id ); + } else { + wp_trash_post( $id ); + $product->set_status( 'trash' ); + do_action( 'woocommerce_trash_' . $post_type, $id ); + } + } + + /* + |-------------------------------------------------------------------------- + | Additional Methods + |-------------------------------------------------------------------------- + */ + + /** + * Read product data. Can be overridden by child classes to load other props. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function read_product_data( &$product ) { + $id = $product->get_id(); + $post_meta_values = get_post_meta( $id ); + $meta_key_to_props = array( + '_sku' => 'sku', + '_regular_price' => 'regular_price', + '_sale_price' => 'sale_price', + '_price' => 'price', + '_sale_price_dates_from' => 'date_on_sale_from', + '_sale_price_dates_to' => 'date_on_sale_to', + 'total_sales' => 'total_sales', + '_tax_status' => 'tax_status', + '_tax_class' => 'tax_class', + '_manage_stock' => 'manage_stock', + '_backorders' => 'backorders', + '_low_stock_amount' => 'low_stock_amount', + '_sold_individually' => 'sold_individually', + '_weight' => 'weight', + '_length' => 'length', + '_width' => 'width', + '_height' => 'height', + '_upsell_ids' => 'upsell_ids', + '_crosssell_ids' => 'cross_sell_ids', + '_purchase_note' => 'purchase_note', + '_default_attributes' => 'default_attributes', + '_virtual' => 'virtual', + '_downloadable' => 'downloadable', + '_download_limit' => 'download_limit', + '_download_expiry' => 'download_expiry', + '_thumbnail_id' => 'image_id', + '_stock' => 'stock_quantity', + '_stock_status' => 'stock_status', + '_wc_average_rating' => 'average_rating', + '_wc_rating_count' => 'rating_counts', + '_wc_review_count' => 'review_count', + '_product_image_gallery' => 'gallery_image_ids', + ); + + $set_props = array(); + + foreach ( $meta_key_to_props as $meta_key => $prop ) { + $meta_value = isset( $post_meta_values[ $meta_key ][0] ) ? $post_meta_values[ $meta_key ][0] : null; + $set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values. + } + + $set_props['category_ids'] = $this->get_term_ids( $product, 'product_cat' ); + $set_props['tag_ids'] = $this->get_term_ids( $product, 'product_tag' ); + $set_props['shipping_class_id'] = current( $this->get_term_ids( $product, 'product_shipping_class' ) ); + $set_props['gallery_image_ids'] = array_filter( explode( ',', $set_props['gallery_image_ids'] ) ); + + $product->set_props( $set_props ); + } + + /** + * Re-reads stock from the DB ignoring changes. + * + * @param WC_Product $product Product object. + * @param int|float $new_stock New stock level if already read. + */ + public function read_stock_quantity( &$product, $new_stock = null ) { + $object_read = $product->get_object_read(); + $product->set_object_read( false ); // This makes update of qty go directly to data- instead of changes-array of the product object (which is needed as the data should hold status of the object as it was read from the db). + $product->set_stock_quantity( is_null( $new_stock ) ? get_post_meta( $product->get_id(), '_stock', true ) : $new_stock ); + $product->set_object_read( $object_read ); + } + + /** + * Read extra data associated with the product, like button text or product URL for external products. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function read_extra_data( &$product ) { + foreach ( $product->get_extra_data_keys() as $key ) { + $function = 'set_' . $key; + if ( is_callable( array( $product, $function ) ) ) { + $product->{$function}( get_post_meta( $product->get_id(), '_' . $key, true ) ); + } + } + } + + /** + * Convert visibility terms to props. + * Catalog visibility valid values are 'visible', 'catalog', 'search', and 'hidden'. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function read_visibility( &$product ) { + $terms = get_the_terms( $product->get_id(), 'product_visibility' ); + $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); + $featured = in_array( 'featured', $term_names, true ); + $exclude_search = in_array( 'exclude-from-search', $term_names, true ); + $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); + + if ( $exclude_search && $exclude_catalog ) { + $catalog_visibility = 'hidden'; + } elseif ( $exclude_search ) { + $catalog_visibility = 'catalog'; + } elseif ( $exclude_catalog ) { + $catalog_visibility = 'search'; + } else { + $catalog_visibility = 'visible'; + } + + $product->set_props( + array( + 'featured' => $featured, + 'catalog_visibility' => $catalog_visibility, + ) + ); + } + + /** + * Read attributes from post meta. + * + * @param WC_Product $product Product object. + */ + protected function read_attributes( &$product ) { + $meta_attributes = get_post_meta( $product->get_id(), '_product_attributes', true ); + + if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { + $attributes = array(); + foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { + $meta_value = array_merge( + array( + 'name' => '', + 'value' => '', + 'position' => 0, + 'is_visible' => 0, + 'is_variation' => 0, + 'is_taxonomy' => 0, + ), + (array) $meta_attribute_value + ); + + // Check if is a taxonomy attribute. + if ( ! empty( $meta_value['is_taxonomy'] ) ) { + if ( ! taxonomy_exists( $meta_value['name'] ) ) { + continue; + } + $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); + $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); + } else { + $id = 0; + $options = wc_get_text_attributes( $meta_value['value'] ); + } + + $attribute = new WC_Product_Attribute(); + $attribute->set_id( $id ); + $attribute->set_name( $meta_value['name'] ); + $attribute->set_options( $options ); + $attribute->set_position( $meta_value['position'] ); + $attribute->set_visible( $meta_value['is_visible'] ); + $attribute->set_variation( $meta_value['is_variation'] ); + $attributes[] = $attribute; + } + $product->set_attributes( $attributes ); + } + } + + /** + * Read downloads from post meta. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function read_downloads( &$product ) { + $meta_values = array_filter( (array) get_post_meta( $product->get_id(), '_downloadable_files', true ) ); + + if ( $meta_values ) { + $downloads = array(); + foreach ( $meta_values as $key => $value ) { + if ( ! isset( $value['name'], $value['file'] ) ) { + continue; + } + $download = new WC_Product_Download(); + $download->set_id( $key ); + $download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) ); + $downloads[] = $download; + } + $product->set_downloads( $downloads ); + } + } + + /** + * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + * @since 3.0.0 + */ + protected function update_post_meta( &$product, $force = false ) { + $meta_key_to_props = array( + '_sku' => 'sku', + '_regular_price' => 'regular_price', + '_sale_price' => 'sale_price', + '_sale_price_dates_from' => 'date_on_sale_from', + '_sale_price_dates_to' => 'date_on_sale_to', + 'total_sales' => 'total_sales', + '_tax_status' => 'tax_status', + '_tax_class' => 'tax_class', + '_manage_stock' => 'manage_stock', + '_backorders' => 'backorders', + '_low_stock_amount' => 'low_stock_amount', + '_sold_individually' => 'sold_individually', + '_weight' => 'weight', + '_length' => 'length', + '_width' => 'width', + '_height' => 'height', + '_upsell_ids' => 'upsell_ids', + '_crosssell_ids' => 'cross_sell_ids', + '_purchase_note' => 'purchase_note', + '_default_attributes' => 'default_attributes', + '_virtual' => 'virtual', + '_downloadable' => 'downloadable', + '_product_image_gallery' => 'gallery_image_ids', + '_download_limit' => 'download_limit', + '_download_expiry' => 'download_expiry', + '_thumbnail_id' => 'image_id', + '_stock' => 'stock_quantity', + '_stock_status' => 'stock_status', + '_wc_average_rating' => 'average_rating', + '_wc_rating_count' => 'rating_counts', + '_wc_review_count' => 'review_count', + ); + + // Make sure to take extra data (like product url or text for external products) into account. + $extra_data_keys = $product->get_extra_data_keys(); + + foreach ( $extra_data_keys as $key ) { + $meta_key_to_props[ '_' . $key ] = $key; + } + + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + switch ( $prop ) { + case 'virtual': + case 'downloadable': + case 'manage_stock': + case 'sold_individually': + $value = wc_bool_to_string( $value ); + break; + case 'gallery_image_ids': + $value = implode( ',', $value ); + break; + case 'date_on_sale_from': + case 'date_on_sale_to': + $value = $value ? $value->getTimestamp() : ''; + break; + case 'stock_quantity': + // Fire actions to let 3rd parties know the stock is about to be changed. + if ( $product->is_type( 'variation' ) ) { + /** + * Action to signal that the value of 'stock_quantity' for a variation is about to change. + * + * @since 4.9 + * + * @param int $product The variation whose stock is about to change. + */ + do_action( 'woocommerce_variation_before_set_stock', $product ); + } else { + /** + * Action to signal that the value of 'stock_quantity' for a product is about to change. + * + * @since 4.9 + * + * @param int $product The product whose stock is about to change. + */ + do_action( 'woocommerce_product_before_set_stock', $product ); + } + break; + } + + $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value ); + + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + // Update extra data associated with the product like button text or product URL for external products. + if ( ! $this->extra_data_saved ) { + foreach ( $extra_data_keys as $key ) { + $meta_key = '_' . $key; + $function = 'get_' . $key; + if ( ! array_key_exists( $meta_key, $props_to_update ) ) { + continue; + } + if ( is_callable( array( $product, $function ) ) ) { + $value = $product->{$function}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + $updated = $this->update_or_delete_post_meta( $product, $meta_key, $value ); + + if ( $updated ) { + $this->updated_props[] = $key; + } + } + } + } + + if ( $this->update_downloads( $product, $force ) ) { + $this->updated_props[] = 'downloads'; + } + } + + /** + * Handle updated meta props after updating meta data. + * + * @since 3.0.0 + * @param WC_Product $product Product Object. + */ + protected function handle_updated_props( &$product ) { + $price_is_synced = $product->is_type( array( 'variable', 'grouped' ) ); + + if ( ! $price_is_synced ) { + if ( in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) ) { + if ( $product->get_sale_price( 'edit' ) >= $product->get_regular_price( 'edit' ) ) { + update_post_meta( $product->get_id(), '_sale_price', '' ); + $product->set_sale_price( '' ); + } + } + + if ( in_array( 'date_on_sale_from', $this->updated_props, true ) || in_array( 'date_on_sale_to', $this->updated_props, true ) || in_array( 'regular_price', $this->updated_props, true ) || in_array( 'sale_price', $this->updated_props, true ) || in_array( 'product_type', $this->updated_props, true ) ) { + if ( $product->is_on_sale( 'edit' ) ) { + update_post_meta( $product->get_id(), '_price', $product->get_sale_price( 'edit' ) ); + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + update_post_meta( $product->get_id(), '_price', $product->get_regular_price( 'edit' ) ); + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + } + } + + if ( in_array( 'stock_quantity', $this->updated_props, true ) ) { + if ( $product->is_type( 'variation' ) ) { + do_action( 'woocommerce_variation_set_stock', $product ); + } else { + do_action( 'woocommerce_product_set_stock', $product ); + } + } + + if ( in_array( 'stock_status', $this->updated_props, true ) ) { + if ( $product->is_type( 'variation' ) ) { + do_action( 'woocommerce_variation_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); + } else { + do_action( 'woocommerce_product_set_stock_status', $product->get_id(), $product->get_stock_status(), $product ); + } + } + + if ( array_intersect( $this->updated_props, array( 'sku', 'regular_price', 'sale_price', 'date_on_sale_from', 'date_on_sale_to', 'total_sales', 'average_rating', 'stock_quantity', 'stock_status', 'manage_stock', 'downloadable', 'virtual', 'tax_status', 'tax_class' ) ) ) { + $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); + } + + // Trigger action so 3rd parties can deal with updated props. + do_action( 'woocommerce_product_object_updated_props', $product, $this->updated_props ); + + // After handling, we can reset the props array. + $this->updated_props = array(); + } + + /** + * For all stored terms in all taxonomies, save them to the DB. + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + * @since 3.0.0 + */ + protected function update_terms( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'category_ids', $changes ) ) { + $categories = $product->get_category_ids( 'edit' ); + + if ( empty( $categories ) && get_option( 'default_product_cat', 0 ) ) { + $categories = array( get_option( 'default_product_cat', 0 ) ); + } + + wp_set_post_terms( $product->get_id(), $categories, 'product_cat', false ); + } + if ( $force || array_key_exists( 'tag_ids', $changes ) ) { + wp_set_post_terms( $product->get_id(), $product->get_tag_ids( 'edit' ), 'product_tag', false ); + } + if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { + wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); + } + + _wc_recount_terms_by_product( $product->get_id() ); + } + + /** + * Update visibility terms based on props. + * + * @since 3.0.0 + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + protected function update_visibility( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_intersect( array( 'featured', 'stock_status', 'average_rating', 'catalog_visibility' ), array_keys( $changes ) ) ) { + $terms = array(); + + if ( $product->get_featured() ) { + $terms[] = 'featured'; + } + + if ( 'outofstock' === $product->get_stock_status() ) { + $terms[] = 'outofstock'; + } + + $rating = min( 5, NumberUtil::round( $product->get_average_rating(), 0 ) ); + + if ( $rating > 0 ) { + $terms[] = 'rated-' . $rating; + } + + switch ( $product->get_catalog_visibility() ) { + case 'hidden': + $terms[] = 'exclude-from-search'; + $terms[] = 'exclude-from-catalog'; + break; + case 'catalog': + $terms[] = 'exclude-from-search'; + break; + case 'search': + $terms[] = 'exclude-from-catalog'; + break; + } + + if ( ! is_wp_error( wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ) ) ) { + do_action( 'woocommerce_product_set_visibility', $product->get_id(), $product->get_catalog_visibility() ); + } + } + } + + /** + * Update attributes which are a mix of terms and meta data. + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + * @since 3.0.0 + */ + protected function update_attributes( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'attributes', $changes ) ) { + $attributes = $product->get_attributes(); + $meta_values = array(); + + if ( $attributes ) { + foreach ( $attributes as $attribute_key => $attribute ) { + $value = ''; + + if ( is_null( $attribute ) ) { + if ( taxonomy_exists( $attribute_key ) ) { + // Handle attributes that have been unset. + wp_set_object_terms( $product->get_id(), array(), $attribute_key ); + } elseif ( taxonomy_exists( urldecode( $attribute_key ) ) ) { + // Handle attributes that have been unset. + wp_set_object_terms( $product->get_id(), array(), urldecode( $attribute_key ) ); + } + continue; + + } elseif ( $attribute->is_taxonomy() ) { + wp_set_object_terms( $product->get_id(), wp_list_pluck( (array) $attribute->get_terms(), 'term_id' ), $attribute->get_name() ); + } else { + $value = wc_implode_text_attributes( $attribute->get_options() ); + } + + // Store in format WC uses in meta. + $meta_values[ $attribute_key ] = array( + 'name' => $attribute->get_name(), + 'value' => $value, + 'position' => $attribute->get_position(), + 'is_visible' => $attribute->get_visible() ? 1 : 0, + 'is_variation' => $attribute->get_variation() ? 1 : 0, + 'is_taxonomy' => $attribute->is_taxonomy() ? 1 : 0, + ); + } + } + // Note, we use wp_slash to add extra level of escaping. See https://codex.wordpress.org/Function_Reference/update_post_meta#Workaround. + $this->update_or_delete_post_meta( $product, '_product_attributes', wp_slash( $meta_values ) ); + } + } + + /** + * Update downloads. + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + * @return bool If updated or not. + */ + protected function update_downloads( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'downloads', $changes ) ) { + $downloads = $product->get_downloads(); + $meta_values = array(); + + if ( $downloads ) { + foreach ( $downloads as $key => $download ) { + // Store in format WC uses in meta. + $meta_values[ $key ] = $download->get_data(); + } + } + + if ( $product->is_type( 'variation' ) ) { + do_action( 'woocommerce_process_product_file_download_paths', $product->get_parent_id(), $product->get_id(), $downloads ); + } else { + do_action( 'woocommerce_process_product_file_download_paths', $product->get_id(), 0, $downloads ); + } + + return $this->update_or_delete_post_meta( $product, '_downloadable_files', wp_slash( $meta_values ) ); + } + return false; + } + + /** + * Make sure we store the product type and version (to track data changes). + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function update_version_and_type( &$product ) { + $old_type = WC_Product_Factory::get_product_type( $product->get_id() ); + $new_type = $product->get_type(); + + wp_set_object_terms( $product->get_id(), $new_type, 'product_type' ); + update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) ); + + // Action for the transition. + if ( $old_type !== $new_type ) { + $this->updated_props[] = 'product_type'; + do_action( 'woocommerce_product_type_changed', $product, $old_type, $new_type ); + } + } + + /** + * Clear any caches. + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function clear_caches( &$product ) { + wc_delete_product_transients( $product->get_id() ); + if ( $product->get_parent_id( 'edit' ) ) { + wc_delete_product_transients( $product->get_parent_id( 'edit' ) ); + WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_parent_id( 'edit' ) ); + } + WC_Cache_Helper::invalidate_attribute_count( array_keys( $product->get_attributes() ) ); + WC_Cache_Helper::invalidate_cache_group( 'product_' . $product->get_id() ); + } + + /* + |-------------------------------------------------------------------------- + | wc-product-functions.php methods + |-------------------------------------------------------------------------- + */ + + /** + * Returns an array of on sale products, as an array of objects with an + * ID and parent_id present. Example: $return[0]->id, $return[0]->parent_id. + * + * @return array + * @since 3.0.0 + */ + public function get_on_sale_products() { + global $wpdb; + + $exclude_term_ids = array(); + $outofstock_join = ''; + $outofstock_where = ''; + $non_published_where = ''; + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; + } + + if ( count( $exclude_term_ids ) ) { + $outofstock_join = " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = id'; + $outofstock_where = ' AND exclude_join.object_id IS NULL'; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + return $wpdb->get_results( + " + SELECT posts.ID as id, posts.post_parent as parent_id + FROM {$wpdb->posts} AS posts + INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id + $outofstock_join + WHERE posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status = 'publish' + AND lookup.onsale = 1 + $outofstock_where + AND posts.post_parent NOT IN ( + SELECT ID FROM `$wpdb->posts` as posts + WHERE posts.post_type = 'product' + AND posts.post_parent = 0 + AND posts.post_status != 'publish' + ) + GROUP BY posts.ID + " + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + /** + * Returns a list of product IDs ( id as key => parent as value) that are + * featured. Uses get_posts instead of wc_get_products since we want + * some extra meta queries and ALL products (posts_per_page = -1). + * + * @return array + * @since 3.0.0 + */ + public function get_featured_product_ids() { + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + return get_posts( + array( + 'post_type' => array( 'product', 'product_variation' ), + 'posts_per_page' => -1, + 'post_status' => 'publish', + 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'relation' => 'AND', + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['featured'] ), + ), + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), + 'operator' => 'NOT IN', + ), + ), + 'fields' => 'id=>parent', + ) + ); + } + + /** + * Check if product sku is found for any other product IDs. + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @param string $sku Will be slashed to work around https://core.trac.wordpress.org/ticket/27421. + * @return bool + */ + public function is_existing_sku( $product_id, $sku ) { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + return (bool) $wpdb->get_var( + $wpdb->prepare( + " + SELECT posts.ID + FROM {$wpdb->posts} as posts + INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id + WHERE + posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status != 'trash' + AND lookup.sku = %s + AND lookup.product_id <> %d + LIMIT 1 + ", + wp_slash( $sku ), + $product_id + ) + ); + } + + /** + * Return product ID based on SKU. + * + * @since 3.0.0 + * @param string $sku Product SKU. + * @return int + */ + public function get_product_id_by_sku( $sku ) { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $id = $wpdb->get_var( + $wpdb->prepare( + " + SELECT posts.ID + FROM {$wpdb->posts} as posts + INNER JOIN {$wpdb->wc_product_meta_lookup} AS lookup ON posts.ID = lookup.product_id + WHERE + posts.post_type IN ( 'product', 'product_variation' ) + AND posts.post_status != 'trash' + AND lookup.sku = %s + LIMIT 1 + ", + $sku + ) + ); + + return (int) apply_filters( 'woocommerce_get_product_id_by_sku', $id, $sku ); + } + + /** + * Returns an array of IDs of products that have sales starting soon. + * + * @since 3.0.0 + * @return array + */ + public function get_starting_sales() { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + return $wpdb->get_col( + $wpdb->prepare( + "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta + LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id + LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id + WHERE postmeta.meta_key = '_sale_price_dates_from' + AND postmeta_2.meta_key = '_price' + AND postmeta_3.meta_key = '_sale_price' + AND postmeta.meta_value > 0 + AND postmeta.meta_value < %s + AND postmeta_2.meta_value != postmeta_3.meta_value", + time() + ) + ); + } + + /** + * Returns an array of IDs of products that have sales which are due to end. + * + * @since 3.0.0 + * @return array + */ + public function get_ending_sales() { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + return $wpdb->get_col( + $wpdb->prepare( + "SELECT postmeta.post_id FROM {$wpdb->postmeta} as postmeta + LEFT JOIN {$wpdb->postmeta} as postmeta_2 ON postmeta.post_id = postmeta_2.post_id + LEFT JOIN {$wpdb->postmeta} as postmeta_3 ON postmeta.post_id = postmeta_3.post_id + WHERE postmeta.meta_key = '_sale_price_dates_to' + AND postmeta_2.meta_key = '_price' + AND postmeta_3.meta_key = '_regular_price' + AND postmeta.meta_value > 0 + AND postmeta.meta_value < %s + AND postmeta_2.meta_value != postmeta_3.meta_value", + time() + ) + ); + } + + /** + * Find a matching (enabled) variation within a variable product. + * + * @since 3.0.0 + * @param WC_Product $product Variable product. + * @param array $match_attributes Array of attributes we want to try to match. + * @return int Matching variation ID or 0. + */ + public function find_matching_product_variation( $product, $match_attributes = array() ) { + global $wpdb; + + $meta_attribute_names = array(); + + // Get attributes to match in meta. + foreach ( $product->get_attributes() as $attribute ) { + if ( ! $attribute->get_variation() ) { + continue; + } + $meta_attribute_names[] = 'attribute_' . sanitize_title( $attribute->get_name() ); + } + + // Get the attributes of the variations. + $query = $wpdb->prepare( + " + SELECT postmeta.post_id, postmeta.meta_key, postmeta.meta_value, posts.menu_order FROM {$wpdb->postmeta} as postmeta + LEFT JOIN {$wpdb->posts} as posts ON postmeta.post_id=posts.ID + WHERE postmeta.post_id IN ( + SELECT ID FROM {$wpdb->posts} + WHERE {$wpdb->posts}.post_parent = %d + AND {$wpdb->posts}.post_status = 'publish' + AND {$wpdb->posts}.post_type = 'product_variation' + ) + ", + $product->get_id() + ); + + $query .= " AND postmeta.meta_key IN ( '" . implode( "','", array_map( 'esc_sql', $meta_attribute_names ) ) . "' )"; + + $query .= ' ORDER BY posts.menu_order ASC, postmeta.post_id ASC;'; + + $attributes = $wpdb->get_results( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + + if ( ! $attributes ) { + return 0; + } + + $sorted_meta = array(); + + foreach ( $attributes as $m ) { + $sorted_meta[ $m->post_id ][ $m->meta_key ] = $m->meta_value; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + } + + /** + * Check each variation to find the one that matches the $match_attributes. + * + * Note: Not all meta fields will be set which is why we check existance. + */ + foreach ( $sorted_meta as $variation_id => $variation ) { + $match = true; + + // Loop over the variation meta keys and values i.e. what is saved to the products. Note: $attribute_value is empty when 'any' is in use. + foreach ( $variation as $attribute_key => $attribute_value ) { + $match_any_value = '' === $attribute_value; + + if ( ! $match_any_value && ! array_key_exists( $attribute_key, $match_attributes ) ) { + $match = false; // Requires a selection but no value was provide. + } + + if ( array_key_exists( $attribute_key, $match_attributes ) ) { // Value to match was provided. + if ( ! $match_any_value && $match_attributes[ $attribute_key ] !== $attribute_value ) { + $match = false; // Provided value does not match variation. + } + } + } + + if ( true === $match ) { + return $variation_id; + } + } + + if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { + /** + * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. + * Fallback is here because there are cases where data will be 'synced' but the product version will remain the same. + */ + return ( array_map( 'sanitize_title', $match_attributes ) === $match_attributes ) ? 0 : $this->find_matching_product_variation( $product, array_map( 'sanitize_title', $match_attributes ) ); + } + + return 0; + } + + /** + * Creates all possible combinations of variations from the attributes, without creating duplicates. + * + * @since 3.6.0 + * @todo Add to interface in 4.0. + * @param WC_Product $product Variable product. + * @param int $limit Limit the number of created variations. + * @return int Number of created variations. + */ + public function create_all_product_variations( $product, $limit = -1 ) { + $count = 0; + + if ( ! $product ) { + return $count; + } + + $attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' ); + + if ( empty( $attributes ) ) { + return $count; + } + + // Get existing variations so we don't create duplicates. + $existing_variations = array_map( 'wc_get_product', $product->get_children() ); + $existing_attributes = array(); + + foreach ( $existing_variations as $existing_variation ) { + $existing_attributes[] = $existing_variation->get_attributes(); + } + + $possible_attributes = array_reverse( wc_array_cartesian( $attributes ) ); + + foreach ( $possible_attributes as $possible_attribute ) { + // Allow any order if key/values -- do not use strict mode. + if ( in_array( $possible_attribute, $existing_attributes ) ) { // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict + continue; + } + $variation = wc_get_product_object( 'variation' ); + $variation->set_parent_id( $product->get_id() ); + $variation->set_attributes( $possible_attribute ); + $variation_id = $variation->save(); + + do_action( 'product_variation_linked', $variation_id ); + + $count ++; + + if ( $limit > 0 && $count >= $limit ) { + break; + } + } + + return $count; + } + + /** + * Make sure all variations have a sort order set so they can be reordered correctly. + * + * @param int $parent_id Product ID. + */ + public function sort_all_product_variations( $parent_id ) { + global $wpdb; + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product_variation' AND post_parent = %d AND post_status = 'publish' ORDER BY menu_order ASC, ID ASC", + $parent_id + ) + ); + $index = 1; + + foreach ( $ids as $id ) { + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->update( $wpdb->posts, array( 'menu_order' => ( $index++ ) ), array( 'ID' => absint( $id ) ) ); + } + } + + /** + * Return a list of related products (using data like categories and IDs). + * + * @since 3.0.0 + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * @param int $product_id Product ID. + * @return array + */ + public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ) { + global $wpdb; + + $args = array( + 'categories' => $cats_array, + 'tags' => $tags_array, + 'exclude_ids' => $exclude_ids, + 'limit' => $limit + 10, + ); + + $related_product_query = (array) apply_filters( 'woocommerce_product_related_posts_query', $this->get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit + 10 ), $product_id, $args ); + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared + return $wpdb->get_col( implode( ' ', $related_product_query ) ); + } + + /** + * Builds the related posts query. + * + * @since 3.0.0 + * + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * + * @return array + */ + public function get_related_products_query( $cats_array, $tags_array, $exclude_ids, $limit ) { + global $wpdb; + + $include_term_ids = array_merge( $cats_array, $tags_array ); + $exclude_term_ids = array(); + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + if ( $product_visibility_term_ids['exclude-from-catalog'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; + } + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; + } + + $query = array( + 'fields' => " + SELECT DISTINCT ID FROM {$wpdb->posts} p + ", + 'join' => '', + 'where' => " + WHERE 1=1 + AND p.post_status = 'publish' + AND p.post_type = 'product' + + ", + 'limits' => ' + LIMIT ' . absint( $limit ) . ' + ', + ); + + if ( count( $exclude_term_ids ) ) { + $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; + $query['where'] .= ' AND exclude_join.object_id IS NULL'; + } + + if ( count( $include_term_ids ) ) { + $query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $include_term_ids ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; + } + + if ( count( $exclude_ids ) ) { + $query['where'] .= ' AND p.ID NOT IN ( ' . implode( ',', array_map( 'absint', $exclude_ids ) ) . ' )'; + } + + return $query; + } + + /** + * Update a product's stock amount directly in the database. + * + * Updates both post meta and lookup tables. Ignores manage stock setting on the product. + * + * @param int $product_id_with_stock Product ID. + * @param int|float|null $stock_quantity Stock quantity. + */ + protected function set_product_stock( $product_id_with_stock, $stock_quantity ) { + global $wpdb; + + // Generate SQL. + $sql = $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", + $stock_quantity, + $product_id_with_stock + ); + + $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $stock_quantity, 'set' ); + + $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared + + // Cache delete is required (not only) to set correct data for lookup table (which reads from cache). + // Sometimes I wonder if it shouldn't be part of update_lookup_table. + wp_cache_delete( $product_id_with_stock, 'post_meta' ); + + $this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' ); + } + + /** + * Update a product's stock amount directly. + * + * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). + * Ignores manage stock setting on the product and sets quantities directly in the db: post meta and lookup tables. + * Uses locking to update the quantity. If the lock is not acquired, change is lost. + * + * @since 3.0.0 this supports set, increase and decrease. + * @param int $product_id_with_stock Product ID. + * @param int|float|null $stock_quantity Stock quantity. + * @param string $operation Set, increase and decrease. + * @return int|float New stock level. + */ + public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ) { + global $wpdb; + + // Ensures a row exists to update. + add_post_meta( $product_id_with_stock, '_stock', 0, true ); + + if ( 'set' === $operation ) { + $new_stock = wc_stock_amount( $stock_quantity ); + + // Generate SQL. + $sql = $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='_stock'", + $new_stock, + $product_id_with_stock + ); + } else { + $current_stock = wc_stock_amount( + $wpdb->get_var( + $wpdb->prepare( + "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key='_stock';", + $product_id_with_stock + ) + ) + ); + + // Calculate new value for filter below. Set multiplier to subtract or add the meta_value. + switch ( $operation ) { + case 'increase': + $new_stock = $current_stock + wc_stock_amount( $stock_quantity ); + $multiplier = 1; + break; + default: + $new_stock = $current_stock - wc_stock_amount( $stock_quantity ); + $multiplier = -1; + break; + } + + // Generate SQL. + $sql = $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = meta_value %+f WHERE post_id = %d AND meta_key='_stock'", + wc_stock_amount( $stock_quantity ) * $multiplier, // This will either subtract or add depending on operation. + $product_id_with_stock + ); + } + + $sql = apply_filters( 'woocommerce_update_product_stock_query', $sql, $product_id_with_stock, $new_stock, $operation ); + + $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared + + // Cache delete is required (not only) to set correct data for lookup table (which reads from cache). + // Sometimes I wonder if it shouldn't be part of update_lookup_table. + wp_cache_delete( $product_id_with_stock, 'post_meta' ); + + $this->update_lookup_table( $product_id_with_stock, 'wc_product_meta_lookup' ); + + /** + * Fire an action for this direct update so it can be detected by other code. + * + * @since 3.6 + * @param int $product_id_with_stock Product ID that was updated directly. + */ + do_action( 'woocommerce_updated_product_stock', $product_id_with_stock ); + + return $new_stock; + } + + /** + * Update a product's sale count directly. + * + * Uses queries rather than update_post_meta so we can do this in one query for performance. + * + * @since 3.0.0 this supports set, increase and decrease. + * @param int $product_id Product ID. + * @param int|null $quantity Quantity. + * @param string $operation set, increase and decrease. + */ + public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ) { + global $wpdb; + add_post_meta( $product_id, 'total_sales', 0, true ); + + // Update stock in DB directly. + switch ( $operation ) { + case 'increase': + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = meta_value + %f WHERE post_id = %d AND meta_key='total_sales'", + $quantity, + $product_id + ) + ); + break; + case 'decrease': + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = meta_value - %f WHERE post_id = %d AND meta_key='total_sales'", + $quantity, + $product_id + ) + ); + break; + default: + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} SET meta_value = %f WHERE post_id = %d AND meta_key='total_sales'", + $quantity, + $product_id + ) + ); + break; + } + + wp_cache_delete( $product_id, 'post_meta' ); + + $this->update_lookup_table( $product_id, 'wc_product_meta_lookup' ); + + /** + * Fire an action for this direct update so it can be detected by other code. + * + * @since 3.6 + * @param int $product_id Product ID that was updated directly. + */ + do_action( 'woocommerce_updated_product_sales', $product_id ); + } + + /** + * Update a products average rating meta. + * + * @since 3.0.0 + * @todo Deprecate unused function? + * @param WC_Product $product Product object. + */ + public function update_average_rating( $product ) { + update_post_meta( $product->get_id(), '_wc_average_rating', $product->get_average_rating( 'edit' ) ); + self::update_visibility( $product, true ); + } + + /** + * Update a products review count meta. + * + * @since 3.0.0 + * @todo Deprecate unused function? + * @param WC_Product $product Product object. + */ + public function update_review_count( $product ) { + update_post_meta( $product->get_id(), '_wc_review_count', $product->get_review_count( 'edit' ) ); + } + + /** + * Update a products rating counts. + * + * @since 3.0.0 + * @todo Deprecate unused function? + * @param WC_Product $product Product object. + */ + public function update_rating_counts( $product ) { + update_post_meta( $product->get_id(), '_wc_rating_count', $product->get_rating_counts( 'edit' ) ); + } + + /** + * Get shipping class ID by slug. + * + * @since 3.0.0 + * @param string $slug Product shipping class slug. + * @return int|false + */ + public function get_shipping_class_id_by_slug( $slug ) { + $shipping_class_term = get_term_by( 'slug', $slug, 'product_shipping_class' ); + if ( $shipping_class_term ) { + return $shipping_class_term->term_id; + } else { + return false; + } + } + + /** + * Returns an array of products. + * + * @param array $args Args to pass to WC_Product_Query(). + * @return array|object + * @see wc_get_products + */ + public function get_products( $args = array() ) { + $query = new WC_Product_Query( $args ); + return $query->get_products(); + } + + /** + * Search product data for a term and return ids. + * + * @param string $term Search term. + * @param string $type Type of product. + * @param bool $include_variations Include variations in search or not. + * @param bool $all_statuses Should we search all statuses or limit to published. + * @param null|int $limit Limit returned results. @since 3.5.0. + * @param null|array $include Keep specific results. @since 3.6.0. + * @param null|array $exclude Discard specific results. @since 3.6.0. + * @return array of ids + */ + public function search_products( $term, $type = '', $include_variations = false, $all_statuses = false, $limit = null, $include = null, $exclude = null ) { + global $wpdb; + + $custom_results = apply_filters( 'woocommerce_product_pre_search_products', false, $term, $type, $include_variations, $all_statuses, $limit ); + + if ( is_array( $custom_results ) ) { + return $custom_results; + } + + $post_types = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' ); + $join_query = ''; + $type_where = ''; + $status_where = ''; + $limit_query = ''; + + // When searching variations we should include the parent's meta table for use in searches. + if ( $include_variations ) { + $join_query = " LEFT JOIN {$wpdb->wc_product_meta_lookup} parent_wc_product_meta_lookup + ON posts.post_type = 'product_variation' AND parent_wc_product_meta_lookup.product_id = posts.post_parent "; + } + + /** + * Hook woocommerce_search_products_post_statuses. + * + * @since 3.7.0 + * @param array $post_statuses List of post statuses. + */ + $post_statuses = apply_filters( + 'woocommerce_search_products_post_statuses', + current_user_can( 'edit_private_products' ) ? array( 'private', 'publish' ) : array( 'publish' ) + ); + + // See if search term contains OR keywords. + if ( stristr( $term, ' or ' ) ) { + $term_groups = preg_split( '/\s+or\s+/i', $term ); + } else { + $term_groups = array( $term ); + } + + $search_where = ''; + $search_queries = array(); + + foreach ( $term_groups as $term_group ) { + // Parse search terms. + if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', $term_group, $matches ) ) { + $search_terms = $this->get_valid_search_terms( $matches[0] ); + $count = count( $search_terms ); + + // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence. + if ( 9 < $count || 0 === $count ) { + $search_terms = array( $term_group ); + } + } else { + $search_terms = array( $term_group ); + } + + $term_group_query = ''; + $searchand = ''; + + foreach ( $search_terms as $search_term ) { + $like = '%' . $wpdb->esc_like( $search_term ) . '%'; + + // Variations should also search the parent's meta table for fallback fields. + if ( $include_variations ) { + $variation_query = $wpdb->prepare( " OR ( wc_product_meta_lookup.sku = '' AND parent_wc_product_meta_lookup.sku LIKE %s ) ", $like ); + } else { + $variation_query = ''; + } + + $term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) $variation_query)", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $searchand = ' AND '; + } + + if ( $term_group_query ) { + $search_queries[] = $term_group_query; + } + } + + if ( ! empty( $search_queries ) ) { + $search_where = ' AND (' . implode( ') OR (', $search_queries ) . ') '; + } + + if ( ! empty( $include ) && is_array( $include ) ) { + $search_where .= ' AND posts.ID IN(' . implode( ',', array_map( 'absint', $include ) ) . ') '; + } + + if ( ! empty( $exclude ) && is_array( $exclude ) ) { + $search_where .= ' AND posts.ID NOT IN(' . implode( ',', array_map( 'absint', $exclude ) ) . ') '; + } + + if ( 'virtual' === $type ) { + $type_where = ' AND ( wc_product_meta_lookup.virtual = 1 ) '; + } elseif ( 'downloadable' === $type ) { + $type_where = ' AND ( wc_product_meta_lookup.downloadable = 1 ) '; + } + + if ( ! $all_statuses ) { + $status_where = " AND posts.post_status IN ('" . implode( "','", $post_statuses ) . "') "; + } + + if ( $limit ) { + $limit_query = $wpdb->prepare( ' LIMIT %d ', $limit ); + } + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $search_results = $wpdb->get_results( + // phpcs:disable + "SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts + LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id + $join_query + WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "') + $search_where + $status_where + $type_where + ORDER BY posts.post_parent ASC, posts.post_title ASC + $limit_query + " + // phpcs:enable + ); + + $product_ids = wp_parse_id_list( array_merge( wp_list_pluck( $search_results, 'product_id' ), wp_list_pluck( $search_results, 'parent_id' ) ) ); + + if ( is_numeric( $term ) ) { + $post_id = absint( $term ); + $post_type = get_post_type( $post_id ); + + if ( 'product_variation' === $post_type && $include_variations ) { + $product_ids[] = $post_id; + } elseif ( 'product' === $post_type ) { + $product_ids[] = $post_id; + } + + $product_ids[] = wp_get_post_parent_id( $post_id ); + } + + return wp_parse_id_list( $product_ids ); + } + + /** + * Get the product type based on product ID. + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @return bool|string + */ + public function get_product_type( $product_id ) { + $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . '_type_' . $product_id; + $product_type = wp_cache_get( $cache_key, 'products' ); + + if ( $product_type ) { + return $product_type; + } + + $post_type = get_post_type( $product_id ); + + if ( 'product_variation' === $post_type ) { + $product_type = 'variation'; + } elseif ( 'product' === $post_type ) { + $terms = get_the_terms( $product_id, 'product_type' ); + $product_type = ! empty( $terms ) && ! is_wp_error( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple'; + } else { + $product_type = false; + } + + wp_cache_set( $cache_key, $product_type, 'products' ); + + return $product_type; + } + + /** + * Add ability to get products by 'reviews_allowed' in WC_Product_Query. + * + * @since 3.2.0 + * @param string $where Where clause. + * @param WP_Query $wp_query WP_Query instance. + * @return string + */ + public function reviews_allowed_query_where( $where, $wp_query ) { + global $wpdb; + + if ( isset( $wp_query->query_vars['reviews_allowed'] ) && is_bool( $wp_query->query_vars['reviews_allowed'] ) ) { + if ( $wp_query->query_vars['reviews_allowed'] ) { + $where .= " AND $wpdb->posts.comment_status = 'open'"; + } else { + $where .= " AND $wpdb->posts.comment_status = 'closed'"; + } + } + + return $where; + } + + /** + * Get valid WP_Query args from a WC_Product_Query's query variables. + * + * @since 3.2.0 + * @param array $query_vars Query vars from a WC_Product_Query. + * @return array + */ + protected function get_wp_query_args( $query_vars ) { + + // Map query vars to ones that get_wp_query_args or WP_Query recognize. + $key_mapping = array( + 'status' => 'post_status', + 'page' => 'paged', + 'include' => 'post__in', + 'stock_quantity' => 'stock', + 'average_rating' => 'wc_average_rating', + 'review_count' => 'wc_review_count', + ); + foreach ( $key_mapping as $query_key => $db_key ) { + if ( isset( $query_vars[ $query_key ] ) ) { + $query_vars[ $db_key ] = $query_vars[ $query_key ]; + unset( $query_vars[ $query_key ] ); + } + } + + // Map boolean queries that are stored as 'yes'/'no' in the DB to 'yes' or 'no'. + $boolean_queries = array( + 'virtual', + 'downloadable', + 'sold_individually', + 'manage_stock', + ); + foreach ( $boolean_queries as $boolean_query ) { + if ( isset( $query_vars[ $boolean_query ] ) && '' !== $query_vars[ $boolean_query ] ) { + $query_vars[ $boolean_query ] = $query_vars[ $boolean_query ] ? 'yes' : 'no'; + } + } + + // These queries cannot be auto-generated so we have to remove them and build them manually. + $manual_queries = array( + 'sku' => '', + 'featured' => '', + 'visibility' => '', + ); + foreach ( $manual_queries as $key => $manual_query ) { + if ( isset( $query_vars[ $key ] ) ) { + $manual_queries[ $key ] = $query_vars[ $key ]; + unset( $query_vars[ $key ] ); + } + } + + $wp_query_args = parent::get_wp_query_args( $query_vars ); + + if ( ! isset( $wp_query_args['date_query'] ) ) { + $wp_query_args['date_query'] = array(); + } + if ( ! isset( $wp_query_args['meta_query'] ) ) { + $wp_query_args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + // Handle product types. + if ( 'variation' === $query_vars['type'] ) { + $wp_query_args['post_type'] = 'product_variation'; + } elseif ( is_array( $query_vars['type'] ) && in_array( 'variation', $query_vars['type'], true ) ) { + $wp_query_args['post_type'] = array( 'product_variation', 'product' ); + $wp_query_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'relation' => 'OR', + array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $query_vars['type'], + ), + array( + 'taxonomy' => 'product_type', + 'field' => 'id', + 'operator' => 'NOT EXISTS', + ), + ); + } else { + $wp_query_args['post_type'] = 'product'; + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $query_vars['type'], + ); + } + + // Handle product categories. + if ( ! empty( $query_vars['category'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_cat', + 'field' => 'slug', + 'terms' => $query_vars['category'], + ); + } + + // Handle product tags. + if ( ! empty( $query_vars['tag'] ) ) { + unset( $wp_query_args['tag'] ); + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_tag', + 'field' => 'slug', + 'terms' => $query_vars['tag'], + ); + } + + // Handle shipping classes. + if ( ! empty( $query_vars['shipping_class'] ) ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_shipping_class', + 'field' => 'slug', + 'terms' => $query_vars['shipping_class'], + ); + } + + // Handle total_sales. + // This query doesn't get auto-generated since the meta key doesn't have the underscore prefix. + if ( isset( $query_vars['total_sales'] ) && '' !== $query_vars['total_sales'] ) { + $wp_query_args['meta_query'][] = array( + 'key' => 'total_sales', + 'value' => absint( $query_vars['total_sales'] ), + 'compare' => '=', + ); + } + + // Handle SKU. + if ( $manual_queries['sku'] ) { + // Check for existing values if wildcard is used. + if ( '*' === $manual_queries['sku'] ) { + $wp_query_args['meta_query'][] = array( + array( + 'key' => '_sku', + 'compare' => 'EXISTS', + ), + array( + 'key' => '_sku', + 'value' => '', + 'compare' => '!=', + ), + ); + } else { + $wp_query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $manual_queries['sku'], + 'compare' => 'LIKE', + ); + } + } + + // Handle featured. + if ( '' !== $manual_queries['featured'] ) { + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + if ( $manual_queries['featured'] ) { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['featured'] ), + ); + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['exclude-from-catalog'] ), + 'operator' => 'NOT IN', + ); + } else { + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_term_ids['featured'] ), + 'operator' => 'NOT IN', + ); + } + } + + // Handle visibility. + if ( $manual_queries['visibility'] ) { + switch ( $manual_queries['visibility'] ) { + case 'search': + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'slug', + 'terms' => array( 'exclude-from-search' ), + 'operator' => 'NOT IN', + ); + break; + case 'catalog': + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'slug', + 'terms' => array( 'exclude-from-catalog' ), + 'operator' => 'NOT IN', + ); + break; + case 'visible': + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'slug', + 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), + 'operator' => 'NOT IN', + ); + break; + case 'hidden': + $wp_query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'slug', + 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), + 'operator' => 'AND', + ); + break; + } + } + + // Handle date queries. + $date_queries = array( + 'date_created' => 'post_date', + 'date_modified' => 'post_modified', + 'date_on_sale_from' => '_sale_price_dates_from', + 'date_on_sale_to' => '_sale_price_dates_to', + ); + foreach ( $date_queries as $query_var_key => $db_key ) { + if ( isset( $query_vars[ $query_var_key ] ) && '' !== $query_vars[ $query_var_key ] ) { + + // Remove any existing meta queries for the same keys to prevent conflicts. + $existing_queries = wp_list_pluck( $wp_query_args['meta_query'], 'key', true ); + foreach ( $existing_queries as $query_index => $query_contents ) { + unset( $wp_query_args['meta_query'][ $query_index ] ); + } + + $wp_query_args = $this->parse_date_for_wp_query( $query_vars[ $query_var_key ], $db_key, $wp_query_args ); + } + } + + // Handle paginate. + if ( ! isset( $query_vars['paginate'] ) || ! $query_vars['paginate'] ) { + $wp_query_args['no_found_rows'] = true; + } + + // Handle reviews_allowed. + if ( isset( $query_vars['reviews_allowed'] ) && is_bool( $query_vars['reviews_allowed'] ) ) { + add_filter( 'posts_where', array( $this, 'reviews_allowed_query_where' ), 10, 2 ); + } + + // Handle orderby. + if ( isset( $query_vars['orderby'] ) && 'include' === $query_vars['orderby'] ) { + $wp_query_args['orderby'] = 'post__in'; + } + + return apply_filters( 'woocommerce_product_data_store_cpt_get_products_query', $wp_query_args, $query_vars, $this ); + } + + /** + * Query for Products matching specific criteria. + * + * @since 3.2.0 + * + * @param array $query_vars Query vars from a WC_Product_Query. + * + * @return array|object + */ + public function query( $query_vars ) { + $args = $this->get_wp_query_args( $query_vars ); + + if ( ! empty( $args['errors'] ) ) { + $query = (object) array( + 'posts' => array(), + 'found_posts' => 0, + 'max_num_pages' => 0, + ); + } else { + $query = new WP_Query( $args ); + } + + if ( isset( $query_vars['return'] ) && 'objects' === $query_vars['return'] && ! empty( $query->posts ) ) { + // Prime caches before grabbing objects. + update_post_caches( $query->posts, array( 'product', 'product_variation' ) ); + } + + $products = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_product', $query->posts ) ); + + if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { + return (object) array( + 'products' => $products, + 'total' => $query->found_posts, + 'max_num_pages' => $query->max_num_pages, + ); + } + + return $products; + } + + /** + * Get data to save to a lookup table. + * + * @since 3.6.0 + * @param int $id ID of object to update. + * @param string $table Lookup table name. + * @return array + */ + protected function get_data_for_lookup_table( $id, $table ) { + if ( 'wc_product_meta_lookup' === $table ) { + $price_meta = (array) get_post_meta( $id, '_price', false ); + $manage_stock = get_post_meta( $id, '_manage_stock', true ); + $stock = 'yes' === $manage_stock ? wc_stock_amount( get_post_meta( $id, '_stock', true ) ) : null; + $price = wc_format_decimal( get_post_meta( $id, '_price', true ) ); + $sale_price = wc_format_decimal( get_post_meta( $id, '_sale_price', true ) ); + return array( + 'product_id' => absint( $id ), + 'sku' => get_post_meta( $id, '_sku', true ), + 'virtual' => 'yes' === get_post_meta( $id, '_virtual', true ) ? 1 : 0, + 'downloadable' => 'yes' === get_post_meta( $id, '_downloadable', true ) ? 1 : 0, + 'min_price' => reset( $price_meta ), + 'max_price' => end( $price_meta ), + 'onsale' => $sale_price && $price === $sale_price ? 1 : 0, + 'stock_quantity' => $stock, + 'stock_status' => get_post_meta( $id, '_stock_status', true ), + 'rating_count' => array_sum( (array) get_post_meta( $id, '_wc_rating_count', true ) ), + 'average_rating' => get_post_meta( $id, '_wc_average_rating', true ), + 'total_sales' => get_post_meta( $id, 'total_sales', true ), + 'tax_status' => get_post_meta( $id, '_tax_status', true ), + 'tax_class' => get_post_meta( $id, '_tax_class', true ), + ); + } + return array(); + } + + /** + * Get primary key name for lookup table. + * + * @since 3.6.0 + * @param string $table Lookup table name. + * @return string + */ + protected function get_primary_key_for_lookup_table( $table ) { + if ( 'wc_product_meta_lookup' === $table ) { + return 'product_id'; + } + return ''; + } + + /** + * Returns query statement for getting current `_stock` of a product. + * + * @internal MAX function below is used to make sure result is a scalar. + * @param int $product_id Product ID. + * @return string|void Query statement. + */ + public function get_query_for_stock( $product_id ) { + global $wpdb; + return $wpdb->prepare( + " + SELECT COALESCE ( MAX( meta_value ), 0 ) FROM $wpdb->postmeta as meta_table + WHERE meta_table.meta_key = '_stock' + AND meta_table.post_id = %d + ", + $product_id + ); + } +} diff --git a/includes/data-stores/class-wc-product-grouped-data-store-cpt.php b/includes/data-stores/class-wc-product-grouped-data-store-cpt.php new file mode 100644 index 0000000..3f7ea7b --- /dev/null +++ b/includes/data-stores/class-wc-product-grouped-data-store-cpt.php @@ -0,0 +1,100 @@ + '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 Product object. + */ + protected function handle_updated_props( &$product ) { + if ( in_array( 'children', $this->updated_props, true ) ) { + $this->update_prices_from_children( $product ); + } + parent::handle_updated_props( $product ); + } + + /** + * Sync grouped product prices with children. + * + * @since 3.0.0 + * @param WC_Product|int $product Product object or product ID. + */ + public function sync_price( &$product ) { + $this->update_prices_from_children( $product ); + } + + /** + * Loop over child products and update the grouped product prices. + * + * @param WC_Product $product Product object. + */ + protected function update_prices_from_children( &$product ) { + $child_prices = array(); + foreach ( $product->get_children( 'edit' ) as $child_id ) { + $child = wc_get_product( $child_id ); + if ( $child ) { + $child_prices[] = $child->get_price( 'edit' ); + } + } + $child_prices = array_filter( $child_prices ); + delete_post_meta( $product->get_id(), '_price' ); + delete_post_meta( $product->get_id(), '_sale_price' ); + delete_post_meta( $product->get_id(), '_regular_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 ) ); + } + + $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); + + /** + * Fire an action for this direct update so it can be detected by other code. + * + * @since 3.6 + * @param int $product_id Product ID that was updated directly. + */ + do_action( 'woocommerce_updated_product_price', $product->get_id() ); + } +} 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 0000000..5b91e54 --- /dev/null +++ b/includes/data-stores/class-wc-product-variable-data-store-cpt.php @@ -0,0 +1,710 @@ +get_id(), '_product_attributes', true ); + + if ( ! empty( $meta_attributes ) && is_array( $meta_attributes ) ) { + $attributes = array(); + $force_update = false; + foreach ( $meta_attributes as $meta_attribute_key => $meta_attribute_value ) { + $meta_value = array_merge( + array( + 'name' => '', + 'value' => '', + 'position' => 0, + 'is_visible' => 0, + 'is_variation' => 0, + 'is_taxonomy' => 0, + ), + (array) $meta_attribute_value + ); + + // Maintain data integrity. 4.9 changed sanitization functions - update the values here so variations function correctly. + if ( $meta_value['is_variation'] && strstr( $meta_value['name'], '/' ) && sanitize_title( $meta_value['name'] ) !== $meta_attribute_key ) { + global $wpdb; + + $old_slug = 'attribute_' . $meta_attribute_key; + $new_slug = 'attribute_' . sanitize_title( $meta_value['name'] ); + $old_meta_rows = $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s;", $old_slug ) ); // WPCS: db call ok, cache ok. + + if ( $old_meta_rows ) { + foreach ( $old_meta_rows as $old_meta_row ) { + update_post_meta( $old_meta_row->post_id, $new_slug, $old_meta_row->meta_value ); + } + } + + $force_update = true; + } + + // Check if is a taxonomy attribute. + if ( ! empty( $meta_value['is_taxonomy'] ) ) { + if ( ! taxonomy_exists( $meta_value['name'] ) ) { + continue; + } + $id = wc_attribute_taxonomy_id_by_name( $meta_value['name'] ); + $options = wc_get_object_terms( $product->get_id(), $meta_value['name'], 'term_id' ); + } else { + $id = 0; + $options = wc_get_text_attributes( $meta_value['value'] ); + } + + $attribute = new WC_Product_Attribute(); + $attribute->set_id( $id ); + $attribute->set_name( $meta_value['name'] ); + $attribute->set_options( $options ); + $attribute->set_position( $meta_value['position'] ); + $attribute->set_visible( $meta_value['is_visible'] ); + $attribute->set_variation( $meta_value['is_variation'] ); + $attributes[] = $attribute; + } + $product->set_attributes( $attributes ); + + if ( $force_update ) { + $this->update_attributes( $product, true ); + } + } + } + + /** + * Read product data. + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + */ + protected function read_product_data( &$product ) { + parent::read_product_data( $product ); + + // Make sure data which does not apply to variables is unset. + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + } + + /** + * Loads variation child IDs. + * + * @param WC_Product $product Product object. + * @param bool $force_read True to bypass the transient. + * + * @return array + */ + public function read_children( &$product, $force_read = false ) { + $children_transient_name = 'wc_product_children_' . $product->get_id(); + $children = get_transient( $children_transient_name ); + + if ( empty( $children ) || ! is_array( $children ) || ! isset( $children['all'] ) || ! isset( $children['visible'] ) || $force_read ) { + $all_args = array( + 'post_parent' => $product->get_id(), + 'post_type' => 'product_variation', + 'orderby' => array( + 'menu_order' => 'ASC', + 'ID' => 'ASC', + ), + 'fields' => 'ids', + 'post_status' => array( 'publish', 'private' ), + 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts + ); + + $visible_only_args = $all_args; + $visible_only_args['post_status'] = 'publish'; + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $visible_only_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'outofstock', + 'operator' => 'NOT IN', + ); + } + $children['all'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $all_args, $product, false ) ); + $children['visible'] = get_posts( apply_filters( 'woocommerce_variable_children_args', $visible_only_args, $product, true ) ); + + set_transient( $children_transient_name, $children, DAY_IN_SECONDS * 30 ); + } + + $children['all'] = wp_parse_id_list( (array) $children['all'] ); + $children['visible'] = wp_parse_id_list( (array) $children['visible'] ); + + return $children; + } + + /** + * Loads an array of attributes used for variations, as well as their possible values. + * + * @param WC_Product $product Product object. + * + * @return array + */ + public function read_variation_attributes( &$product ) { + global $wpdb; + + $variation_attributes = array(); + $attributes = $product->get_attributes(); + $child_ids = $product->get_children(); + $cache_key = WC_Cache_Helper::get_cache_prefix( 'product_' . $product->get_id() ) . 'product_variation_attributes_' . $product->get_id(); + $cache_group = 'products'; + $cached_data = wp_cache_get( $cache_key, $cache_group ); + + if ( false !== $cached_data ) { + return $cached_data; + } + + if ( ! empty( $attributes ) ) { + foreach ( $attributes as $attribute ) { + if ( empty( $attribute['is_variation'] ) ) { + continue; + } + + // Get possible values for this attribute, for only visible variations. + if ( ! empty( $child_ids ) ) { + $format = array_fill( 0, count( $child_ids ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids; + $values = array_unique( + $wpdb->get_col( + $wpdb->prepare( + "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine. + $query_args + ) + ) + ); + } else { + $values = array(); + } + + // Empty value indicates that all options for given attribute are available. + if ( in_array( null, $values, true ) || in_array( '', $values, true ) || empty( $values ) ) { + $values = $attribute['is_taxonomy'] ? wc_get_object_terms( $product->get_id(), $attribute['name'], 'slug' ) : wc_get_text_attributes( $attribute['value'] ); + // Get custom attributes (non taxonomy) as defined. + } elseif ( ! $attribute['is_taxonomy'] ) { + $text_attributes = wc_get_text_attributes( $attribute['value'] ); + $assigned_text_attributes = $values; + $values = array(); + + // Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. + if ( version_compare( get_post_meta( $product->get_id(), '_product_version', true ), '2.4.0', '<' ) ) { + $assigned_text_attributes = array_map( 'sanitize_title', $assigned_text_attributes ); + foreach ( $text_attributes as $text_attribute ) { + if ( in_array( sanitize_title( $text_attribute ), $assigned_text_attributes, true ) ) { + $values[] = $text_attribute; + } + } + } else { + foreach ( $text_attributes as $text_attribute ) { + if ( in_array( $text_attribute, $assigned_text_attributes, true ) ) { + $values[] = $text_attribute; + } + } + } + } + $variation_attributes[ $attribute['name'] ] = array_unique( $values ); + } + } + + wp_cache_set( $cache_key, $variation_attributes, $cache_group ); + + return $variation_attributes; + } + + /** + * Get an array of all sale and regular prices from all variations. This is used for example when displaying the price range at variable product level or seeing if the variable product is on sale. + * + * Can be filtered by plugins which modify costs, but otherwise will include the raw meta costs unlike get_price() which runs costs through the woocommerce_get_price filter. + * This is to ensure modified prices are not cached, unless intended. + * + * @param WC_Product $product Product object. + * @param bool $for_display If true, prices will be adapted for display based on the `woocommerce_tax_display_shop` setting (including or excluding taxes). + * + * @return array of prices + * @since 3.0.0 + */ + public function read_price_data( &$product, $for_display = false ) { + + /** + * Transient name for storing prices for this product (note: Max transient length is 45) + * + * @since 2.5.0 a single transient is used per product for all prices, rather than many transients per product. + */ + $transient_name = 'wc_var_prices_' . $product->get_id(); + $transient_version = WC_Cache_Helper::get_transient_version( 'product' ); + $price_hash = $this->get_price_hash( $product, $for_display ); + + // Check if prices array is stale. + if ( ! isset( $this->prices_array['version'] ) || $this->prices_array['version'] !== $transient_version ) { + $this->prices_array = array( + 'version' => $transient_version, + ); + } + + /** + * $this->prices_array is an array of values which may have been modified from what is stored in transients - this may not match $transient_cached_prices_array. + * If the value has already been generated, we don't need to grab the values again so just return them. They are already filtered. + */ + if ( empty( $this->prices_array[ $price_hash ] ) ) { + $transient_cached_prices_array = array_filter( (array) json_decode( strval( get_transient( $transient_name ) ), true ) ); + + // If the product version has changed since the transient was last saved, reset the transient cache. + if ( ! isset( $transient_cached_prices_array['version'] ) || $transient_version !== $transient_cached_prices_array['version'] ) { + $transient_cached_prices_array = array( + 'version' => $transient_version, + ); + } + + // If the prices are not stored for this hash, generate them and add to the transient. + if ( empty( $transient_cached_prices_array[ $price_hash ] ) ) { + $prices_array = array( + 'price' => array(), + 'regular_price' => array(), + 'sale_price' => array(), + ); + + $variation_ids = $product->get_visible_children(); + + if ( is_callable( '_prime_post_caches' ) ) { + _prime_post_caches( $variation_ids ); + } + + foreach ( $variation_ids as $variation_id ) { + $variation = wc_get_product( $variation_id ); + + if ( $variation ) { + $price = apply_filters( 'woocommerce_variation_prices_price', $variation->get_price( 'edit' ), $variation, $product ); + $regular_price = apply_filters( 'woocommerce_variation_prices_regular_price', $variation->get_regular_price( 'edit' ), $variation, $product ); + $sale_price = apply_filters( 'woocommerce_variation_prices_sale_price', $variation->get_sale_price( 'edit' ), $variation, $product ); + + // Skip empty prices. + if ( '' === $price ) { + continue; + } + + // If sale price does not equal price, the product is not yet on sale. + if ( $sale_price === $regular_price || $sale_price !== $price ) { + $sale_price = $regular_price; + } + + // If we are getting prices for display, we need to account for taxes. + if ( $for_display ) { + if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) { + $price = '' === $price ? '' : wc_get_price_including_tax( + $variation, + array( + 'qty' => 1, + 'price' => $price, + ) + ); + $regular_price = '' === $regular_price ? '' : wc_get_price_including_tax( + $variation, + array( + 'qty' => 1, + 'price' => $regular_price, + ) + ); + $sale_price = '' === $sale_price ? '' : wc_get_price_including_tax( + $variation, + array( + 'qty' => 1, + 'price' => $sale_price, + ) + ); + } else { + $price = '' === $price ? '' : wc_get_price_excluding_tax( + $variation, + array( + 'qty' => 1, + 'price' => $price, + ) + ); + $regular_price = '' === $regular_price ? '' : wc_get_price_excluding_tax( + $variation, + array( + 'qty' => 1, + 'price' => $regular_price, + ) + ); + $sale_price = '' === $sale_price ? '' : wc_get_price_excluding_tax( + $variation, + array( + 'qty' => 1, + 'price' => $sale_price, + ) + ); + } + } + + $prices_array['price'][ $variation_id ] = wc_format_decimal( $price, wc_get_price_decimals() ); + $prices_array['regular_price'][ $variation_id ] = wc_format_decimal( $regular_price, wc_get_price_decimals() ); + $prices_array['sale_price'][ $variation_id ] = wc_format_decimal( $sale_price, wc_get_price_decimals() ); + + $prices_array = apply_filters( 'woocommerce_variation_prices_array', $prices_array, $variation, $for_display ); + } + } + + // Add all pricing data to the transient array. + foreach ( $prices_array as $key => $values ) { + $transient_cached_prices_array[ $price_hash ][ $key ] = $values; + } + + set_transient( $transient_name, wp_json_encode( $transient_cached_prices_array ), DAY_IN_SECONDS * 30 ); + } + + /** + * Give plugins one last chance to filter the variation prices array which has been generated and store locally to the class. + * This value may differ from the transient cache. It is filtered once before storing locally. + */ + $this->prices_array[ $price_hash ] = apply_filters( 'woocommerce_variation_prices', $transient_cached_prices_array[ $price_hash ], $product, $for_display ); + } + return $this->prices_array[ $price_hash ]; + } + + /** + * Create unique cache key based on the tax location (affects displayed/cached prices), product version and active price filters. + * DEVELOPERS should filter this hash if offering conditional pricing to keep it unique. + * + * @param WC_Product $product Product object. + * @param bool $for_display If taxes should be calculated or not. + * + * @since 3.0.0 + * @return string + */ + protected function get_price_hash( &$product, $for_display = false ) { + global $wp_filter; + + $price_hash = array( false ); + + if ( $for_display && wc_tax_enabled() ) { + $price_hash = array( + get_option( 'woocommerce_tax_display_shop', 'excl' ), + WC_Tax::get_rates(), + empty( WC()->customer ) ? false : WC()->customer->is_vat_exempt(), + ); + } + + $filter_names = array( 'woocommerce_variation_prices_price', 'woocommerce_variation_prices_regular_price', 'woocommerce_variation_prices_sale_price' ); + + foreach ( $filter_names as $filter_name ) { + if ( ! empty( $wp_filter[ $filter_name ] ) ) { + $price_hash[ $filter_name ] = array(); + + foreach ( $wp_filter[ $filter_name ] as $priority => $callbacks ) { + $price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) ); + } + } + } + + return md5( wp_json_encode( apply_filters( 'woocommerce_get_variation_prices_hash', $price_hash, $product, $for_display ) ) ); + } + + /** + * Does a child have a weight set? + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + * @return boolean + */ + public function child_has_weight( $product ) { + global $wpdb; + $children = $product->get_visible_children(); + if ( ! $children ) { + return false; + } + + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + + return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_weight' AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine. + } + + /** + * Does a child have dimensions set? + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + * @return boolean + */ + public function child_has_dimensions( $product ) { + global $wpdb; + $children = $product->get_visible_children(); + if ( ! $children ) { + return false; + } + + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + + return null !== $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key IN ( '_length', '_width', '_height' ) AND meta_value > 0 AND post_id IN {$query_in}", $children ) ); // @codingStandardsIgnoreLine. + } + + /** + * Is a child in stock? + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + * @return boolean + */ + public function child_is_in_stock( $product ) { + return $this->child_has_stock_status( $product, 'instock' ); + } + + /** + * Does a child have a stock status? + * + * @param WC_Product $product Product object. + * @param string $status 'instock', 'outofstock', or 'onbackorder'. + * + * @since 3.3.0 + * @return boolean + */ + public function child_has_stock_status( $product, $status ) { + global $wpdb; + + $children = $product->get_children(); + + if ( $children ) { + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $query_args = array( 'stock_status' => $status ) + $children; + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + if ( get_option( 'woocommerce_product_lookup_table_is_generating' ) ) { + $query = "SELECT COUNT( post_id ) FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = %s AND post_id IN {$query_in}"; + } else { + $query = "SELECT COUNT( product_id ) FROM {$wpdb->wc_product_meta_lookup} WHERE stock_status = %s AND product_id IN {$query_in}"; + } + $children_with_status = $wpdb->get_var( + $wpdb->prepare( + $query, + $query_args + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + } else { + $children_with_status = 0; + } + + return (bool) $children_with_status; + } + + /** + * Syncs all variation names if the parent name is changed. + * + * @param WC_Product $product Product object. + * @param string $previous_name Variation previous name. + * @param string $new_name Variation new name. + * + * @since 3.0.0 + */ + public function sync_variation_names( &$product, $previous_name = '', $new_name = '' ) { + if ( $new_name !== $previous_name ) { + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->posts} + SET post_title = REPLACE( post_title, %s, %s ) + WHERE post_type = 'product_variation' + AND post_parent = %d", + $previous_name ? $previous_name : 'AUTO-DRAFT', + $new_name, + $product->get_id() + ) + ); + } + } + + /** + * Stock managed at the parent level - update children being managed by this product. + * This sync function syncs downwards (from parent to child) when the variable product is saved. + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + */ + public function sync_managed_variation_stock_status( &$product ) { + global $wpdb; + + if ( $product->get_manage_stock() ) { + $children = $product->get_children(); + $changed = false; + + if ( $children ) { + $status = $product->get_stock_status(); + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $managed_children = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_manage_stock' AND meta_value != 'yes' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine. + foreach ( $managed_children as $managed_child ) { + if ( update_post_meta( $managed_child, '_stock_status', $status ) ) { + $this->update_lookup_table( $managed_child, 'wc_product_meta_lookup' ); + $changed = true; + } + } + } + + if ( $changed ) { + $children = $this->read_children( $product, true ); + $product->set_children( $children['all'] ); + $product->set_visible_children( $children['visible'] ); + } + } + } + + /** + * Sync variable product prices with children. + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + */ + public function sync_price( &$product ) { + global $wpdb; + + $children = $product->get_visible_children(); + if ( $children ) { + $format = array_fill( 0, count( $children ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $prices = array_unique( $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_price' AND post_id IN {$query_in}", $children ) ) ); // @codingStandardsIgnoreLine. + } else { + $prices = array(); + } + + delete_post_meta( $product->get_id(), '_price' ); + delete_post_meta( $product->get_id(), '_sale_price' ); + delete_post_meta( $product->get_id(), '_regular_price' ); + + if ( $prices ) { + sort( $prices, SORT_NUMERIC ); + // To allow sorting and filtering by multiple values, we have no choice but to store child prices in this manner. + foreach ( $prices as $price ) { + if ( is_null( $price ) || '' === $price ) { + continue; + } + add_post_meta( $product->get_id(), '_price', $price, false ); + } + } + + $this->update_lookup_table( $product->get_id(), 'wc_product_meta_lookup' ); + + /** + * Fire an action for this direct update so it can be detected by other code. + * + * @since 3.6 + * @param int $product_id Product ID that was updated directly. + */ + do_action( 'woocommerce_updated_product_price', $product->get_id() ); + } + + /** + * Sync variable product stock status with children. + * Change does not persist unless saved by caller. + * + * @param WC_Product $product Product object. + * + * @since 3.0.0 + */ + public function sync_stock_status( &$product ) { + if ( $product->child_is_in_stock() ) { + $product->set_stock_status( 'instock' ); + } elseif ( $product->child_is_on_backorder() ) { + $product->set_stock_status( 'onbackorder' ); + } else { + $product->set_stock_status( 'outofstock' ); + } + } + + /** + * Delete variations of a product. + * + * @param int $product_id Product ID. + * @param bool $force_delete False to trash. + * + * @since 3.0.0 + */ + public function delete_variations( $product_id, $force_delete = false ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + $variation_ids = wp_parse_id_list( + get_posts( + array( + 'post_parent' => $product_id, + 'post_type' => 'product_variation', + 'fields' => 'ids', + 'post_status' => array( 'any', 'trash', 'auto-draft' ), + 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts + ) + ) + ); + + if ( ! empty( $variation_ids ) ) { + foreach ( $variation_ids as $variation_id ) { + if ( $force_delete ) { + do_action( 'woocommerce_before_delete_product_variation', $variation_id ); + wp_delete_post( $variation_id, true ); + do_action( 'woocommerce_delete_product_variation', $variation_id ); + } else { + wp_trash_post( $variation_id ); + do_action( 'woocommerce_trash_product_variation', $variation_id ); + } + } + } + + delete_transient( 'wc_product_children_' . $product_id ); + } + + /** + * Untrash variations. + * + * @param int $product_id Product ID. + */ + public function untrash_variations( $product_id ) { + $variation_ids = wp_parse_id_list( + get_posts( + array( + 'post_parent' => $product_id, + 'post_type' => 'product_variation', + 'fields' => 'ids', + 'post_status' => 'trash', + 'numberposts' => -1, // phpcs:ignore WordPress.VIP.PostsPerPage.posts_per_page_numberposts + ) + ) + ); + + if ( ! empty( $variation_ids ) ) { + foreach ( $variation_ids as $variation_id ) { + wp_untrash_post( $variation_id ); + } + } + + delete_transient( 'wc_product_children_' . $product_id ); + } +} diff --git a/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 0000000..cf481af --- /dev/null +++ b/includes/data-stores/class-wc-product-variation-data-store-cpt.php @@ -0,0 +1,547 @@ +meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'attribute_' ) && 0 !== stripos( $meta->meta_key, 'wp_' ); + } + + /* + |-------------------------------------------------------------------------- + | CRUD Methods + |-------------------------------------------------------------------------- + */ + + /** + * Reads a product from the database and sets its data to the class. + * + * @since 3.0.0 + * @param WC_Product_Variation $product Product object. + * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status (via read_product_data), or when passing an invalid ID. + */ + public function read( &$product ) { + $product->set_defaults(); + + if ( ! $product->get_id() ) { + return; + } + + $post_object = get_post( $product->get_id() ); + + if ( ! $post_object ) { + return; + } + + if ( 'product_variation' !== $post_object->post_type ) { + throw new WC_Data_Exception( 'variation_invalid_id', __( 'Invalid product type: passed ID does not correspond to a product variation.', 'woocommerce' ) ); + } + + $product->set_props( + array( + 'name' => $post_object->post_title, + 'slug' => $post_object->post_name, + 'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ), + 'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ), + 'status' => $post_object->post_status, + 'menu_order' => $post_object->menu_order, + 'reviews_allowed' => 'open' === $post_object->comment_status, + 'parent_id' => $post_object->post_parent, + 'attribute_summary' => $post_object->post_excerpt, + ) + ); + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $this->read_downloads( $product ); + $this->read_product_data( $product ); + $this->read_extra_data( $product ); + $product->set_attributes( wc_get_product_variation_attributes( $product->get_id() ) ); + + $updates = array(); + /** + * If a variation title is not in sync with the parent e.g. saved prior to 3.0, or if the parent title has changed, detect here and update. + */ + $new_title = $this->generate_product_title( $product ); + + if ( $post_object->post_title !== $new_title ) { + $product->set_name( $new_title ); + $updates = array_merge( $updates, array( 'post_title' => $new_title ) ); + } + + /** + * If the attribute summary is not in sync, update here. Used when searching for variations by attribute values. + * This is meant to also cover the case when global attribute name or value is updated, then the attribute summary is updated + * for respective products when they're read. + */ + $new_attribute_summary = $this->generate_attribute_summary( $product ); + + if ( $new_attribute_summary !== $post_object->post_excerpt ) { + $product->set_attribute_summary( $new_attribute_summary ); + $updates = array_merge( $updates, array( 'post_excerpt' => $new_attribute_summary ) ); + } + + if ( ! empty( $updates ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $updates, array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } + + // Set object_read true once all data is read. + $product->set_object_read( true ); + } + + /** + * Create a new product. + * + * @since 3.0.0 + * @param WC_Product_Variation $product Product object. + */ + public function create( &$product ) { + if ( ! $product->get_date_created() ) { + $product->set_date_created( time() ); + } + + $new_title = $this->generate_product_title( $product ); + + if ( $product->get_name( 'edit' ) !== $new_title ) { + $product->set_name( $new_title ); + } + + $attribute_summary = $this->generate_attribute_summary( $product ); + $product->set_attribute_summary( $attribute_summary ); + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $id = wp_insert_post( + apply_filters( + 'woocommerce_new_product_variation_data', + array( + 'post_type' => 'product_variation', + 'post_status' => $product->get_status() ? $product->get_status() : 'publish', + 'post_author' => get_current_user_id(), + 'post_title' => $product->get_name( 'edit' ), + 'post_excerpt' => $product->get_attribute_summary( 'edit' ), + 'post_content' => '', + 'post_parent' => $product->get_parent_id(), + 'comment_status' => 'closed', + 'ping_status' => 'closed', + 'menu_order' => $product->get_menu_order(), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_name' => $product->get_slug( 'edit' ), + ) + ), + true + ); + + if ( $id && ! is_wp_error( $id ) ) { + $product->set_id( $id ); + + $this->update_post_meta( $product, true ); + $this->update_terms( $product, true ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product, true ); + $this->handle_updated_props( $product ); + + $product->save_meta_data(); + $product->apply_changes(); + + $this->update_version_and_type( $product ); + $this->update_guid( $product ); + + $this->clear_caches( $product ); + + do_action( 'woocommerce_new_product_variation', $id, $product ); + } + } + + /** + * Updates an existing product. + * + * @since 3.0.0 + * @param WC_Product_Variation $product Product object. + */ + public function update( &$product ) { + $product->save_meta_data(); + + if ( ! $product->get_date_created() ) { + $product->set_date_created( time() ); + } + + $new_title = $this->generate_product_title( $product ); + + if ( $product->get_name( 'edit' ) !== $new_title ) { + $product->set_name( $new_title ); + } + + // The post parent is not a valid variable product so we should prevent this. + if ( $product->get_parent_id( 'edit' ) && 'product' !== get_post_type( $product->get_parent_id( 'edit' ) ) ) { + $product->set_parent_id( 0 ); + } + + $changes = $product->get_changes(); + + if ( array_intersect( array( 'attributes' ), array_keys( $changes ) ) ) { + $product->set_attribute_summary( $this->generate_attribute_summary( $product ) ); + } + + // Only update the post when the post data changes. + if ( array_intersect( array( 'name', 'parent_id', 'status', 'menu_order', 'date_created', 'date_modified', 'attributes' ), array_keys( $changes ) ) ) { + $post_data = array( + 'post_title' => $product->get_name( 'edit' ), + 'post_excerpt' => $product->get_attribute_summary( 'edit' ), + 'post_parent' => $product->get_parent_id( 'edit' ), + 'comment_status' => 'closed', + 'post_status' => $product->get_status( 'edit' ) ? $product->get_status( 'edit' ) : 'publish', + 'menu_order' => $product->get_menu_order( 'edit' ), + 'post_date' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getOffsetTimestamp() ), + 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $product->get_date_created( 'edit' )->getTimestamp() ), + 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), + 'post_modified_gmt' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $product->get_date_modified( 'edit' )->getTimestamp() ) : current_time( 'mysql', 1 ), + 'post_type' => 'product_variation', + 'post_name' => $product->get_slug( 'edit' ), + ); + + /** + * When updating this object, to prevent infinite loops, use $wpdb + * to update data, since wp_update_post spawns more calls to the + * save_post action. + * + * This ensures hooks are fired by either WP itself (admin screen save), + * or an update purely from CRUD. + */ + if ( doing_action( 'save_post' ) ) { + $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $product->get_id() ) ); + clean_post_cache( $product->get_id() ); + } else { + wp_update_post( array_merge( array( 'ID' => $product->get_id() ), $post_data ) ); + } + $product->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. + + } else { // Only update post modified time to record this save event. + $GLOBALS['wpdb']->update( + $GLOBALS['wpdb']->posts, + array( + 'post_modified' => current_time( 'mysql' ), + 'post_modified_gmt' => current_time( 'mysql', 1 ), + ), + array( + 'ID' => $product->get_id(), + ) + ); + clean_post_cache( $product->get_id() ); + } + + $this->update_post_meta( $product ); + $this->update_terms( $product ); + $this->update_visibility( $product, true ); + $this->update_attributes( $product ); + $this->handle_updated_props( $product ); + + $product->apply_changes(); + + $this->update_version_and_type( $product ); + + $this->clear_caches( $product ); + + do_action( 'woocommerce_update_product_variation', $product->get_id(), $product ); + } + + /* + |-------------------------------------------------------------------------- + | Additional Methods + |-------------------------------------------------------------------------- + */ + + /** + * Generates a title with attribute information for a variation. + * Products will get a title of the form "Name - Value, Value" or just "Name". + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @return string + */ + protected function generate_product_title( $product ) { + $attributes = (array) $product->get_attributes(); + + // Do not include attributes if the product has 3+ attributes. + $should_include_attributes = count( $attributes ) < 3; + + // Do not include attributes if an attribute name has 2+ words and the + // product has multiple attributes. + if ( $should_include_attributes && 1 < count( $attributes ) ) { + foreach ( $attributes as $name => $value ) { + if ( false !== strpos( $name, '-' ) ) { + $should_include_attributes = false; + break; + } + } + } + + $should_include_attributes = apply_filters( 'woocommerce_product_variation_title_include_attributes', $should_include_attributes, $product ); + $separator = apply_filters( 'woocommerce_product_variation_title_attributes_separator', ' - ', $product ); + $title_base = get_post_field( 'post_title', $product->get_parent_id() ); + $title_suffix = $should_include_attributes ? wc_get_formatted_variation( $product, true, false ) : ''; + + return apply_filters( 'woocommerce_product_variation_title', $title_suffix ? $title_base . $separator . $title_suffix : $title_base, $product, $title_base, $title_suffix ); + } + + /** + * Generates attribute summary for the variation. + * + * Attribute summary contains comma-delimited 'attribute_name: attribute_value' pairs for all attributes. + * + * @since 3.6.0 + * @param WC_Product_Variation $product Product variation to generate the attribute summary for. + * + * @return string + */ + protected function generate_attribute_summary( $product ) { + return wc_get_formatted_variation( $product, true, true ); + } + + /** + * Make sure we store the product version (to track data changes). + * + * @param WC_Product $product Product object. + * @since 3.0.0 + */ + protected function update_version_and_type( &$product ) { + wp_set_object_terms( $product->get_id(), '', 'product_type' ); + update_post_meta( $product->get_id(), '_product_version', Constants::get_constant( 'WC_VERSION' ) ); + } + + /** + * Read post data. + * + * @since 3.0.0 + * @param WC_Product_Variation $product Product object. + * @throws WC_Data_Exception If WC_Product::set_tax_status() is called with an invalid tax status. + */ + protected function read_product_data( &$product ) { + $id = $product->get_id(); + + $product->set_props( + array( + 'description' => get_post_meta( $id, '_variation_description', true ), + 'regular_price' => get_post_meta( $id, '_regular_price', true ), + 'sale_price' => get_post_meta( $id, '_sale_price', true ), + 'date_on_sale_from' => get_post_meta( $id, '_sale_price_dates_from', true ), + 'date_on_sale_to' => get_post_meta( $id, '_sale_price_dates_to', true ), + 'manage_stock' => get_post_meta( $id, '_manage_stock', true ), + 'stock_status' => get_post_meta( $id, '_stock_status', true ), + 'low_stock_amount' => get_post_meta( $id, '_low_stock_amount', true ), + 'shipping_class_id' => current( $this->get_term_ids( $id, 'product_shipping_class' ) ), + 'virtual' => get_post_meta( $id, '_virtual', true ), + 'downloadable' => get_post_meta( $id, '_downloadable', true ), + 'gallery_image_ids' => array_filter( explode( ',', get_post_meta( $id, '_product_image_gallery', true ) ) ), + 'download_limit' => get_post_meta( $id, '_download_limit', true ), + 'download_expiry' => get_post_meta( $id, '_download_expiry', true ), + 'image_id' => get_post_thumbnail_id( $id ), + 'backorders' => get_post_meta( $id, '_backorders', true ), + 'sku' => get_post_meta( $id, '_sku', true ), + 'stock_quantity' => get_post_meta( $id, '_stock', true ), + 'weight' => get_post_meta( $id, '_weight', true ), + 'length' => get_post_meta( $id, '_length', true ), + 'width' => get_post_meta( $id, '_width', true ), + 'height' => get_post_meta( $id, '_height', true ), + 'tax_class' => ! metadata_exists( 'post', $id, '_tax_class' ) ? 'parent' : get_post_meta( $id, '_tax_class', true ), + ) + ); + + if ( $product->is_on_sale( 'edit' ) ) { + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + + $parent_object = get_post( $product->get_parent_id() ); + $terms = get_the_terms( $product->get_parent_id(), 'product_visibility' ); + $term_names = is_array( $terms ) ? wp_list_pluck( $terms, 'name' ) : array(); + $exclude_search = in_array( 'exclude-from-search', $term_names, true ); + $exclude_catalog = in_array( 'exclude-from-catalog', $term_names, true ); + + if ( $exclude_search && $exclude_catalog ) { + $catalog_visibility = 'hidden'; + } elseif ( $exclude_search ) { + $catalog_visibility = 'catalog'; + } elseif ( $exclude_catalog ) { + $catalog_visibility = 'search'; + } else { + $catalog_visibility = 'visible'; + } + + $product->set_parent_data( + array( + 'title' => $parent_object ? $parent_object->post_title : '', + 'status' => $parent_object ? $parent_object->post_status : '', + 'sku' => get_post_meta( $product->get_parent_id(), '_sku', true ), + 'manage_stock' => get_post_meta( $product->get_parent_id(), '_manage_stock', true ), + 'backorders' => get_post_meta( $product->get_parent_id(), '_backorders', true ), + 'stock_quantity' => wc_stock_amount( get_post_meta( $product->get_parent_id(), '_stock', true ) ), + 'weight' => get_post_meta( $product->get_parent_id(), '_weight', true ), + 'length' => get_post_meta( $product->get_parent_id(), '_length', true ), + 'width' => get_post_meta( $product->get_parent_id(), '_width', true ), + 'height' => get_post_meta( $product->get_parent_id(), '_height', true ), + 'tax_class' => get_post_meta( $product->get_parent_id(), '_tax_class', true ), + 'shipping_class_id' => absint( current( $this->get_term_ids( $product->get_parent_id(), 'product_shipping_class' ) ) ), + 'image_id' => get_post_thumbnail_id( $product->get_parent_id() ), + 'purchase_note' => get_post_meta( $product->get_parent_id(), '_purchase_note', true ), + 'catalog_visibility' => $catalog_visibility, + ) + ); + + // Pull data from the parent when there is no user-facing way to set props. + $product->set_sold_individually( get_post_meta( $product->get_parent_id(), '_sold_individually', true ) ); + $product->set_tax_status( get_post_meta( $product->get_parent_id(), '_tax_status', true ) ); + $product->set_cross_sell_ids( get_post_meta( $product->get_parent_id(), '_crosssell_ids', true ) ); + } + + /** + * For all stored terms in all taxonomies, save them to the DB. + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + protected function update_terms( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'shipping_class_id', $changes ) ) { + wp_set_post_terms( $product->get_id(), array( $product->get_shipping_class_id( 'edit' ) ), 'product_shipping_class', false ); + } + } + + /** + * Update visibility terms based on props. + * + * @since 3.0.0 + * + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + protected function update_visibility( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_intersect( array( 'stock_status' ), array_keys( $changes ) ) ) { + $terms = array(); + + if ( 'outofstock' === $product->get_stock_status() ) { + $terms[] = 'outofstock'; + } + + wp_set_post_terms( $product->get_id(), $terms, 'product_visibility', false ); + } + } + + /** + * Update attribute meta values. + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + protected function update_attributes( &$product, $force = false ) { + $changes = $product->get_changes(); + + if ( $force || array_key_exists( 'attributes', $changes ) ) { + global $wpdb; + + $product_id = $product->get_id(); + $attributes = $product->get_attributes(); + $updated_attribute_keys = array(); + foreach ( $attributes as $key => $value ) { + update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) ); + $updated_attribute_keys[] = 'attribute_' . $key; + } + + // Remove old taxonomies attributes so data is kept up to date - first get attribute key names. + $delete_attribute_keys = $wpdb->get_col( + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration + "SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d", + $wpdb->esc_like( 'attribute_' ) . '%', + $product_id + ) + ); + + foreach ( $delete_attribute_keys as $key ) { + delete_post_meta( $product_id, $key ); + } + } + } + + /** + * Helper method that updates all the post meta for a product based on it's settings in the WC_Product class. + * + * @since 3.0.0 + * @param WC_Product $product Product object. + * @param bool $force Force update. Used during create. + */ + public function update_post_meta( &$product, $force = false ) { + $meta_key_to_props = array( + '_variation_description' => 'description', + ); + + $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $product, $meta_key_to_props ); + + foreach ( $props_to_update as $meta_key => $prop ) { + $value = $product->{"get_$prop"}( 'edit' ); + $updated = update_post_meta( $product->get_id(), $meta_key, $value ); + if ( $updated ) { + $this->updated_props[] = $prop; + } + } + + parent::update_post_meta( $product, $force ); + } + + /** + * Update product variation guid. + * + * @param WC_Product_Variation $product Product variation object. + * + * @since 3.6.0 + */ + protected function update_guid( $product ) { + global $wpdb; + + $guid = home_url( + add_query_arg( + array( + 'post_type' => 'product_variation', + 'p' => $product->get_id(), + ), + '' + ) + ); + $wpdb->update( $wpdb->posts, array( 'guid' => $guid ), array( 'ID' => $product->get_id() ) ); + } +} diff --git a/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 0000000..fe9c54d --- /dev/null +++ b/includes/data-stores/class-wc-shipping-zone-data-store.php @@ -0,0 +1,373 @@ +insert( + $wpdb->prefix . 'woocommerce_shipping_zones', + array( + 'zone_name' => $zone->get_zone_name(), + 'zone_order' => $zone->get_zone_order(), + ) + ); + $zone->set_id( $wpdb->insert_id ); + $zone->save_meta_data(); + $this->save_locations( $zone ); + $zone->apply_changes(); + WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + } + + /** + * Update zone in the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone Shipping zone object. + */ + public function update( &$zone ) { + global $wpdb; + if ( $zone->get_id() ) { + $wpdb->update( + $wpdb->prefix . 'woocommerce_shipping_zones', + array( + 'zone_name' => $zone->get_zone_name(), + 'zone_order' => $zone->get_zone_order(), + ), + array( 'zone_id' => $zone->get_id() ) + ); + } + $zone->save_meta_data(); + $this->save_locations( $zone ); + $zone->apply_changes(); + WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + } + + /** + * Method to read a shipping zone from the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone Shipping zone object. + * @throws Exception If invalid data store. + */ + public function read( &$zone ) { + global $wpdb; + + // Zone 0 is used as a default if no other zones fit. + if ( 0 === $zone->get_id() || '0' === $zone->get_id() ) { + $this->read_zone_locations( $zone ); + $zone->set_zone_name( __( 'Locations not covered by your other zones', 'woocommerce' ) ); + $zone->read_meta_data(); + $zone->set_object_read( true ); + + /** + * Indicate that the WooCommerce shipping zone has been loaded. + * + * @param WC_Shipping_Zone $zone The shipping zone that has been loaded. + */ + do_action( 'woocommerce_shipping_zone_loaded', $zone ); + return; + } + + $zone_data = $wpdb->get_row( + $wpdb->prepare( + "SELECT zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones WHERE zone_id = %d LIMIT 1", + $zone->get_id() + ) + ); + + if ( ! $zone_data ) { + throw new Exception( __( 'Invalid data store.', 'woocommerce' ) ); + } + + $zone->set_zone_name( $zone_data->zone_name ); + $zone->set_zone_order( $zone_data->zone_order ); + $this->read_zone_locations( $zone ); + $zone->read_meta_data(); + $zone->set_object_read( true ); + + /** This action is documented in includes/datastores/class-wc-shipping-zone-data-store.php. */ + do_action( 'woocommerce_shipping_zone_loaded', $zone ); + } + + /** + * Deletes a shipping zone from the database. + * + * @since 3.0.0 + * @param WC_Shipping_Zone $zone Shipping zone object. + * @param array $args Array of args to pass to the delete method. + * @return void + */ + public function delete( &$zone, $args = array() ) { + $zone_id = $zone->get_id(); + + if ( $zone_id ) { + global $wpdb; + + // Delete methods and their settings. + $methods = $this->get_methods( $zone_id, false ); + + if ( $methods ) { + foreach ( $methods as $method ) { + $this->delete_method( $method->instance_id ); + } + } + + // Delete zone. + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone_id ) ); + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zones', array( 'zone_id' => $zone_id ) ); + + $zone->set_id( null ); + + WC_Cache_Helper::invalidate_cache_group( 'shipping_zones' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + + do_action( 'woocommerce_delete_shipping_zone', $zone_id ); + } + } + + /** + * Get a list of shipping methods for a specific zone. + * + * @since 3.0.0 + * @param int $zone_id Zone ID. + * @param bool $enabled_only True to request enabled methods only. + * @return array Array of objects containing method_id, method_order, instance_id, is_enabled + */ + public function get_methods( $zone_id, $enabled_only ) { + global $wpdb; + + if ( $enabled_only ) { + $raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d AND is_enabled = 1"; + } else { + $raw_methods_sql = "SELECT method_id, method_order, instance_id, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d"; + } + + return $wpdb->get_results( $wpdb->prepare( $raw_methods_sql, $zone_id ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Get count of methods for a zone. + * + * @since 3.0.0 + * @param int $zone_id Zone ID. + * @return int Method Count + */ + public function get_method_count( $zone_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE zone_id = %d", $zone_id ) ); + } + + /** + * Add a shipping method to a zone. + * + * @since 3.0.0 + * @param int $zone_id Zone ID. + * @param string $type Method Type/ID. + * @param int $order Method Order. + * @return int Instance ID + */ + public function add_method( $zone_id, $type, $order ) { + global $wpdb; + $wpdb->insert( + $wpdb->prefix . 'woocommerce_shipping_zone_methods', + array( + 'method_id' => $type, + 'zone_id' => $zone_id, + 'method_order' => $order, + ), + array( + '%s', + '%d', + '%d', + ) + ); + return $wpdb->insert_id; + } + + /** + * Delete a method instance. + * + * @since 3.0.0 + * @param int $instance_id Instance ID. + */ + public function delete_method( $instance_id ) { + global $wpdb; + + $method = $this->get_method( $instance_id ); + + if ( ! $method ) { + return; + } + + delete_option( 'woocommerce_' . $method->method_id . '_' . $instance_id . '_settings' ); + + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_methods', array( 'instance_id' => $instance_id ) ); + + do_action( 'woocommerce_delete_shipping_zone_method', $instance_id ); + } + + /** + * Get a shipping zone method instance. + * + * @since 3.0.0 + * @param int $instance_id Instance ID. + * @return object + */ + public function get_method( $instance_id ) { + global $wpdb; + return $wpdb->get_row( $wpdb->prepare( "SELECT zone_id, method_id, instance_id, method_order, is_enabled FROM {$wpdb->prefix}woocommerce_shipping_zone_methods WHERE instance_id = %d LIMIT 1;", $instance_id ) ); + } + + /** + * Find a matching zone ID for a given package. + * + * @since 3.0.0 + * @param object $package Package information. + * @return int + */ + public function get_zone_id_from_package( $package ) { + global $wpdb; + + $country = strtoupper( wc_clean( $package['destination']['country'] ) ); + $state = strtoupper( wc_clean( $package['destination']['state'] ) ); + $continent = strtoupper( wc_clean( WC()->countries->get_continent_code_for_country( $country ) ) ); + $postcode = wc_normalize_postcode( wc_clean( $package['destination']['postcode'] ) ); + + // Work out criteria for our zone search. + $criteria = array(); + $criteria[] = $wpdb->prepare( "( ( location_type = 'country' AND location_code = %s )", $country ); + $criteria[] = $wpdb->prepare( "OR ( location_type = 'state' AND location_code = %s )", $country . ':' . $state ); + $criteria[] = $wpdb->prepare( "OR ( location_type = 'continent' AND location_code = %s )", $continent ); + $criteria[] = 'OR ( location_type IS NULL ) )'; + + // Postcode range and wildcard matching. + $postcode_locations = $wpdb->get_results( "SELECT zone_id, location_code FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE location_type = 'postcode';" ); + + if ( $postcode_locations ) { + $zone_ids_with_postcode_rules = array_map( 'absint', wp_list_pluck( $postcode_locations, 'zone_id' ) ); + $matches = wc_postcode_location_matcher( $postcode, $postcode_locations, 'zone_id', 'location_code', $country ); + $do_not_match = array_unique( array_diff( $zone_ids_with_postcode_rules, array_keys( $matches ) ) ); + + if ( ! empty( $do_not_match ) ) { + $criteria[] = 'AND zones.zone_id NOT IN (' . implode( ',', $do_not_match ) . ')'; + } + } + + /** + * Get shipping zone criteria + * + * @since 3.6.6 + * @param array $criteria Get zone criteria. + * @param array $package Package information. + * @param array $postcode_locations Postcode range and wildcard matching. + */ + $criteria = apply_filters( 'woocommerce_get_zone_criteria', $criteria, $package, $postcode_locations ); + + // Get matching zones. + return $wpdb->get_var( + "SELECT zones.zone_id FROM {$wpdb->prefix}woocommerce_shipping_zones as zones + LEFT OUTER JOIN {$wpdb->prefix}woocommerce_shipping_zone_locations as locations ON zones.zone_id = locations.zone_id AND location_type != 'postcode' + WHERE " . implode( ' ', $criteria ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + . ' ORDER BY zone_order ASC, zones.zone_id ASC LIMIT 1' + ); + } + + /** + * Return an ordered list of zones. + * + * @since 3.0.0 + * @return array An array of objects containing a zone_id, zone_name, and zone_order. + */ + public function get_zones() { + global $wpdb; + return $wpdb->get_results( "SELECT zone_id, zone_name, zone_order FROM {$wpdb->prefix}woocommerce_shipping_zones order by zone_order ASC, zone_id ASC;" ); + } + + + /** + * Return a zone ID from an instance ID. + * + * @since 3.0.0 + * @param int $id Instnace ID. + * @return int + */ + public function get_zone_id_by_instance_id( $id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT zone_id FROM {$wpdb->prefix}woocommerce_shipping_zone_methods as methods WHERE methods.instance_id = %d LIMIT 1;", $id ) ); + } + + /** + * Read location data from the database. + * + * @param WC_Shipping_Zone $zone Shipping zone object. + */ + private function read_zone_locations( &$zone ) { + global $wpdb; + + $locations = $wpdb->get_results( + $wpdb->prepare( + "SELECT location_code, location_type FROM {$wpdb->prefix}woocommerce_shipping_zone_locations WHERE zone_id = %d", + $zone->get_id() + ) + ); + + if ( $locations ) { + foreach ( $locations as $location ) { + $zone->add_location( $location->location_code, $location->location_type ); + } + } + } + + /** + * Save locations to the DB. + * This function clears old locations, then re-inserts new if any changes are found. + * + * @since 3.0.0 + * + * @param WC_Shipping_Zone $zone Shipping zone object. + * + * @return bool|void + */ + private function save_locations( &$zone ) { + $changed_props = array_keys( $zone->get_changes() ); + if ( ! in_array( 'zone_locations', $changed_props, true ) ) { + return false; + } + + global $wpdb; + $wpdb->delete( $wpdb->prefix . 'woocommerce_shipping_zone_locations', array( 'zone_id' => $zone->get_id() ) ); + + foreach ( $zone->get_zone_locations( 'edit' ) as $location ) { + $wpdb->insert( + $wpdb->prefix . 'woocommerce_shipping_zone_locations', + array( + 'zone_id' => $zone->get_id(), + 'location_code' => $location->code, + 'location_type' => $location->type, + ) + ); + } + } +} diff --git a/includes/data-stores/class-wc-webhook-data-store.php b/includes/data-stores/class-wc-webhook-data-store.php new file mode 100644 index 0000000..e8b417e --- /dev/null +++ b/includes/data-stores/class-wc-webhook-data-store.php @@ -0,0 +1,448 @@ +get_changes(); + if ( isset( $changes['date_created'] ) ) { + $date_created = $webhook->get_date_created()->date( 'Y-m-d H:i:s' ); + $date_created_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_created()->getTimestamp() ); + } else { + $date_created = current_time( 'mysql' ); + $date_created_gmt = current_time( 'mysql', 1 ); + $webhook->set_date_created( $date_created ); + } + + // Pending delivery by default if not set while creating a new webhook. + if ( ! isset( $changes['pending_delivery'] ) ) { + $webhook->set_pending_delivery( true ); + } + + $data = array( + 'status' => $webhook->get_status( 'edit' ), + 'name' => $webhook->get_name( 'edit' ), + 'user_id' => $webhook->get_user_id( 'edit' ), + 'delivery_url' => $webhook->get_delivery_url( 'edit' ), + 'secret' => $webhook->get_secret( 'edit' ), + 'topic' => $webhook->get_topic( 'edit' ), + 'date_created' => $date_created, + 'date_created_gmt' => $date_created_gmt, + 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ), + 'failure_count' => $webhook->get_failure_count( 'edit' ), + 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ), + ); + + $wpdb->insert( $wpdb->prefix . 'wc_webhooks', $data ); // WPCS: DB call ok. + + $webhook_id = $wpdb->insert_id; + $webhook->set_id( $webhook_id ); + $webhook->apply_changes(); + + $this->delete_transients( $webhook->get_status( 'edit' ) ); + WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); + do_action( 'woocommerce_new_webhook', $webhook_id, $webhook ); + } + + /** + * Read a webhook from the database. + * + * @since 3.3.0 + * @param WC_Webhook $webhook Webhook instance. + * @throws Exception When webhook is invalid. + */ + public function read( &$webhook ) { + global $wpdb; + + $data = wp_cache_get( $webhook->get_id(), 'webhooks' ); + + if ( false === $data ) { + $data = $wpdb->get_row( $wpdb->prepare( "SELECT webhook_id, status, name, user_id, delivery_url, secret, topic, date_created, date_modified, api_version, failure_count, pending_delivery FROM {$wpdb->prefix}wc_webhooks WHERE webhook_id = %d LIMIT 1;", $webhook->get_id() ), ARRAY_A ); // WPCS: cache ok, DB call ok. + + wp_cache_add( $webhook->get_id(), $data, 'webhooks' ); + } + + if ( is_array( $data ) ) { + $webhook->set_props( + array( + 'id' => $data['webhook_id'], + 'status' => $data['status'], + 'name' => $data['name'], + 'user_id' => $data['user_id'], + 'delivery_url' => $data['delivery_url'], + 'secret' => $data['secret'], + 'topic' => $data['topic'], + 'date_created' => '0000-00-00 00:00:00' === $data['date_created'] ? null : $data['date_created'], + 'date_modified' => '0000-00-00 00:00:00' === $data['date_modified'] ? null : $data['date_modified'], + 'api_version' => $data['api_version'], + 'failure_count' => $data['failure_count'], + 'pending_delivery' => $data['pending_delivery'], + ) + ); + $webhook->set_object_read( true ); + + do_action( 'woocommerce_webhook_loaded', $webhook ); + } else { + throw new Exception( __( 'Invalid webhook.', 'woocommerce' ) ); + } + } + + /** + * Update a webhook. + * + * @since 3.3.0 + * @param WC_Webhook $webhook Webhook instance. + */ + public function update( &$webhook ) { + global $wpdb; + + $changes = $webhook->get_changes(); + $trigger = isset( $changes['delivery_url'] ); + + if ( isset( $changes['date_modified'] ) ) { + $date_modified = $webhook->get_date_modified()->date( 'Y-m-d H:i:s' ); + $date_modified_gmt = gmdate( 'Y-m-d H:i:s', $webhook->get_date_modified()->getTimestamp() ); + } else { + $date_modified = current_time( 'mysql' ); + $date_modified_gmt = current_time( 'mysql', 1 ); + $webhook->set_date_modified( $date_modified ); + } + + $data = array( + 'status' => $webhook->get_status( 'edit' ), + 'name' => $webhook->get_name( 'edit' ), + 'user_id' => $webhook->get_user_id( 'edit' ), + 'delivery_url' => $webhook->get_delivery_url( 'edit' ), + 'secret' => $webhook->get_secret( 'edit' ), + 'topic' => $webhook->get_topic( 'edit' ), + 'date_modified' => $date_modified, + 'date_modified_gmt' => $date_modified_gmt, + 'api_version' => $this->get_api_version_number( $webhook->get_api_version( 'edit' ) ), + 'failure_count' => $webhook->get_failure_count( 'edit' ), + 'pending_delivery' => $webhook->get_pending_delivery( 'edit' ), + ); + + $wpdb->update( + $wpdb->prefix . 'wc_webhooks', + $data, + array( + 'webhook_id' => $webhook->get_id(), + ) + ); // WPCS: DB call ok. + + $webhook->apply_changes(); + + if ( isset( $changes['status'] ) ) { + // We need to delete all transients, because we can't be sure of the old status. + $this->delete_transients( 'all' ); + } + wp_cache_delete( $webhook->get_id(), 'webhooks' ); + WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); + + if ( 'active' === $webhook->get_status() && ( $trigger || $webhook->get_pending_delivery() ) ) { + $webhook->deliver_ping(); + } + + do_action( 'woocommerce_webhook_updated', $webhook->get_id() ); + } + + /** + * Remove a webhook from the database. + * + * @since 3.3.0 + * @param WC_Webhook $webhook Webhook instance. + */ + public function delete( &$webhook ) { + global $wpdb; + + $wpdb->delete( + $wpdb->prefix . 'wc_webhooks', + array( + 'webhook_id' => $webhook->get_id(), + ), + array( '%d' ) + ); // WPCS: cache ok, DB call ok. + + $this->delete_transients( 'all' ); + wp_cache_delete( $webhook->get_id(), 'webhooks' ); + WC_Cache_Helper::invalidate_cache_group( 'webhooks' ); + do_action( 'woocommerce_webhook_deleted', $webhook->get_id(), $webhook ); + } + + /** + * Get API version number. + * + * @since 3.3.0 + * @param string $api_version REST API version. + * @return int + */ + public function get_api_version_number( $api_version ) { + return 'legacy_v3' === $api_version ? -1 : intval( substr( $api_version, -1 ) ); + } + + /** + * Get webhooks IDs from the database. + * + * @since 3.3.0 + * @throws InvalidArgumentException If a $status value is passed in that is not in the known wc_get_webhook_statuses() keys. + * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.6.0. + * @return int[] + */ + public function get_webhooks_ids( $status = '' ) { + if ( ! empty( $status ) ) { + $this->validate_status( $status ); + } + + $ids = get_transient( $this->get_transient_key( $status ) ); + + if ( false === $ids ) { + $ids = $this->search_webhooks( + array( + 'limit' => -1, + 'status' => $status, + ) + ); + $ids = array_map( 'absint', $ids ); + set_transient( $this->get_transient_key( $status ), $ids ); + } + + return $ids; + } + + /** + * Search webhooks. + * + * @param array $args Search arguments. + * @return array|object + */ + public function search_webhooks( $args ) { + global $wpdb; + + $args = wp_parse_args( + $args, + array( + 'limit' => 10, + 'offset' => 0, + 'order' => 'DESC', + 'orderby' => 'id', + 'paginate' => false, + ) + ); + + // Map post statuses. + $statuses = array( + 'publish' => 'active', + 'draft' => 'paused', + 'pending' => 'disabled', + ); + + // Map orderby to support a few post keys. + $orderby_mapping = array( + 'ID' => 'webhook_id', + 'id' => 'webhook_id', + 'name' => 'name', + 'title' => 'name', + 'post_title' => 'name', + 'post_name' => 'name', + 'date_created' => 'date_created_gmt', + 'date' => 'date_created_gmt', + 'post_date' => 'date_created_gmt', + 'date_modified' => 'date_modified_gmt', + 'modified' => 'date_modified_gmt', + 'post_modified' => 'date_modified_gmt', + ); + $orderby = isset( $orderby_mapping[ $args['orderby'] ] ) ? $orderby_mapping[ $args['orderby'] ] : 'webhook_id'; + $sort = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; + $order = "ORDER BY {$orderby} {$sort}"; + $limit = -1 < $args['limit'] ? $wpdb->prepare( 'LIMIT %d', $args['limit'] ) : ''; + $offset = 0 < $args['offset'] ? $wpdb->prepare( 'OFFSET %d', $args['offset'] ) : ''; + $status = ! empty( $args['status'] ) ? $wpdb->prepare( 'AND `status` = %s', isset( $statuses[ $args['status'] ] ) ? $statuses[ $args['status'] ] : $args['status'] ) : ''; + $search = ! empty( $args['search'] ) ? $wpdb->prepare( 'AND `name` LIKE %s', '%' . $wpdb->esc_like( sanitize_text_field( $args['search'] ) ) . '%' ) : ''; + $include = ''; + $exclude = ''; + $date_created = ''; + $date_modified = ''; + + if ( ! empty( $args['include'] ) ) { + $args['include'] = implode( ',', wp_parse_id_list( $args['include'] ) ); + $include = 'AND webhook_id IN (' . $args['include'] . ')'; + } + + if ( ! empty( $args['exclude'] ) ) { + $args['exclude'] = implode( ',', wp_parse_id_list( $args['exclude'] ) ); + $exclude = 'AND webhook_id NOT IN (' . $args['exclude'] . ')'; + } + + if ( ! empty( $args['after'] ) || ! empty( $args['before'] ) ) { + $args['after'] = empty( $args['after'] ) ? '0000-00-00' : $args['after']; + $args['before'] = empty( $args['before'] ) ? current_time( 'mysql', 1 ) : $args['before']; + + $date_created = "AND `date_created_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['before'] ) . "', '%Y-%m-%d %H:%i:%s')"; + } + + if ( ! empty( $args['modified_after'] ) || ! empty( $args['modified_before'] ) ) { + $args['modified_after'] = empty( $args['modified_after'] ) ? '0000-00-00' : $args['modified_after']; + $args['modified_before'] = empty( $args['modified_before'] ) ? current_time( 'mysql', 1 ) : $args['modified_before']; + + $date_modified = "AND `date_modified_gmt` BETWEEN STR_TO_DATE('" . esc_sql( $args['modified_after'] ) . "', '%Y-%m-%d %H:%i:%s') and STR_TO_DATE('" . esc_sql( $args['modified_before'] ) . "', '%Y-%m-%d %H:%i:%s')"; + } + + // Check for cache. + $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . 'search_webhooks' . md5( implode( ',', $args ) ); + $cache_value = wp_cache_get( $cache_key, 'webhook_search_results' ); + + if ( $cache_value ) { + return $cache_value; + } + + if ( $args['paginate'] ) { + $query = trim( + "SELECT SQL_CALC_FOUND_ROWS webhook_id + FROM {$wpdb->prefix}wc_webhooks + WHERE 1=1 + {$status} + {$search} + {$include} + {$exclude} + {$date_created} + {$date_modified} + {$order} + {$limit} + {$offset}" + ); + + $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $total = (int) $wpdb->get_var( 'SELECT FOUND_ROWS();' ); + $return_value = (object) array( + 'webhooks' => $webhook_ids, + 'total' => $total, + 'max_num_pages' => $args['limit'] > 1 ? ceil( $total / $args['limit'] ) : 1, + ); + } else { + $query = trim( + "SELECT webhook_id + FROM {$wpdb->prefix}wc_webhooks + WHERE 1=1 + {$status} + {$search} + {$include} + {$exclude} + {$date_created} + {$date_modified} + {$order} + {$limit} + {$offset}" + ); + + $webhook_ids = wp_parse_id_list( $wpdb->get_col( $query ) ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $return_value = $webhook_ids; + } + + wp_cache_set( $cache_key, $return_value, 'webhook_search_results' ); + + return $return_value; + } + + /** + * Count webhooks. + * + * @since 3.6.0 + * @param string $status Status to count. + * @return int + */ + protected function get_webhook_count( $status = 'active' ) { + global $wpdb; + $cache_key = WC_Cache_Helper::get_cache_prefix( 'webhooks' ) . $status . '_count'; + $count = wp_cache_get( $cache_key, 'webhooks' ); + + if ( false === $count ) { + $count = absint( $wpdb->get_var( $wpdb->prepare( "SELECT count( webhook_id ) FROM {$wpdb->prefix}wc_webhooks WHERE `status` = %s;", $status ) ) ); + + wp_cache_add( $cache_key, $count, 'webhooks' ); + } + + return $count; + } + + /** + * Get total webhook counts by status. + * + * @return array + */ + public function get_count_webhooks_by_status() { + $statuses = array_keys( wc_get_webhook_statuses() ); + $counts = array(); + + foreach ( $statuses as $status ) { + $counts[ $status ] = $this->get_webhook_count( $status ); + } + + return $counts; + } + + /** + * Check if a given string is in known statuses, based on return value of @see wc_get_webhook_statuses(). + * + * @since 3.6.0 + * @throws InvalidArgumentException If $status is not empty and not in the known wc_get_webhook_statuses() keys. + * @param string $status Status to check. + */ + private function validate_status( $status ) { + if ( ! array_key_exists( $status, wc_get_webhook_statuses() ) ) { + throw new InvalidArgumentException( sprintf( 'Invalid status given: %s. Status must be one of: %s.', $status, implode( ', ', array_keys( wc_get_webhook_statuses() ) ) ) ); + } + } + + /** + * Get the transient key used to cache a set of webhook IDs, optionally filtered by status. + * + * @since 3.6.0 + * @param string $status Optional - status of cache key. + * @return string + */ + private function get_transient_key( $status = '' ) { + return empty( $status ) ? 'woocommerce_webhook_ids' : sprintf( 'woocommerce_webhook_ids_status_%s', $status ); + } + + /** + * Delete the transients used to cache a set of webhook IDs, optionally filtered by status. + * + * @since 3.6.0 + * @param string $status Optional - status of cache to delete, or 'all' to delete all caches. + */ + private function delete_transients( $status = '' ) { + + // Always delete the non-filtered cache. + delete_transient( $this->get_transient_key( '' ) ); + + if ( ! empty( $status ) ) { + if ( 'all' === $status ) { + foreach ( wc_get_webhook_statuses() as $status_key => $status_string ) { + delete_transient( $this->get_transient_key( $status_key ) ); + } + } else { + delete_transient( $this->get_transient_key( $status ) ); + } + } + } +} diff --git a/includes/emails/class-wc-email-cancelled-order.php b/includes/emails/class-wc-email-cancelled-order.php new file mode 100644 index 0000000..eeb4d34 --- /dev/null +++ b/includes/emails/class-wc-email-cancelled-order.php @@ -0,0 +1,209 @@ +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'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + '{order_billing_full_name}' => '', + ); + + // 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}]: Order #{order_number} has been cancelled', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Order Cancelled: #{order_number}', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order|false $order Order object. + */ + public function trigger( $order_id, $order = false ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + $this->placeholders['{order_billing_full_name}'] = $this->object->get_formatted_billing_full_name(); + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Thanks for reading.', 'woocommerce' ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + /* translators: %s: list of placeholders */ + $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . esc_html( implode( ', ', array_keys( $this->placeholders ) ) ) . '' ); + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + '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, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'additional_content' => array( + 'title' => __( 'Additional content', 'woocommerce' ), + 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + 'default' => $this->get_default_additional_content(), + 'desc_tip' => true, + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + } + +endif; + +return new WC_Email_Cancelled_Order(); diff --git a/includes/emails/class-wc-email-customer-completed-order.php b/includes/emails/class-wc-email-customer-completed-order.php new file mode 100644 index 0000000..1795867 --- /dev/null +++ b/includes/emails/class-wc-email-customer-completed-order.php @@ -0,0 +1,146 @@ +id = 'customer_completed_order'; + $this->customer_email = true; + $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->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // Triggers for this email. + add_action( 'woocommerce_order_status_completed_notification', array( $this, 'trigger' ), 10, 2 ); + + // Call parent constructor. + parent::__construct(); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order|false $order Order object. + */ + public function trigger( $order_id, $order = false ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} order is now complete', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Thanks for shopping with us', 'woocommerce' ); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Thanks for shopping with us.', 'woocommerce' ); + } + } + +endif; + +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 new file mode 100644 index 0000000..0a2592c --- /dev/null +++ b/includes/emails/class-wc-email-customer-invoice.php @@ -0,0 +1,246 @@ +id = 'customer_invoice'; + $this->customer_email = true; + $this->title = __( 'Customer invoice / Order details', '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->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // Call parent constructor. + parent::__construct(); + + $this->manual = true; + } + + /** + * Get email subject. + * + * @param bool $paid Whether the order has been paid or not. + * @since 3.1.0 + * @return string + */ + public function get_default_subject( $paid = false ) { + if ( $paid ) { + return __( 'Invoice for order #{order_number} on {site_title}', 'woocommerce' ); + } else { + return __( 'Your latest {site_title} invoice', 'woocommerce' ); + } + } + + /** + * Get email heading. + * + * @param bool $paid Whether the order has been paid or not. + * @since 3.1.0 + * @return string + */ + public function get_default_heading( $paid = false ) { + if ( $paid ) { + return __( 'Invoice for order #{order_number}', 'woocommerce' ); + } else { + return __( 'Your invoice for order #{order_number}', 'woocommerce' ); + } + } + + /** + * Get email subject. + * + * @return string + */ + public function get_subject() { + if ( $this->object->has_status( array( 'completed', 'processing' ) ) ) { + $subject = $this->get_option( 'subject_paid', $this->get_default_subject( true ) ); + + return apply_filters( 'woocommerce_email_subject_customer_invoice_paid', $this->format_string( $subject ), $this->object, $this ); + } + + $subject = $this->get_option( 'subject', $this->get_default_subject() ); + return apply_filters( 'woocommerce_email_subject_customer_invoice', $this->format_string( $subject ), $this->object, $this ); + } + + /** + * Get email heading. + * + * @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 ) ); + return apply_filters( 'woocommerce_email_heading_customer_invoice_paid', $this->format_string( $heading ), $this->object, $this ); + } + + $heading = $this->get_option( 'heading', $this->get_default_heading() ); + return apply_filters( 'woocommerce_email_heading_customer_invoice', $this->format_string( $heading ), $this->object, $this ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Thanks for using {site_url}!', '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 ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + } + + if ( $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + /* translators: %s: list of placeholders */ + $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . esc_html( implode( ', ', array_keys( $this->placeholders ) ) ) . '' ); + $this->form_fields = array( + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'subject_paid' => array( + 'title' => __( 'Subject (paid)', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject( true ), + 'default' => '', + ), + 'heading_paid' => array( + 'title' => __( 'Email heading (paid)', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading( true ), + 'default' => '', + ), + 'additional_content' => array( + 'title' => __( 'Additional content', 'woocommerce' ), + 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + 'default' => $this->get_default_additional_content(), + 'desc_tip' => true, + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + } + +endif; + +return new WC_Email_Customer_Invoice(); diff --git a/includes/emails/class-wc-email-customer-new-account.php b/includes/emails/class-wc-email-customer-new-account.php new file mode 100644 index 0000000..3cd0a82 --- /dev/null +++ b/includes/emails/class-wc-email-customer-new-account.php @@ -0,0 +1,173 @@ +id = 'customer_new_account'; + $this->customer_email = true; + $this->title = __( 'New account', 'woocommerce' ); + $this->description = __( 'Customer "new account" emails are sent to the customer when a customer signs up via checkout or account pages.', 'woocommerce' ); + $this->template_html = 'emails/customer-new-account.php'; + $this->template_plain = 'emails/plain/customer-new-account.php'; + + // Call parent constructor. + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} account has been created!', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Welcome to {site_title}', 'woocommerce' ); + } + + /** + * Trigger. + * + * @param int $user_id User ID. + * @param string $user_pass User password. + * @param bool $password_generated Whether the password was generated automatically or not. + */ + public function trigger( $user_id, $user_pass = '', $password_generated = false ) { + $this->setup_locale(); + + if ( $user_id ) { + $this->object = new WP_User( $user_id ); + + $this->user_pass = $user_pass; + $this->user_login = stripslashes( $this->object->user_login ); + $this->user_email = stripslashes( $this->object->user_email ); + $this->recipient = $this->user_email; + $this->password_generated = $password_generated; + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'user_login' => $this->user_login, + 'user_pass' => $this->user_pass, + 'blogname' => $this->get_blogname(), + 'password_generated' => $this->password_generated, + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'user_login' => $this->user_login, + 'user_pass' => $this->user_pass, + 'blogname' => $this->get_blogname(), + 'password_generated' => $this->password_generated, + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'We look forward to seeing you soon.', 'woocommerce' ); + } + } + +endif; + +return new WC_Email_Customer_New_Account(); diff --git a/includes/emails/class-wc-email-customer-note.php b/includes/emails/class-wc-email-customer-note.php new file mode 100644 index 0000000..cf1b265 --- /dev/null +++ b/includes/emails/class-wc-email-customer-note.php @@ -0,0 +1,166 @@ +id = 'customer_note'; + $this->customer_email = true; + $this->title = __( 'Customer note', 'woocommerce' ); + $this->description = __( 'Customer note emails are sent when you add a note to an order.', 'woocommerce' ); + $this->template_html = 'emails/customer-note.php'; + $this->template_plain = 'emails/plain/customer-note.php'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // Triggers. + add_action( 'woocommerce_new_customer_note_notification', array( $this, 'trigger' ) ); + + // Call parent constructor. + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Note added to your {site_title} order from {order_date}', 'woocommerce' ); + } + + /** + * 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 Email arguments. + */ + public function trigger( $args ) { + $this->setup_locale(); + + if ( ! empty( $args ) ) { + $defaults = array( + 'order_id' => '', + 'customer_note' => '', + ); + + $args = wp_parse_args( $args, $defaults ); + + $order_id = $args['order_id']; + $customer_note = $args['customer_note']; + + if ( $order_id ) { + $this->object = wc_get_order( $order_id ); + + if ( $this->object ) { + $this->recipient = $this->object->get_billing_email(); + $this->customer_note = $customer_note; + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + } + } + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'customer_note' => $this->customer_note, + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'customer_note' => $this->customer_note, + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Thanks for reading.', 'woocommerce' ); + } + } + +endif; + +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 0000000..080abb9 --- /dev/null +++ b/includes/emails/class-wc-email-customer-on-hold-order.php @@ -0,0 +1,148 @@ +id = 'customer_on_hold_order'; + $this->customer_email = true; + $this->title = __( 'Order on-hold', 'woocommerce' ); + $this->description = __( 'This is an order notification sent to customers containing order details after an order is placed on-hold.', 'woocommerce' ); + $this->template_html = 'emails/customer-on-hold-order.php'; + $this->template_plain = 'emails/plain/customer-on-hold-order.php'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // Triggers for this email. + add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + + // Call parent constructor. + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} order has been received!', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Thank you for your order', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order|false $order Order object. + */ + public function trigger( $order_id, $order = false ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'We look forward to fulfilling your order soon.', 'woocommerce' ); + } + } + +endif; + +return new WC_Email_Customer_On_Hold_Order(); diff --git a/includes/emails/class-wc-email-customer-processing-order.php b/includes/emails/class-wc-email-customer-processing-order.php new file mode 100644 index 0000000..18b8c95 --- /dev/null +++ b/includes/emails/class-wc-email-customer-processing-order.php @@ -0,0 +1,150 @@ +id = 'customer_processing_order'; + $this->customer_email = true; + + $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'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // Triggers for this email. + add_action( 'woocommerce_order_status_cancelled_to_processing_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_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(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} order has been received!', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Thank you for your order', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order|false $order Order object. + */ + public function trigger( $order_id, $order = false ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->recipient = $this->object->get_billing_email(); + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Thanks for using {site_url}!', 'woocommerce' ); + } + } + +endif; + +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 0000000..1536ba3 --- /dev/null +++ b/includes/emails/class-wc-email-customer-refunded-order.php @@ -0,0 +1,302 @@ +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'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // 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 constructor. + parent::__construct(); + } + + /** + * Get email subject. + * + * @param bool $partial Whether it is a partial refund or a full refund. + * @since 3.1.0 + * @return string + */ + public function get_default_subject( $partial = false ) { + if ( $partial ) { + return __( 'Your {site_title} order #{order_number} has been partially refunded', 'woocommerce' ); + } else { + return __( 'Your {site_title} order #{order_number} has been refunded', 'woocommerce' ); + } + } + + /** + * Get email heading. + * + * @param bool $partial Whether it is a partial refund or a full refund. + * @since 3.1.0 + * @return string + */ + public function get_default_heading( $partial = false ) { + if ( $partial ) { + return __( 'Partial Refund: Order {order_number}', 'woocommerce' ); + } else { + return __( 'Order Refunded: {order_number}', 'woocommerce' ); + } + } + + /** + * Get email subject. + * + * @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_subject() ); + } + return apply_filters( 'woocommerce_email_subject_customer_refunded_order', $this->format_string( $subject ), $this->object, $this ); + } + + /** + * Get email heading. + * + * @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, $this ); + } + + /** + * Set email strings. + * + * @param bool $partial_refund Whether it is a partial refund or a full refund. + * @deprecated 3.1.0 Unused. + */ + public function set_email_strings( $partial_refund = false ) {} + + /** + * Full refund notification. + * + * @param int $order_id Order ID. + * @param int $refund_id 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 Order ID. + * @param int $refund_id Refund ID. + */ + public function trigger_partial( $order_id, $refund_id = null ) { + $this->trigger( $order_id, true, $refund_id ); + } + + /** + * Trigger. + * + * @param int $order_id Order ID. + * @param bool $partial_refund Whether it is a partial refund or a full refund. + * @param int $refund_id Refund ID. + */ + public function trigger( $order_id, $partial_refund = false, $refund_id = null ) { + $this->setup_locale(); + $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->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{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() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'refund' => $this->refund, + 'partial_refund' => $this->partial_refund, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'refund' => $this->refund, + 'partial_refund' => $this->partial_refund, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'We hope to see you again soon.', 'woocommerce' ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + /* translators: %s: list of placeholders */ + $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . esc_html( implode( ', ', array_keys( $this->placeholders ) ) ) . '' ); + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'subject_full' => array( + 'title' => __( 'Full refund subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'subject_partial' => array( + 'title' => __( 'Partial refund subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject( true ), + 'default' => '', + ), + 'heading_full' => array( + 'title' => __( 'Full refund email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'heading_partial' => array( + 'title' => __( 'Partial refund email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading( true ), + 'default' => '', + ), + 'additional_content' => array( + 'title' => __( 'Additional content', 'woocommerce' ), + 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + 'default' => $this->get_default_additional_content(), + 'desc_tip' => true, + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + } + +endif; + +return new WC_Email_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 new file mode 100644 index 0000000..7603097 --- /dev/null +++ b/includes/emails/class-wc-email-customer-reset-password.php @@ -0,0 +1,177 @@ +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 ); + + // Call parent constructor. + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Password Reset Request for {site_title}', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Password Reset Request', 'woocommerce' ); + } + + /** + * Trigger. + * + * @param string $user_login User login. + * @param string $reset_key Password reset key. + */ + public function trigger( $user_login = '', $reset_key = '' ) { + $this->setup_locale(); + + if ( $user_login && $reset_key ) { + $this->object = get_user_by( 'login', $user_login ); + $this->user_id = $this->object->ID; + $this->user_login = $user_login; + $this->reset_key = $reset_key; + $this->user_email = stripslashes( $this->object->user_email ); + $this->recipient = $this->user_email; + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'email_heading' => $this->get_heading(), + 'user_id' => $this->user_id, + 'user_login' => $this->user_login, + 'reset_key' => $this->reset_key, + 'blogname' => $this->get_blogname(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'email_heading' => $this->get_heading(), + 'user_id' => $this->user_id, + 'user_login' => $this->user_login, + 'reset_key' => $this->reset_key, + 'blogname' => $this->get_blogname(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Thanks for reading.', 'woocommerce' ); + } + } + +endif; + +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 0000000..0766d9a --- /dev/null +++ b/includes/emails/class-wc-email-failed-order.php @@ -0,0 +1,207 @@ +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 pending or on-hold).', 'woocommerce' ); + $this->template_html = 'emails/admin-failed-order.php'; + $this->template_plain = 'emails/plain/admin-failed-order.php'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // 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}]: Order #{order_number} has failed', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Order Failed: #{order_number}', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order|false $order Order object. + */ + public function trigger( $order_id, $order = false ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Hopefully they’ll be back. Read more about troubleshooting failed payments.', 'woocommerce' ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + /* translators: %s: list of placeholders */ + $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . esc_html( implode( ', ', array_keys( $this->placeholders ) ) ) . '' ); + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'recipient' => array( + 'title' => __( 'Recipient(s)', 'woocommerce' ), + 'type' => 'text', + /* translators: %s: WP admin email */ + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), + 'placeholder' => '', + 'default' => '', + 'desc_tip' => true, + ), + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'additional_content' => array( + 'title' => __( 'Additional content', 'woocommerce' ), + 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + 'default' => $this->get_default_additional_content(), + 'desc_tip' => true, + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + } + +endif; + +return new WC_Email_Failed_Order(); diff --git a/includes/emails/class-wc-email-new-order.php b/includes/emails/class-wc-email-new-order.php new file mode 100644 index 0000000..f4b2f38 --- /dev/null +++ b/includes/emails/class-wc-email-new-order.php @@ -0,0 +1,229 @@ +id = 'new_order'; + $this->title = __( 'New order', 'woocommerce' ); + $this->description = __( 'New order emails are sent to chosen recipient(s) when a new order is received.', 'woocommerce' ); + $this->template_html = 'emails/admin-new-order.php'; + $this->template_plain = 'emails/plain/admin-new-order.php'; + $this->placeholders = array( + '{order_date}' => '', + '{order_number}' => '', + ); + + // Triggers for this email. + add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_pending_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_processing_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_completed_notification', array( $this, 'trigger' ), 10, 2 ); + add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'trigger' ), 10, 2 ); + + // Call parent constructor. + parent::__construct(); + + // Other settings. + $this->recipient = $this->get_option( 'recipient', get_option( 'admin_email' ) ); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( '[{site_title}]: New order #{order_number}', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'New Order: #{order_number}', 'woocommerce' ); + } + + /** + * Trigger the sending of this email. + * + * @param int $order_id The order ID. + * @param WC_Order|false $order Order object. + */ + public function trigger( $order_id, $order = false ) { + $this->setup_locale(); + + if ( $order_id && ! is_a( $order, 'WC_Order' ) ) { + $order = wc_get_order( $order_id ); + } + + if ( is_a( $order, 'WC_Order' ) ) { + $this->object = $order; + $this->placeholders['{order_date}'] = wc_format_datetime( $this->object->get_date_created() ); + $this->placeholders['{order_number}'] = $this->object->get_order_number(); + + $email_already_sent = $order->get_meta( '_new_order_email_sent' ); + } + + /** + * Controls if new order emails can be resend multiple times. + * + * @since 5.0.0 + * @param bool $allows Defaults to false. + */ + if ( 'true' === $email_already_sent && ! apply_filters( 'woocommerce_new_order_email_allows_resend', false ) ) { + return; + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments() ); + + $order->update_meta_data( '_new_order_email_sent', 'true' ); + $order->save(); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => true, + 'plain_text' => false, + 'email' => $this, + ) + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'order' => $this->object, + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'sent_to_admin' => true, + 'plain_text' => true, + 'email' => $this, + ) + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'Congratulations on the sale.', 'woocommerce' ); + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + /* translators: %s: list of placeholders */ + $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . implode( ', ', array_keys( $this->placeholders ) ) . '' ); + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'recipient' => array( + 'title' => __( 'Recipient(s)', 'woocommerce' ), + 'type' => 'text', + /* translators: %s: WP admin email */ + 'description' => sprintf( __( 'Enter recipients (comma separated) for this email. Defaults to %s.', 'woocommerce' ), '' . esc_attr( get_option( 'admin_email' ) ) . '' ), + 'placeholder' => '', + 'default' => '', + 'desc_tip' => true, + ), + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'additional_content' => array( + 'title' => __( 'Additional content', 'woocommerce' ), + 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + 'default' => $this->get_default_additional_content(), + 'desc_tip' => true, + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + } + +endif; + +return new WC_Email_New_Order(); diff --git a/includes/emails/class-wc-email.php b/includes/emails/class-wc-email.php new file mode 100644 index 0000000..babe3cf --- /dev/null +++ b/includes/emails/class-wc-email.php @@ -0,0 +1,1092 @@ +', // Greater-than. + '<', // Less-than. + '&', // Ampersand. + '&', // Ampersand. + '(c)', // Copyright. + '(tm)', // Trademark. + '(R)', // Registered. + '--', // mdash. + '-', // ndash. + '*', // Bullet. + '£', // Pound sign. + 'EUR', // Euro sign. € ?. + '$', // Dollar sign. + '', // Unknown/unhandled entities. + ' ', // Runs of spaces, post-handling. + ); + + /** + * Strings to find/replace in subjects/headings. + * + * @var array + */ + protected $placeholders = array(); + + /** + * Strings to find in subjects/headings. + * + * @deprecated 3.2.0 in favour of placeholders + * @var array + */ + public $find = array(); + + /** + * Strings to replace in subjects/headings. + * + * @deprecated 3.2.0 in favour of placeholders + * @var array + */ + public $replace = array(); + + /** + * Constructor. + */ + public function __construct() { + // Find/replace. + $this->placeholders = array_merge( + array( + '{site_title}' => $this->get_blogname(), + '{site_address}' => wp_parse_url( home_url(), PHP_URL_HOST ), + '{site_url}' => wp_parse_url( home_url(), PHP_URL_HOST ), + ), + $this->placeholders + ); + + // Init settings. + $this->init_form_fields(); + $this->init_settings(); + + // Default template base if not declared in child constructor. + if ( is_null( $this->template_base ) ) { + $this->template_base = WC()->plugin_path() . '/templates/'; + } + + $this->email_type = $this->get_option( 'email_type' ); + $this->enabled = $this->get_option( 'enabled' ); + + add_action( 'phpmailer_init', array( $this, 'handle_multipart' ) ); + add_action( 'woocommerce_update_options_email_' . $this->id, array( $this, 'process_admin_options' ) ); + } + + /** + * Handle multipart mail. + * + * @param PHPMailer $mailer PHPMailer object. + * @return PHPMailer + */ + public function handle_multipart( $mailer ) { + if ( $this->sending && 'multipart' === $this->get_email_type() ) { + $mailer->AltBody = wordwrap( // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ) + ); + $this->sending = false; + } + return $mailer; + } + + /** + * Format email string. + * + * @param mixed $string Text to replace placeholders in. + * @return string + */ + public function format_string( $string ) { + $find = array_keys( $this->placeholders ); + $replace = array_values( $this->placeholders ); + + // If using legacy find replace, add those to our find/replace arrays first. @todo deprecate in 4.0.0. + $find = array_merge( (array) $this->find, $find ); + $replace = array_merge( (array) $this->replace, $replace ); + + // Take care of blogname which is no longer defined as a valid placeholder. + $find[] = '{blogname}'; + $replace[] = $this->get_blogname(); + + // If using the older style filters for find and replace, ensure the array is associative and then pass through filters. @todo deprecate in 4.0.0. + if ( has_filter( 'woocommerce_email_format_string_replace' ) || has_filter( 'woocommerce_email_format_string_find' ) ) { + $legacy_find = $this->find; + $legacy_replace = $this->replace; + + foreach ( $this->placeholders as $find => $replace ) { + $legacy_key = sanitize_title( str_replace( '_', '-', trim( $find, '{}' ) ) ); + $legacy_find[ $legacy_key ] = $find; + $legacy_replace[ $legacy_key ] = $replace; + } + + $string = str_replace( apply_filters( 'woocommerce_email_format_string_find', $legacy_find, $this ), apply_filters( 'woocommerce_email_format_string_replace', $legacy_replace, $this ), $string ); + } + + /** + * Filter for main find/replace. + * + * @since 3.2.0 + */ + return apply_filters( 'woocommerce_email_format_string', str_replace( $find, $replace, $string ), $this ); + } + + /** + * Set the locale to the store locale for customer emails to make sure emails are in the store language. + */ + public function setup_locale() { + if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_setup_locale', true ) ) { + wc_switch_to_site_locale(); + } + } + + /** + * Restore the locale to the default locale. Use after finished with setup_locale. + */ + public function restore_locale() { + if ( $this->is_customer_email() && apply_filters( 'woocommerce_email_restore_locale', true ) ) { + wc_restore_locale(); + } + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return $this->subject; + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return $this->heading; + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return ''; + } + + /** + * Return content from the additional_content field. + * + * Displayed above the footer. + * + * @since 3.7.0 + * @return string + */ + public function get_additional_content() { + $content = $this->get_option( 'additional_content', '' ); + + return apply_filters( 'woocommerce_email_additional_content_' . $this->id, $this->format_string( $content ), $this->object, $this ); + } + + /** + * Get email subject. + * + * @return string + */ + public function get_subject() { + return apply_filters( 'woocommerce_email_subject_' . $this->id, $this->format_string( $this->get_option( 'subject', $this->get_default_subject() ) ), $this->object, $this ); + } + + /** + * Get email heading. + * + * @return string + */ + public function get_heading() { + return apply_filters( 'woocommerce_email_heading_' . $this->id, $this->format_string( $this->get_option( 'heading', $this->get_default_heading() ) ), $this->object, $this ); + } + + /** + * Get valid recipients. + * + * @return string + */ + public function get_recipient() { + $recipient = apply_filters( 'woocommerce_email_recipient_' . $this->id, $this->recipient, $this->object, $this ); + $recipients = array_map( 'trim', explode( ',', $recipient ) ); + $recipients = array_filter( $recipients, 'is_email' ); + return implode( ', ', $recipients ); + } + + /** + * Get email headers. + * + * @return string + */ + public function get_headers() { + $header = 'Content-Type: ' . $this->get_content_type() . "\r\n"; + + if ( in_array( $this->id, array( 'new_order', 'cancelled_order', 'failed_order' ), true ) ) { + if ( $this->object && $this->object->get_billing_email() && ( $this->object->get_billing_first_name() || $this->object->get_billing_last_name() ) ) { + $header .= 'Reply-to: ' . $this->object->get_billing_first_name() . ' ' . $this->object->get_billing_last_name() . ' <' . $this->object->get_billing_email() . ">\r\n"; + } + } elseif ( $this->get_from_address() && $this->get_from_name() ) { + $header .= 'Reply-to: ' . $this->get_from_name() . ' <' . $this->get_from_address() . ">\r\n"; + } + + return apply_filters( 'woocommerce_email_headers', $header, $this->id, $this->object, $this ); + } + + /** + * Get email attachments. + * + * @return array + */ + public function get_attachments() { + return apply_filters( 'woocommerce_email_attachments', array(), $this->id, $this->object, $this ); + } + + /** + * Return email type. + * + * @return string + */ + public function get_email_type() { + return $this->email_type && class_exists( 'DOMDocument' ) ? $this->email_type : 'plain'; + } + + /** + * Get email content type. + * + * @param string $default_content_type Default wp_mail() content type. + * @return string + */ + public function get_content_type( $default_content_type = '' ) { + switch ( $this->get_email_type() ) { + case 'html': + $content_type = 'text/html'; + break; + case 'multipart': + $content_type = 'multipart/alternative'; + break; + default: + $content_type = 'text/plain'; + break; + } + + return apply_filters( 'woocommerce_email_content_type', $content_type, $this, $default_content_type ); + } + + /** + * Return the email's title + * + * @return string + */ + public function get_title() { + return apply_filters( 'woocommerce_email_title', $this->title, $this ); + } + + /** + * Return the email's description + * + * @return string + */ + public function get_description() { + return apply_filters( 'woocommerce_email_description', $this->description, $this ); + } + + /** + * Proxy to parent's get_option and attempt to localize the result using gettext. + * + * @param string $key Option key. + * @param mixed $empty_value Value to use when option is empty. + * @return string + */ + public function get_option( $key, $empty_value = null ) { + $value = parent::get_option( $key, $empty_value ); + return apply_filters( 'woocommerce_email_get_option', $value, $this, $value, $key, $empty_value ); + } + + /** + * Checks if this email is enabled and will be sent. + * + * @return bool + */ + public function is_enabled() { + return apply_filters( 'woocommerce_email_enabled_' . $this->id, 'yes' === $this->enabled, $this->object, $this ); + } + + /** + * Checks if this email is manually sent + * + * @return bool + */ + public function is_manual() { + return $this->manual; + } + + /** + * Checks if this email is customer focussed. + * + * @return bool + */ + public function is_customer_email() { + return $this->customer_email; + } + + /** + * Get WordPress blog name. + * + * @return string + */ + public function get_blogname() { + return wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ); + } + + /** + * Get email content. + * + * @return string + */ + public function get_content() { + $this->sending = true; + + if ( 'plain' === $this->get_email_type() ) { + $email_content = wordwrap( preg_replace( $this->plain_search, $this->plain_replace, wp_strip_all_tags( $this->get_content_plain() ) ), 70 ); + } else { + $email_content = $this->get_content_html(); + } + + return $email_content; + } + + /** + * Apply inline styles to dynamic content. + * + * We only inline CSS for html emails, and to do so we use Emogrifier library (if supported). + * + * @version 4.0.0 + * @param string|null $content Content that will receive inline styles. + * @return string + */ + public function style_inline( $content ) { + if ( in_array( $this->get_content_type(), array( 'text/html', 'multipart/alternative' ), true ) ) { + ob_start(); + wc_get_template( 'emails/email-styles.php' ); + $css = apply_filters( 'woocommerce_email_styles', ob_get_clean(), $this ); + + $emogrifier_class = 'Pelago\\Emogrifier'; + + if ( $this->supports_emogrifier() && class_exists( $emogrifier_class ) ) { + try { + $emogrifier = new $emogrifier_class( $content, $css ); + + do_action( 'woocommerce_emogrifier', $emogrifier, $this ); + + $content = $emogrifier->emogrify(); + $html_prune = \Pelago\Emogrifier\HtmlProcessor\HtmlPruner::fromHtml( $content ); + $html_prune->removeElementsWithDisplayNone(); + $content = $html_prune->render(); + } catch ( Exception $e ) { + $logger = wc_get_logger(); + $logger->error( $e->getMessage(), array( 'source' => 'emogrifier' ) ); + } + } else { + $content = '' . $content; + } + } + + return $content; + } + + /** + * Return if emogrifier library is supported. + * + * @version 4.0.0 + * @since 3.5.0 + * @return bool + */ + protected function supports_emogrifier() { + return class_exists( 'DOMDocument' ); + } + + /** + * Get the email content in plain text format. + * + * @return string + */ + public function get_content_plain() { + return ''; + } + + /** + * Get the email content in HTML format. + * + * @return string + */ + public function get_content_html() { + return ''; + } + + /** + * Get the from name for outgoing emails. + * + * @param string $from_name Default wp_mail() name associated with the "from" email address. + * @return string + */ + public function get_from_name( $from_name = '' ) { + $from_name = apply_filters( 'woocommerce_email_from_name', get_option( 'woocommerce_email_from_name' ), $this, $from_name ); + return wp_specialchars_decode( esc_html( $from_name ), ENT_QUOTES ); + } + + /** + * Get the from address for outgoing emails. + * + * @param string $from_email Default wp_mail() email address to send from. + * @return string + */ + public function get_from_address( $from_email = '' ) { + $from_email = apply_filters( 'woocommerce_email_from_address', get_option( 'woocommerce_email_from_address' ), $this, $from_email ); + return sanitize_email( $from_email ); + } + + /** + * Send an email. + * + * @param string $to Email to. + * @param string $subject Email subject. + * @param string $message Email message. + * @param string $headers Email headers. + * @param array $attachments Email attachments. + * @return bool success + */ + public function send( $to, $subject, $message, $headers, $attachments ) { + add_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); + add_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); + add_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); + + $message = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) ); + $mail_callback = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this ); + $mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, $subject, $message, $headers, $attachments ), $this ); + $return = $mail_callback( ...$mail_callback_params ); + + remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) ); + remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) ); + remove_filter( 'wp_mail_content_type', array( $this, 'get_content_type' ) ); + + /** + * Action hook fired when an email is sent. + * + * @since 5.6.0 + * @param bool $return Whether the email was sent successfully. + * @param int $id Email ID. + * @param WC_Email $this WC_Email instance. + */ + do_action( 'woocommerce_email_sent', $return, $this->id, $this ); + + return $return; + } + + /** + * Initialise Settings Form Fields - these are generic email options most will use. + */ + public function init_form_fields() { + /* translators: %s: list of placeholders */ + $placeholder_text = sprintf( __( 'Available placeholders: %s', 'woocommerce' ), '' . esc_html( implode( ', ', array_keys( $this->placeholders ) ) ) . '' ); + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable this email notification', 'woocommerce' ), + 'default' => 'yes', + ), + 'subject' => array( + 'title' => __( 'Subject', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_subject(), + 'default' => '', + ), + 'heading' => array( + 'title' => __( 'Email heading', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => $placeholder_text, + 'placeholder' => $this->get_default_heading(), + 'default' => '', + ), + 'additional_content' => array( + 'title' => __( 'Additional content', 'woocommerce' ), + 'description' => __( 'Text to appear below the main email content.', 'woocommerce' ) . ' ' . $placeholder_text, + 'css' => 'width:400px; height: 75px;', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'type' => 'textarea', + 'default' => $this->get_default_additional_content(), + 'desc_tip' => true, + ), + 'email_type' => array( + 'title' => __( 'Email type', 'woocommerce' ), + 'type' => 'select', + 'description' => __( 'Choose which format of email to send.', 'woocommerce' ), + 'default' => 'html', + 'class' => 'email_type wc-enhanced-select', + 'options' => $this->get_email_type_options(), + 'desc_tip' => true, + ), + ); + } + + /** + * Email type options. + * + * @return array + */ + public function get_email_type_options() { + $types = array( 'plain' => __( 'Plain text', 'woocommerce' ) ); + + if ( class_exists( 'DOMDocument' ) ) { + $types['html'] = __( 'HTML', 'woocommerce' ); + $types['multipart'] = __( 'Multipart', 'woocommerce' ); + } + + return $types; + } + + /** + * Admin Panel Options Processing. + */ + public function process_admin_options() { + // Save regular options. + parent::process_admin_options(); + + $post_data = $this->get_post_data(); + + // Save templates. + if ( isset( $post_data['template_html_code'] ) ) { + $this->save_template( $post_data['template_html_code'], $this->template_html ); + } + if ( isset( $post_data['template_plain_code'] ) ) { + $this->save_template( $post_data['template_plain_code'], $this->template_plain ); + } + } + + /** + * Get template. + * + * @param string $type Template type. Can be either 'template_html' or 'template_plain'. + * @return string + */ + public function get_template( $type ) { + $type = basename( $type ); + + if ( 'template_html' === $type ) { + return $this->template_html; + } elseif ( 'template_plain' === $type ) { + return $this->template_plain; + } + return ''; + } + + /** + * Save the email templates. + * + * @since 2.4.0 + * @param string $template_code Template code. + * @param string $template_path Template path. + */ + protected function save_template( $template_code, $template_path ) { + if ( current_user_can( 'edit_themes' ) && ! empty( $template_code ) && ! empty( $template_path ) ) { + $saved = false; + $file = get_stylesheet_directory() . '/' . WC()->template_path() . $template_path; + $code = wp_unslash( $template_code ); + + if ( is_writeable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writeable + $f = fopen( $file, 'w+' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen + + if ( false !== $f ) { + fwrite( $f, $code ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite + fclose( $f ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose + $saved = true; + } + } + + if ( ! $saved ) { + $redirect = add_query_arg( 'wc_error', rawurlencode( __( 'Could not write to template file.', 'woocommerce' ) ) ); + wp_safe_redirect( $redirect ); + exit; + } + } + } + + /** + * Get the template file in the current theme. + * + * @param string $template Template name. + * + * @return string + */ + public function get_theme_template_file( $template ) { + return get_stylesheet_directory() . '/' . apply_filters( 'woocommerce_template_directory', 'woocommerce', $template ) . '/' . $template; + } + + /** + * Move template action. + * + * @param string $template_type Template type. + */ + protected function move_template_action( $template_type ) { + $template = $this->get_template( $template_type ); + if ( ! empty( $template ) ) { + $theme_file = $this->get_theme_template_file( $template ); + + if ( wp_mkdir_p( dirname( $theme_file ) ) && ! file_exists( $theme_file ) ) { + + // Locate template file. + $core_file = $this->template_base . $template; + $template_file = apply_filters( 'woocommerce_locate_core_template', $core_file, $template, $this->template_base, $this->id ); + + // Copy template file. + copy( $template_file, $theme_file ); + + /** + * Action hook fired after copying email template file. + * + * @param string $template_type The copied template type + * @param string $email The email object + */ + do_action( 'woocommerce_copy_email_template', $template_type, $this ); + + ?> +
    +

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

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

    get_title() ); ?>

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

    + + +

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

    + + + +

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

    + + + +

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

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

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

    + : +

    +
    + form_fields = include __DIR__ . '/includes/settings-paypal.php'; + } + + /** + * Get the transaction URL. + * + * @param WC_Order $order Order object. + * @return string + */ + public function get_transaction_url( $order ) { + if ( $this->testmode ) { + $this->view_transaction_url = 'https://www.sandbox.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; + } else { + $this->view_transaction_url = 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; + } + return parent::get_transaction_url( $order ); + } + + /** + * Process the payment and return the result. + * + * @param int $order_id Order ID. + * @return array + */ + public function process_payment( $order_id ) { + include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-request.php'; + + $order = wc_get_order( $order_id ); + $paypal_request = new WC_Gateway_Paypal_Request( $this ); + + return array( + 'result' => 'success', + 'redirect' => $paypal_request->get_request_url( $order, $this->testmode ), + ); + } + + /** + * Can the order be refunded via PayPal? + * + * @param WC_Order $order Order object. + * @return bool + */ + public function can_refund_order( $order ) { + $has_api_creds = false; + + if ( $this->testmode ) { + $has_api_creds = $this->get_option( 'sandbox_api_username' ) && $this->get_option( 'sandbox_api_password' ) && $this->get_option( 'sandbox_api_signature' ); + } else { + $has_api_creds = $this->get_option( 'api_username' ) && $this->get_option( 'api_password' ) && $this->get_option( 'api_signature' ); + } + + return $order && $order->get_transaction_id() && $has_api_creds; + } + + /** + * Init the API class and set the username/password etc. + */ + protected function init_api() { + include_once dirname( __FILE__ ) . '/includes/class-wc-gateway-paypal-api-handler.php'; + + WC_Gateway_Paypal_API_Handler::$api_username = $this->testmode ? $this->get_option( 'sandbox_api_username' ) : $this->get_option( 'api_username' ); + WC_Gateway_Paypal_API_Handler::$api_password = $this->testmode ? $this->get_option( 'sandbox_api_password' ) : $this->get_option( 'api_password' ); + WC_Gateway_Paypal_API_Handler::$api_signature = $this->testmode ? $this->get_option( 'sandbox_api_signature' ) : $this->get_option( 'api_signature' ); + WC_Gateway_Paypal_API_Handler::$sandbox = $this->testmode; + } + + /** + * Process a refund if supported. + * + * @param int $order_id Order ID. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * @return bool|WP_Error + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $order = wc_get_order( $order_id ); + + if ( ! $this->can_refund_order( $order ) ) { + return new WP_Error( 'error', __( 'Refund failed.', 'woocommerce' ) ); + } + + $this->init_api(); + + $result = WC_Gateway_Paypal_API_Handler::refund_transaction( $order, $amount, $reason ); + + if ( is_wp_error( $result ) ) { + $this->log( 'Refund Failed: ' . $result->get_error_message(), 'error' ); + return new WP_Error( 'error', $result->get_error_message() ); + } + + $this->log( 'Refund Result: ' . wc_print_r( $result, true ) ); + + switch ( strtolower( $result->ACK ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + case 'success': + case 'successwithwarning': + $order->add_order_note( + /* translators: 1: Refund amount, 2: Refund ID */ + sprintf( __( 'Refunded %1$s - Refund ID: %2$s', 'woocommerce' ), $result->GROSSREFUNDAMT, $result->REFUNDTRANSACTIONID ) // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + ); + return true; + } + + return isset( $result->L_LONGMESSAGE0 ) ? new WP_Error( 'error', $result->L_LONGMESSAGE0 ) : false; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } + + /** + * Capture payment when the order is changed from on-hold to complete or processing + * + * @param int $order_id Order ID. + */ + public function capture_payment( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( 'paypal' === $order->get_payment_method() && 'pending' === $order->get_meta( '_paypal_status', true ) && $order->get_transaction_id() ) { + $this->init_api(); + $result = WC_Gateway_Paypal_API_Handler::do_capture( $order ); + + if ( is_wp_error( $result ) ) { + $this->log( 'Capture Failed: ' . $result->get_error_message(), 'error' ); + /* translators: %s: Paypal gateway error message */ + $order->add_order_note( sprintf( __( 'Payment could not be captured: %s', 'woocommerce' ), $result->get_error_message() ) ); + return; + } + + $this->log( 'Capture Result: ' . wc_print_r( $result, true ) ); + + // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + if ( ! empty( $result->PAYMENTSTATUS ) ) { + switch ( $result->PAYMENTSTATUS ) { + case 'Completed': + /* translators: 1: Amount, 2: Authorization ID, 3: Transaction ID */ + $order->add_order_note( sprintf( __( 'Payment of %1$s was captured - Auth ID: %2$s, Transaction ID: %3$s', 'woocommerce' ), $result->AMT, $result->AUTHORIZATIONID, $result->TRANSACTIONID ) ); + update_post_meta( $order->get_id(), '_paypal_status', $result->PAYMENTSTATUS ); + update_post_meta( $order->get_id(), '_transaction_id', $result->TRANSACTIONID ); + break; + default: + /* translators: 1: Authorization ID, 2: Payment status */ + $order->add_order_note( sprintf( __( 'Payment could not be captured - Auth ID: %1$s, Status: %2$s', 'woocommerce' ), $result->AUTHORIZATIONID, $result->PAYMENTSTATUS ) ); + break; + } + } + // phpcs:enable + } + } + + /** + * Load admin scripts. + * + * @since 3.3.0 + */ + public function admin_scripts() { + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + if ( 'woocommerce_page_wc-settings' !== $screen_id ) { + return; + } + + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + + wp_enqueue_script( 'woocommerce_paypal_admin', WC()->plugin_url() . '/includes/gateways/paypal/assets/js/paypal-admin' . $suffix . '.js', array(), $version, true ); + } + + /** + * Custom PayPal order received text. + * + * @since 3.9.0 + * @param string $text Default text. + * @param WC_Order $order Order data. + * @return string + */ + public function order_received_text( $text, $order ) { + if ( $order && $this->id === $order->get_payment_method() ) { + return esc_html__( 'Thank you for your payment. Your transaction has been completed, and a receipt for your purchase has been emailed to you. Log into your PayPal account to view transaction details.', 'woocommerce' ); + } + + return $text; + } + + /** + * Determines whether PayPal Standard should be loaded or not. + * + * By default PayPal Standard isn't loaded on new installs or on existing sites which haven't set up the gateway. + * + * @since 5.5.0 + * + * @return bool Whether PayPal Standard should be loaded. + */ + public function should_load() { + $option_key = '_should_load'; + $should_load = $this->get_option( $option_key ); + + if ( '' === $should_load ) { + + // New installs without PayPal Standard enabled don't load it. + if ( 'no' === $this->enabled && WC_Install::is_new_install() ) { + $should_load = false; + } else { + $should_load = true; + } + + $this->update_option( $option_key, wc_bool_to_string( $should_load ) ); + } else { + $should_load = wc_string_to_bool( $should_load ); + } + + /** + * Allow third-parties to filter whether PayPal Standard should be loaded or not. + * + * @since 5.5.0 + * + * @param bool $should_load Whether PayPal Standard should be loaded. + * @param WC_Gateway_Paypal $this The WC_Gateway_Paypal instance. + */ + return apply_filters( 'woocommerce_should_load_paypal_standard', $should_load, $this ); + } +} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php new file mode 100644 index 0000000..6d012d4 --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-api-handler.php @@ -0,0 +1,202 @@ + '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 Order object. + * @param float $amount Refund amount. + * @param string $reason Refund 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 Order object. + * @param float $amount 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 ( is_wp_error( $raw_response ) ) { + return $raw_response; + } elseif ( empty( $raw_response['body'] ) ) { + return new WP_Error( 'paypal-api', 'Empty Response' ); + } + + parse_str( $raw_response['body'], $response ); + + return (object) $response; + } + + /** + * Refund an order via PayPal. + * + * @param WC_Order $order Order object. + * @param float $amount Refund amount. + * @param string $reason Refund 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 ( is_wp_error( $raw_response ) ) { + return $raw_response; + } elseif ( empty( $raw_response['body'] ) ) { + return new WP_Error( 'paypal-api', 'Empty 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 { + /** + * Get refund request args. Proxy to WC_Gateway_Paypal_API_Handler::get_refund_request(). + * + * @param WC_Order $order Order object. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * + * @return array + */ + public static function get_request( $order, $amount = null, $reason = '' ) { + return self::get_refund_request( $order, $amount, $reason ); + } + + /** + * Process an order refund. + * + * @param WC_Order $order Order object. + * @param float $amount Refund amount. + * @param string $reason Refund reason. + * @param bool $sandbox Whether to use sandbox mode or not. + * @return object Either an object of name value pairs for a success, or a WP_ERROR object. + */ + 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 0000000..b98b8ee --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php @@ -0,0 +1,376 @@ +receiver_email = $receiver_email; + $this->sandbox = $sandbox; + } + + /** + * Check for PayPal IPN Response. + */ + public function check_response() { + if ( ! empty( $_POST ) && $this->validate_ipn() ) { // WPCS: CSRF ok. + $posted = wp_unslash( $_POST ); // WPCS: CSRF ok, input var ok. + + // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + do_action( 'valid-paypal-standard-ipn-request', $posted ); + 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 ) { + $order = ! empty( $posted['custom'] ) ? $this->get_paypal_order( $posted['custom'] ) : false; + + if ( $order ) { + + // Lowercase returned variables. + $posted['payment_status'] = strtolower( $posted['payment_status'] ); + + 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 ); // WPCS: CSRF ok, input var ok. + $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 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 IPN' ); + return true; + } + + WC_Gateway_Paypal::log( 'Received invalid response from PayPal IPN' ); + + 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 Transaction 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, true ) ) { + WC_Gateway_Paypal::log( 'Aborting, Invalid type:' . $txn_type ); + exit; + } + } + + /** + * Check currency from IPN matches the order. + * + * @param WC_Order $order Order object. + * @param string $currency Currency code. + */ + 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 . '")' ); + + /* translators: %s: currency code. */ + $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 Order object. + * @param int $amount Amount to validate. + */ + 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 . ')' ); + + /* translators: %s: Amount. */ + $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 Order object. + * @param string $receiver_email Email to validate. + */ + 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}" ); + + /* translators: %s: email address . */ + $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 Order object. + * @param array $posted Posted data. + */ + 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 ); + } + + if ( ! empty( $posted['mc_fee'] ) ) { + $order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $posted['mc_fee'] ) ); + } + + $this->payment_complete( $order, ( ! empty( $posted['txn_id'] ) ? wc_clean( $posted['txn_id'] ) : '' ), __( 'IPN payment completed', 'woocommerce' ) ); + } 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 { + /* translators: %s: pending reason. */ + $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $posted['pending_reason'] ) ); + } + } + } + + /** + * Handle a pending payment. + * + * @param WC_Order $order Order object. + * @param array $posted Posted data. + */ + protected function payment_status_pending( $order, $posted ) { + $this->payment_status_completed( $order, $posted ); + } + + /** + * Handle a failed payment. + * + * @param WC_Order $order Order object. + * @param array $posted Posted data. + */ + protected function payment_status_failed( $order, $posted ) { + /* translators: %s: payment status. */ + $order->update_status( 'failed', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) ); + } + + /** + * Handle a denied payment. + * + * @param WC_Order $order Order object. + * @param array $posted Posted data. + */ + protected function payment_status_denied( $order, $posted ) { + $this->payment_status_failed( $order, $posted ); + } + + /** + * Handle an expired payment. + * + * @param WC_Order $order Order object. + * @param array $posted Posted data. + */ + protected function payment_status_expired( $order, $posted ) { + $this->payment_status_failed( $order, $posted ); + } + + /** + * Handle a voided payment. + * + * @param WC_Order $order Order object. + * @param array $posted Posted data. + */ + 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 Order object. + * @param array $posted Posted data. + */ + protected function payment_status_paid_cancelled_order( $order, $posted ) { + $this->send_ipn_email_notification( + /* translators: %s: order link. */ + sprintf( __( 'Payment for cancelled order %s received', 'woocommerce' ), '' . $order->get_order_number() . '' ), + /* translators: %s: order ID. */ + sprintf( __( 'Order #%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 Order object. + * @param array $posted Posted data. + */ + protected function payment_status_refunded( $order, $posted ) { + // Only handle full refunds, not partial. + if ( $order->get_total() === wc_format_decimal( $posted['mc_gross'] * -1, wc_get_price_decimals() ) ) { + + /* translators: %s: payment status. */ + $order->update_status( 'refunded', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), strtolower( $posted['payment_status'] ) ) ); + + $this->send_ipn_email_notification( + /* translators: %s: order link. */ + sprintf( __( 'Payment for order %s refunded', 'woocommerce' ), '' . $order->get_order_number() . '' ), + /* translators: %1$s: order ID, %2$s: reason code. */ + 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 Order object. + * @param array $posted Posted data. + */ + protected function payment_status_reversed( $order, $posted ) { + /* translators: %s: payment status. */ + $order->update_status( 'on-hold', sprintf( __( 'Payment %s via IPN.', 'woocommerce' ), wc_clean( $posted['payment_status'] ) ) ); + + $this->send_ipn_email_notification( + /* translators: %s: order link. */ + sprintf( __( 'Payment for order %s reversed', 'woocommerce' ), '' . $order->get_order_number() . '' ), + /* translators: %1$s: order ID, %2$s: reason code. */ + 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 Order object. + * @param array $posted Posted data. + */ + protected function payment_status_canceled_reversal( $order, $posted ) { + $this->send_ipn_email_notification( + /* translators: %s: order link. */ + sprintf( __( 'Reversal cancelled for order #%s', 'woocommerce' ), $order->get_order_number() ), + /* translators: %1$s: order ID, %2$s: order link. */ + 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( $order->get_edit_order_url() ) ) + ); + } + + /** + * Save important data from the IPN to the order. + * + * @param WC_Order $order Order object. + * @param array $posted Posted data. + */ + protected function save_paypal_meta_data( $order, $posted ) { + 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 Email subject. + * @param string $message Email 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 ); + + $woocommerce_paypal_settings = get_option( 'woocommerce_paypal_settings' ); + if ( ! empty( $woocommerce_paypal_settings['ipn_notification'] ) && 'no' === $woocommerce_paypal_settings['ipn_notification'] ) { + return; + } + + $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 0000000..c37d4d1 --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-pdt-handler.php @@ -0,0 +1,138 @@ +identity_token = $identity_token; + $this->sandbox = $sandbox; + } + + /** + * Validate a PDT transaction to ensure its authentic. + * + * @param string $transaction TX ID. + * @return bool|array False or result array if successful and valid. + */ + protected function validate_transaction( $transaction ) { + $pdt = array( + 'body' => array( + 'cmd' => '_notify-synch', + 'tx' => $transaction, + 'at' => $this->identity_token, + ), + 'timeout' => 60, + 'httpversion' => '1.1', + 'user-agent' => 'WooCommerce/' . Constants::get_constant( 'WC_VERSION' ), + ); + + // Post back to get a response. + $response = wp_safe_remote_post( $this->sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr', $pdt ); + + if ( is_wp_error( $response ) || strpos( $response['body'], 'SUCCESS' ) !== 0 ) { + return false; + } + + // Parse transaction result data. + $transaction_result = array_map( 'wc_clean', array_map( 'urldecode', explode( "\n", $response['body'] ) ) ); + $transaction_results = array(); + + foreach ( $transaction_result as $line ) { + $line = explode( '=', $line ); + $transaction_results[ $line[0] ] = isset( $line[1] ) ? $line[1] : ''; + } + + if ( ! empty( $transaction_results['charset'] ) && function_exists( 'iconv' ) ) { + foreach ( $transaction_results as $key => $value ) { + $transaction_results[ $key ] = iconv( $transaction_results['charset'], 'utf-8', $value ); + } + } + + return $transaction_results; + } + + /** + * Check Response for PDT. + */ + public function check_response() { + if ( empty( $_REQUEST['cm'] ) || empty( $_REQUEST['tx'] ) || empty( $_REQUEST['st'] ) ) { // WPCS: Input var ok, CSRF ok, sanitization ok. + return; + } + + $order_id = wc_clean( wp_unslash( $_REQUEST['cm'] ) ); // WPCS: input var ok, CSRF ok, sanitization ok. + $status = wc_clean( strtolower( wp_unslash( $_REQUEST['st'] ) ) ); // WPCS: input var ok, CSRF ok, sanitization ok. + $amount = isset( $_REQUEST['amt'] ) ? wc_clean( wp_unslash( $_REQUEST['amt'] ) ) : 0; // WPCS: input var ok, CSRF ok, sanitization ok. + $transaction = wc_clean( wp_unslash( $_REQUEST['tx'] ) ); // WPCS: input var ok, CSRF ok, sanitization ok. + $order = $this->get_paypal_order( $order_id ); + + if ( ! $order || ! $order->needs_payment() ) { + return false; + } + + $transaction_result = $this->validate_transaction( $transaction ); + + if ( $transaction_result ) { + WC_Gateway_Paypal::log( 'PDT Transaction Status: ' . wc_print_r( $status, true ) ); + + $order->add_meta_data( '_paypal_status', $status ); + $order->set_transaction_id( $transaction ); + + if ( 'completed' === $status ) { + if ( number_format( $order->get_total(), 2, '.', '' ) !== number_format( $amount, 2, '.', '' ) ) { + WC_Gateway_Paypal::log( 'Payment error: Amounts do not match (amt ' . $amount . ')', 'error' ); + /* translators: 1: Payment amount */ + $this->payment_on_hold( $order, sprintf( __( 'Validation error: PayPal amounts do not match (amt %s).', 'woocommerce' ), $amount ) ); + } else { + // Log paypal transaction fee and payment type. + if ( ! empty( $transaction_result['mc_fee'] ) ) { + $order->add_meta_data( 'PayPal Transaction Fee', wc_clean( $transaction_result['mc_fee'] ) ); + } + if ( ! empty( $transaction_result['payment_type'] ) ) { + $order->add_meta_data( 'Payment type', wc_clean( $transaction_result['payment_type'] ) ); + } + + $this->payment_complete( $order, $transaction, __( 'PDT payment completed', 'woocommerce' ) ); + } + } else { + if ( 'authorization' === $transaction_result['pending_reason'] ) { + $this->payment_on_hold( $order, __( 'Payment authorized. Change payment status to processing or complete to capture funds.', 'woocommerce' ) ); + } else { + /* translators: 1: Pending reason */ + $this->payment_on_hold( $order, sprintf( __( 'Payment pending (%s).', 'woocommerce' ), $transaction_result['pending_reason'] ) ); + } + } + } else { + WC_Gateway_Paypal::log( 'Received invalid response from PayPal PDT' ); + } + } +} diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php new file mode 100644 index 0000000..b7b0a94 --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php @@ -0,0 +1,580 @@ +gateway = $gateway; + $this->notify_url = WC()->api_request_url( 'WC_Gateway_Paypal' ); + } + + /** + * Get the PayPal request URL for an order. + * + * @param WC_Order $order Order object. + * @param bool $sandbox Whether to use sandbox mode or not. + * @return string + */ + public function get_request_url( $order, $sandbox = false ) { + $this->endpoint = $sandbox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr?test_ipn=1&' : 'https://www.paypal.com/cgi-bin/webscr?'; + $paypal_args = $this->get_paypal_args( $order ); + $paypal_args['bn'] = 'WooThemes_Cart'; // Append WooCommerce PayPal Partner Attribution ID. This should not be overridden for this gateway. + + // Mask (remove) PII from the logs. + $mask = array( + 'first_name' => '***', + 'last_name' => '***', + 'address1' => '***', + 'address2' => '***', + 'city' => '***', + 'state' => '***', + 'zip' => '***', + 'country' => '***', + 'email' => '***@***', + 'night_phone_a' => '***', + 'night_phone_b' => '***', + 'night_phone_c' => '***', + ); + + WC_Gateway_Paypal::log( 'PayPal Request Args for order ' . $order->get_order_number() . ': ' . wc_print_r( array_merge( $paypal_args, array_intersect_key( $mask, $paypal_args ) ), true ) ); + + return $this->endpoint . http_build_query( $paypal_args, '', '&' ); + } + + /** + * Limit length of an arg. + * + * @param string $string Argument to limit. + * @param integer $limit Limit size in characters. + * @return string + */ + protected function limit_length( $string, $limit = 127 ) { + $str_limit = $limit - 3; + if ( function_exists( 'mb_strimwidth' ) ) { + if ( mb_strlen( $string ) > $limit ) { + $string = mb_strimwidth( $string, 0, $str_limit ) . '...'; + } + } else { + if ( strlen( $string ) > $limit ) { + $string = substr( $string, 0, $str_limit ) . '...'; + } + } + return $string; + } + + /** + * Get transaction args for paypal request, except for line item args. + * + * @param WC_Order $order Order object. + * @return array + */ + protected function get_transaction_args( $order ) { + return 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() ), + 'image_url' => esc_url_raw( $this->gateway->get_option( 'image_url' ) ), + 'paymentaction' => $this->gateway->get_option( 'paymentaction' ), + 'invoice' => $this->limit_length( $this->gateway->get_option( 'invoice_prefix' ) . $order->get_order_number(), 127 ), + 'custom' => wp_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 ) + ); + } + + /** + * If the default request with line items is too long, generate a new one with only one line item. + * + * If URL is longer than 2,083 chars, ignore line items and send cart to Paypal as a single item. + * One item's name can only be 127 characters long, so the URL should not be longer than limit. + * URL character limit via: + * https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer. + * + * @param WC_Order $order Order to be sent to Paypal. + * @param array $paypal_args Arguments sent to Paypal in the request. + * @return array + */ + protected function fix_request_length( $order, $paypal_args ) { + $max_paypal_length = 2083; + $query_candidate = http_build_query( $paypal_args, '', '&' ); + + if ( strlen( $this->endpoint . $query_candidate ) <= $max_paypal_length ) { + return $paypal_args; + } + + return apply_filters( + 'woocommerce_paypal_args', + array_merge( + $this->get_transaction_args( $order ), + $this->get_line_item_args( $order, true ) + ), + $order + ); + + } + + /** + * Get PayPal Args for passing to PP. + * + * @param WC_Order $order Order object. + * @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 ); + + $force_one_line_item = apply_filters( 'woocommerce_paypal_force_one_line_item', false, $order ); + + if ( ( wc_tax_enabled() && wc_prices_include_tax() ) || ! $this->line_items_valid( $order ) ) { + $force_one_line_item = true; + } + + $paypal_args = apply_filters( + 'woocommerce_paypal_args', + array_merge( + $this->get_transaction_args( $order ), + $this->get_line_item_args( $order, $force_one_line_item ) + ), + $order + ); + + return $this->fix_request_length( $order, $paypal_args ); + } + + /** + * Get phone number args for paypal request. + * + * @param WC_Order $order Order object. + * @return array + */ + protected function get_phone_number_args( $order ) { + $phone_number = wc_sanitize_phone_number( $order->get_billing_phone() ); + + if ( in_array( $order->get_billing_country(), array( 'US', 'CA' ), true ) ) { + $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 { + $calling_code = WC()->countries->get_country_calling_code( $order->get_billing_country() ); + $calling_code = is_array( $calling_code ) ? $calling_code[0] : $calling_code; + + if ( $calling_code ) { + $phone_number = str_replace( $calling_code, '', preg_replace( '/^0/', '', $order->get_billing_phone() ) ); + } + + $phone_args = array( + 'night_phone_a' => $calling_code, + 'night_phone_b' => $phone_number, + ); + } + return $phone_args; + } + + /** + * Get shipping args for paypal request. + * + * @param WC_Order $order Order object. + * @return array + */ + protected function get_shipping_args( $order ) { + $shipping_args = array(); + if ( $order->needs_shipping_address() ) { + $shipping_args['address_override'] = $this->gateway->get_option( 'address_override' ) === 'yes' ? 1 : 0; + $shipping_args['no_shipping'] = 0; + if ( 'yes' === $this->gateway->get_option( 'send_shipping' ) ) { + // 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 shipping cost line item args for paypal request. + * + * @param WC_Order $order Order object. + * @param bool $force_one_line_item Whether one line item was forced by validation or URL length. + * @return array + */ + protected function get_shipping_cost_line_item( $order, $force_one_line_item ) { + $line_item_args = array(); + $shipping_total = $order->get_shipping_total(); + if ( $force_one_line_item ) { + $shipping_total += $order->get_shipping_tax(); + } + + // 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( $shipping_total, $order ); + } elseif ( $order->get_shipping_total() > 0 ) { + /* translators: %s: Order shipping method */ + $this->add_line_item( sprintf( __( 'Shipping via %s', 'woocommerce' ), $order->get_shipping_method() ), 1, $this->number_format( $shipping_total, $order ) ); + } + + return $line_item_args; + } + + /** + * Get line item args for paypal request as a single line item. + * + * @param WC_Order $order Order object. + * @return array + */ + protected function get_line_item_args_single_item( $order ) { + $this->delete_line_items(); + + $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() ); + $line_item_args = $this->get_shipping_cost_line_item( $order, true ); + + return array_merge( $line_item_args, $this->get_line_items() ); + } + + /** + * Get line item args for paypal request. + * + * @param WC_Order $order Order object. + * @param bool $force_one_line_item Create only one item for this order. + * @return array + */ + protected function get_line_item_args( $order, $force_one_line_item = false ) { + $line_item_args = array(); + + if ( $force_one_line_item ) { + /** + * 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). + */ + $line_item_args = $this->get_line_item_args_single_item( $order ); + } else { + /** + * Passing a line item per product if supported. + */ + $this->prepare_line_items( $order ); + $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 ); + } + + $line_item_args = array_merge( $line_item_args, $this->get_shipping_cost_line_item( $order, false ) ); + $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 Order object. + * @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 = wp_strip_all_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 Order object. + * @param WC_Order_Item $item Order item object. + * @return string + */ + protected function get_order_item_name( $order, $item ) { + $item_name = $item->get_name(); + $item_meta = wp_strip_all_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(); + } + + /** + * Check if the order has valid line items to use for PayPal request. + * + * The line items are invalid in case of mismatch in totals or if any amount < 0. + * + * @param WC_Order $order Order to be examined. + * @return bool + */ + protected function line_items_valid( $order ) { + $negative_item_amount = false; + $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 ); + $calculated_total += $item_line_total; + } else { + $item_line_total = $this->number_format( $order->get_item_subtotal( $item, false ), $order ); + $calculated_total += $item_line_total * $item->get_quantity(); + } + + if ( $item_line_total < 0 ) { + $negative_item_amount = true; + } + } + $mismatched_totals = $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 ! $negative_item_amount && ! $mismatched_totals; + } + + /** + * Get line items to send to paypal. + * + * @param WC_Order $order Order object. + */ + protected function prepare_line_items( $order ) { + $this->delete_line_items(); + + // 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 ); + $this->add_line_item( $item->get_name(), 1, $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 ); + $this->add_line_item( $this->get_order_item_name( $order, $item ), $item->get_quantity(), $item_line_total, $sku ); + } + } + } + + /** + * Add PayPal Line Item. + * + * @param string $item_name Item name. + * @param int $quantity Item quantity. + * @param float $amount Amount. + * @param string $item_number Item number. + */ + protected function add_line_item( $item_name, $quantity = 1, $amount = 0.0, $item_number = '' ) { + $index = ( count( $this->line_items ) / 4 ) + 1; + + $item = apply_filters( + 'woocommerce_paypal_line_item', + array( + 'item_name' => html_entity_decode( wc_trim_string( $item_name ? wp_strip_all_tags( $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 ); + } + + /** + * Get the state to send to paypal. + * + * @param string $cc Country two letter code. + * @param string $state State code. + * @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 Currency to check. + * @return bool + */ + protected function currency_has_decimals( $currency ) { + if ( in_array( $currency, array( 'HUF', 'JPY', 'TWD' ), true ) ) { + return false; + } + + return true; + } + + /** + * Round prices. + * + * @param double $price Price to round. + * @param WC_Order $order Order object. + * @return double + */ + protected function round( $price, $order ) { + $precision = 2; + + if ( ! $this->currency_has_decimals( $order->get_currency() ) ) { + $precision = 0; + } + + return NumberUtil::round( $price, $precision ); + } + + /** + * Format prices. + * + * @param float|int $price Price to format. + * @param WC_Order $order Order object. + * @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 0000000..ba3ab28 --- /dev/null +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-response.php @@ -0,0 +1,89 @@ +order_id; + $order_key = $custom->order_key; + } else { + // Nothing was found. + WC_Gateway_Paypal::log( 'Order ID and key were not found in "custom".', 'error' ); + return false; + } + + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + // 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 || ! hash_equals( $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 Order object. + * @param string $txn_id Transaction ID. + * @param string $note Payment note. + */ + protected function payment_complete( $order, $txn_id = '', $note = '' ) { + if ( ! $order->has_status( array( 'processing', 'completed' ) ) ) { + $order->add_order_note( $note ); + $order->payment_complete( $txn_id ); + + if ( isset( WC()->cart ) ) { + WC()->cart->empty_cart(); + } + } + } + + /** + * Hold order and add note. + * + * @param WC_Order $order Order object. + * @param string $reason Reason why the payment is on hold. + */ + protected function payment_on_hold( $order, $reason = '' ) { + $order->update_status( 'on-hold', $reason ); + + if ( isset( WC()->cart ) ) { + 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 0000000..5372942 --- /dev/null +++ b/includes/gateways/paypal/includes/settings-paypal.php @@ -0,0 +1,178 @@ + array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal Standard', 'woocommerce' ), + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'PayPal', 'woocommerce' ), + 'desc_tip' => true, + ), + 'description' => array( + 'title' => __( 'Description', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'This controls the description which the user sees during checkout.', 'woocommerce' ), + 'default' => __( "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", 'woocommerce' ), + ), + 'email' => array( + 'title' => __( 'PayPal email', 'woocommerce' ), + 'type' => 'email', + 'description' => __( 'Please enter your PayPal email address; this is needed in order to take payment.', 'woocommerce' ), + 'default' => get_option( 'admin_email' ), + 'desc_tip' => true, + 'placeholder' => 'you@youremail.com', + ), + 'advanced' => array( + 'title' => __( 'Advanced options', 'woocommerce' ), + 'type' => 'title', + 'description' => '', + ), + 'testmode' => array( + 'title' => __( 'PayPal sandbox', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable PayPal sandbox', 'woocommerce' ), + 'default' => 'no', + /* translators: %s: URL */ + 'description' => sprintf( __( 'PayPal sandbox can be used to test payments. Sign up for a developer account.', 'woocommerce' ), 'https://developer.paypal.com/' ), + ), + 'debug' => array( + 'title' => __( 'Debug log', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable logging', 'woocommerce' ), + 'default' => 'no', + /* translators: %s: URL */ + 'description' => sprintf( __( 'Log PayPal events, such as IPN requests, inside %s Note: this may log personal information. We recommend using this for debugging purposes only and deleting the logs when finished.', 'woocommerce' ), '' . WC_Log_Handler_File::get_log_file_path( 'paypal' ) . '' ), + ), + 'ipn_notification' => array( + 'title' => __( 'IPN email notifications', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable IPN email notifications', 'woocommerce' ), + 'default' => 'yes', + 'description' => __( 'Send notifications when an IPN is received from PayPal indicating refunds, chargebacks and cancellations.', 'woocommerce' ), + ), + 'receiver_email' => array( + 'title' => __( 'Receiver email', 'woocommerce' ), + 'type' => 'email', + 'description' => __( 'If your main PayPal email differs from the PayPal email entered above, input your main receiver email for your PayPal account here. This is used to validate IPN requests.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => 'you@youremail.com', + ), + 'identity_token' => array( + 'title' => __( 'PayPal identity token', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Optionally enable "Payment Data Transfer" (Profile > Profile and Settings > My Selling Tools > Website Preferences) and then copy your identity token here. This will allow payments to be verified without the need for PayPal IPN.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => '', + ), + 'invoice_prefix' => array( + 'title' => __( 'Invoice prefix', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Please enter a prefix for your invoice numbers. If you use your PayPal account for multiple stores ensure this prefix is unique as PayPal will not allow orders with the same invoice number.', 'woocommerce' ), + 'default' => 'WC-', + 'desc_tip' => true, + ), + 'send_shipping' => array( + 'title' => __( 'Shipping details', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Send shipping details to PayPal instead of billing.', 'woocommerce' ), + 'description' => __( 'PayPal allows us to send one address. If you are using PayPal for shipping labels you may prefer to send the shipping address rather than billing. Turning this option off may prevent PayPal Seller protection from applying.', 'woocommerce' ), + 'default' => 'yes', + ), + 'address_override' => array( + 'title' => __( 'Address override', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Enable "address_override" to prevent address information from being changed.', 'woocommerce' ), + 'description' => __( 'PayPal verifies addresses therefore this setting can cause errors (we recommend keeping it disabled).', 'woocommerce' ), + 'default' => 'no', + ), + 'paymentaction' => array( + 'title' => __( 'Payment action', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'description' => __( 'Choose whether you wish to capture funds immediately or authorize payment only.', 'woocommerce' ), + 'default' => 'sale', + 'desc_tip' => true, + 'options' => array( + 'sale' => __( 'Capture', 'woocommerce' ), + 'authorization' => __( 'Authorize', 'woocommerce' ), + ), + ), + 'image_url' => array( + 'title' => __( 'Image url', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Optionally enter the URL to a 150x50px image displayed as your logo in the upper left corner of the PayPal checkout pages.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_details' => array( + 'title' => __( 'API credentials', 'woocommerce' ), + 'type' => 'title', + /* translators: %s: URL */ + 'description' => sprintf( __( 'Enter your PayPal API credentials to process refunds via PayPal. Learn how to access your PayPal API Credentials.', 'woocommerce' ), 'https://developer.paypal.com/webapps/developer/docs/classic/api/apiCredentials/#create-an-api-signature' ), + ), + 'api_username' => array( + 'title' => __( 'Live API username', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_password' => array( + 'title' => __( 'Live API password', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'api_signature' => array( + 'title' => __( 'Live API signature', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'sandbox_api_username' => array( + 'title' => __( 'Sandbox API username', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'sandbox_api_password' => array( + 'title' => __( 'Sandbox API password', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), + 'sandbox_api_signature' => array( + 'title' => __( 'Sandbox API signature', 'woocommerce' ), + 'type' => 'password', + 'description' => __( 'Get your API credentials from PayPal.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Optional', 'woocommerce' ), + ), +); diff --git a/includes/import/abstract-wc-product-importer.php b/includes/import/abstract-wc-product-importer.php new file mode 100644 index 0000000..edf74a9 --- /dev/null +++ b/includes/import/abstract-wc-product-importer.php @@ -0,0 +1,812 @@ +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() { + /** + * Filter product importer parsed data. + * + * @param array $parsed_data Parsed data. + * @param WC_Product_Importer $importer Importer instance. + */ + return apply_filters( 'woocommerce_product_importer_parsed_data', $this->parsed_data, $this ); + } + + /** + * Get importer parameters. + * + * @return array + */ + public function get_params() { + return $this->params; + } + + /** + * 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( NumberUtil::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'] ) ) { + + if ( ! array_key_exists( $data['type'], WC_Admin_Exporters::get_product_types() ) ) { + return new WP_Error( 'woocommerce_product_importer_invalid_type', __( 'Invalid product type.', 'woocommerce' ), array( 'status' => 401 ) ); + } + + try { + // Prevent getting "variation_invalid_id" error message from Variation Data Store. + if ( 'variation' === $data['type'] ) { + $id = wp_update_post( + array( + 'ID' => $id, + 'post_type' => 'product_variation', + ) + ); + } + + $product = wc_get_product_object( $data['type'], $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( 'woocommerce_product_csv_importer_' . $e->getErrorCode(), $e->getMessage(), array( 'status' => 401 ) ); + } + } elseif ( ! empty( $data['id'] ) ) { + $product = wc_get_product( $id ); + + if ( ! $product ) { + return new WP_Error( + 'woocommerce_product_csv_importer_invalid_id', + /* translators: %d: product ID */ + sprintf( __( 'Invalid product ID %d.', 'woocommerce' ), $id ), + array( + 'id' => $id, + 'status' => 401, + ) + ); + } + } else { + $product = wc_get_product_object( 'simple', $id ); + } + + return apply_filters( 'woocommerce_product_import_get_product_object', $product, $data ); + } + + /** + * Process a single item and save. + * + * @throws Exception If item cannot be processed. + * @param array $data Raw CSV data. + * @return array|WP_Error + */ + protected function process_item( $data ) { + try { + do_action( 'woocommerce_product_import_before_process_item', $data ); + $data = apply_filters( 'woocommerce_product_import_process_item_data', $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'] ); + + if ( $product_id ) { + $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'], $data['low_stock_amount'] ); + } + + if ( 'variation' === $object->get_type() ) { + if ( isset( $data['status'] ) && -1 === $data['status'] ) { + $data['status'] = 0; // Variations cannot be drafts - set to private. + } + } + + 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. + * @throws Exception If data cannot be set. + */ + protected function set_product_data( &$product, $data ) { + if ( isset( $data['raw_attributes'] ) ) { + $attributes = array(); + $default_attributes = array(); + $existing_attributes = $product->get_attributes(); + + 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; + } + + // Get name. + $attribute_name = $attribute_id ? wc_attribute_taxonomy_name_by_id( $attribute_id ) : $attribute['name']; + + // Set if is a variation attribute based on existing attributes if possible so updates via CSV do not change this. + $is_variation = 0; + + if ( $existing_attributes ) { + foreach ( $existing_attributes as $existing_attribute ) { + if ( $existing_attribute->get_name() === $attribute_name ) { + $is_variation = $existing_attribute->get_variation(); + break; + } + } + } + + if ( $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, true ) ) { + $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'], true ) ) { + $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 If data cannot be set. + */ + 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 ) ); + } + + // Stop if parent is a product variation. + if ( $parent->is_type( 'variation' ) ) { + return new WP_Error( 'woocommerce_product_importer_parent_set_as_variation', __( 'Variation cannot be imported: Parent product cannot be a product variation', '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 ) { + $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'] ); + } + + 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 generate 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 + * @throws Exception If attachment cannot be loaded. + */ + public function get_attachment_id_from_url( $url, $product_id ) { + if ( empty( $url ) ) { + return 0; + } + + $id = 0; + $upload_dir = wp_upload_dir( null, false ); + $base_url = $upload_dir['baseurl'] . '/'; + + // Check first if attachment is inside the WordPress uploads directory, or we're given a filename only. + if ( false !== strpos( $url, $base_url ) || false === strpos( $url, '://' ) ) { + // Search for yyyy/mm/slug.extension or slug.extension - remove the base URL. + $file = str_replace( $base_url, '', $url ); + $args = array( + 'post_type' => 'attachment', + 'post_status' => 'any', + 'fields' => 'ids', + 'meta_query' => array( // @codingStandardsIgnoreLine. + 'relation' => 'OR', + array( + 'key' => '_wp_attached_file', + 'value' => '^' . $file, + 'compare' => 'REGEXP', + ), + array( + 'key' => '_wp_attached_file', + 'value' => '/' . $file, + 'compare' => 'LIKE', + ), + array( + 'key' => '_wc_attachment_source', + 'value' => '/' . $file, + 'compare' => 'LIKE', + ), + ), + ); + } else { + // This is an external URL, so compare to source. + $args = array( + 'post_type' => 'attachment', + 'post_status' => 'any', + 'fields' => 'ids', + 'meta_query' => array( // @codingStandardsIgnoreLine. + array( + 'value' => $url, + 'key' => '_wc_attachment_source', + ), + ), + ); + } + + $ids = get_posts( $args ); // @codingStandardsIgnoreLine. + + if ( $ids ) { + $id = current( $ids ); + } + + // Upload if attachment does not exists. + if ( ! $id && stristr( $url, '://' ) ) { + $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 ) ) { + /* translators: %s: image URL */ + 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 ); + } + + if ( ! $id ) { + /* translators: %s: image URL */ + throw new Exception( sprintf( __( 'Unable to use image "%s".', 'woocommerce' ), $url ), 400 ); + } + + return $id; + } + + /** + * Get attribute taxonomy ID from the imported data. + * If does not exists register a new attribute. + * + * @param string $raw_name Attribute name. + * @return int + * @throws Exception If taxonomy cannot be loaded. + */ + public 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 = array_search( $raw_name, $attribute_labels, true ); + + if ( ! $attribute_name ) { + $attribute_name = wc_sanitize_taxonomy_name( $raw_name ); + } + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name ); + + // Get the ID from the name. + if ( $attribute_id ) { + return $attribute_id; + } + + // If the attribute does not exist, create it. + $attribute_id = wc_create_attribute( + array( + 'name' => $raw_name, + 'slug' => $attribute_name, + 'type' => 'select', + 'order_by' => 'menu_order', + 'has_archives' => false, + ) + ); + + if ( is_wp_error( $attribute_id ) ) { + throw new Exception( $attribute_id->get_error_message(), 400 ); + } + + // Register as taxonomy while importing. + $taxonomy_name = wc_attribute_taxonomy_name( $attribute_name ); + register_taxonomy( + $taxonomy_name, + apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ), + apply_filters( + 'woocommerce_taxonomy_args_' . $taxonomy_name, + array( + 'labels' => array( + 'name' => $raw_name, + ), + 'hierarchical' => true, + 'show_ui' => false, + 'query_var' => true, + 'rewrite' => false, + ) + ) + ); + + // Set product attributes global. + $wc_product_attributes = array(); + + foreach ( wc_get_attribute_taxonomies() as $taxonomy ) { + $wc_product_attributes[ wc_attribute_taxonomy_name( $taxonomy->attribute_name ) ] = $taxonomy; + } + + 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 ); + } + + /** + * Explode CSV cell values using commas by default, and handling escaped + * separators. + * + * @since 3.2.0 + * @param string $value Value to explode. + * @param string $separator Separator separating each value. Defaults to comma. + * @return array + */ + protected function explode_values( $value, $separator = ',' ) { + $value = str_replace( '\\,', '::separator::', $value ); + $values = explode( $separator, $value ); + $values = array_map( array( $this, 'explode_values_formatter' ), $values ); + + return $values; + } + + /** + * Remove formatting and trim each value. + * + * @since 3.2.0 + * @param string $value Value to format. + * @return string + */ + protected function explode_values_formatter( $value ) { + return trim( str_replace( '::separator::', ',', $value ) ); + } + + /** + * The exporter prepends a ' to escape fields that start with =, +, - or @. + * Remove the prepended ' character preceding those characters. + * + * @since 3.5.2 + * @param string $value A string that may or may not have been escaped with '. + * @return string + */ + protected function unescape_data( $value ) { + $active_content_triggers = array( "'=", "'+", "'-", "'@" ); + + if ( in_array( mb_substr( $value, 0, 2 ), $active_content_triggers, true ) ) { + $value = mb_substr( $value, 1 ); + } + + return $value; + } + +} 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 0000000..78d65f7 --- /dev/null +++ b/includes/import/class-wc-product-csv-importer.php @@ -0,0 +1,1135 @@ + 0, // File pointer start. + 'end_pos' => -1, // File pointer end. + 'lines' => -1, // Max lines to read. + 'mapping' => array(), // Column mapping. csv_heading => schema_heading. + 'parse' => false, // Whether to sanitize and format data. + 'update_existing' => false, // Whether to update existing items. + 'delimiter' => ',', // CSV delimiter. + 'prevent_timeouts' => true, // Check memory and time usage and abort if reaching limit. + 'enclosure' => '"', // The character used to wrap text in the CSV. + 'escape' => "\0", // PHP uses '\' as the default escape character. This is not RFC-4180 compliant. This disables the escape character. + ); + + $this->params = wp_parse_args( $params, $default_args ); + $this->file = $file; + + if ( isset( $this->params['mapping']['from'], $this->params['mapping']['to'] ) ) { + $this->params['mapping'] = array_combine( $this->params['mapping']['from'], $this->params['mapping']['to'] ); + } + + // Import mappings for CSV data. + include_once dirname( dirname( __FILE__ ) ) . '/admin/importers/mappings/mappings.php'; + + $this->read_file(); + } + + /** + * Read file. + */ + protected function read_file() { + if ( ! WC_Product_CSV_Importer_Controller::is_file_valid_csv( $this->file ) ) { + wp_die( esc_html__( 'Invalid file type. The importer supports CSV and TXT file formats.', 'woocommerce' ) ); + } + + $handle = fopen( $this->file, 'r' ); // @codingStandardsIgnoreLine. + + if ( false !== $handle ) { + $this->raw_keys = version_compare( PHP_VERSION, '5.3', '>=' ) ? array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) ) : array_map( 'trim', fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ) ); // @codingStandardsIgnoreLine + + // Remove BOM signature from the first item. + if ( isset( $this->raw_keys[0] ) ) { + $this->raw_keys[0] = $this->remove_utf8_bom( $this->raw_keys[0] ); + } + + if ( 0 !== $this->params['start_pos'] ) { + fseek( $handle, (int) $this->params['start_pos'] ); + } + + while ( 1 ) { + $row = version_compare( PHP_VERSION, '5.3', '>=' ) ? fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'], $this->params['escape'] ) : fgetcsv( $handle, 0, $this->params['delimiter'], $this->params['enclosure'] ); // @codingStandardsIgnoreLine + + if ( false !== $row ) { + $this->raw_data[] = $row; + $this->file_positions[ count( $this->raw_data ) ] = ftell( $handle ); + + if ( ( $this->params['end_pos'] > 0 && ftell( $handle ) >= $this->params['end_pos'] ) || 0 === --$this->params['lines'] ) { + break; + } + } else { + break; + } + } + + $this->file_position = ftell( $handle ); + } + + if ( ! empty( $this->params['mapping'] ) ) { + $this->set_mapped_keys(); + } + + if ( $this->params['parse'] ) { + $this->set_parsed_data(); + } + } + + /** + * Remove UTF-8 BOM signature. + * + * @param string $string String to handle. + * + * @return string + */ + protected function remove_utf8_bom( $string ) { + if ( 'efbbbf' === substr( bin2hex( $string ), 0, 6 ) ) { + $string = substr( $string, 3 ); + } + + return $string; + } + + /** + * Set file mapped keys. + */ + protected function set_mapped_keys() { + $mapping = $this->params['mapping']; + + foreach ( $this->raw_keys as $key ) { + $this->mapped_keys[] = isset( $mapping[ $key ] ) ? $mapping[ $key ] : $key; + } + } + + /** + * Parse relative field and return product ID. + * + * Handles `id:xx` and SKUs. + * + * If mapping to an id: and the product ID does not exist, this link is not + * valid. + * + * If mapping to a SKU and the product ID does not exist, a temporary object + * will be created so it can be updated later. + * + * @param string $value Field value. + * + * @return int|string + */ + public function parse_relative_field( $value ) { + global $wpdb; + + if ( empty( $value ) ) { + return ''; + } + + // IDs are prefixed with id:. + if ( preg_match( '/^id:(\d+)$/', $value, $matches ) ) { + $id = intval( $matches[1] ); + + // If original_id is found, use that instead of the given ID since a new placeholder must have been created already. + $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok. + + if ( $original_id ) { + return absint( $original_id ); + } + + // See if the given ID maps to a valid product allready. + $existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type IN ( 'product', 'product_variation' ) AND ID = %d;", $id ) ); // WPCS: db call ok, cache ok. + + if ( $existing_id ) { + return absint( $existing_id ); + } + + // If we're not updating existing posts, we may need a placeholder product to map to. + if ( ! $this->params['update_existing'] ) { + $product = wc_get_product_object( 'simple' ); + $product->set_name( 'Import placeholder for ' . $id ); + $product->set_status( 'importing' ); + $product->add_meta_data( '_original_id', $id, true ); + $id = $product->save(); + } + + return $id; + } + + $id = wc_get_product_id_by_sku( $value ); + + if ( $id ) { + return $id; + } + + try { + $product = wc_get_product_object( 'simple' ); + $product->set_name( 'Import placeholder for ' . $value ); + $product->set_status( 'importing' ); + $product->set_sku( $value ); + $id = $product->save(); + + if ( $id && ! is_wp_error( $id ) ) { + return $id; + } + } catch ( Exception $e ) { + return ''; + } + + return ''; + } + + /** + * Parse the ID field. + * + * If we're not doing an update, create a placeholder product so mapping works + * for rows following this one. + * + * @param string $value Field value. + * + * @return int + */ + public function parse_id_field( $value ) { + global $wpdb; + + $id = absint( $value ); + + if ( ! $id ) { + return 0; + } + + // See if this maps to an ID placeholder already. + $original_id = $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_original_id' AND meta_value = %s;", $id ) ); // WPCS: db call ok, cache ok. + + if ( $original_id ) { + return absint( $original_id ); + } + + // Not updating? Make sure we have a new placeholder for this ID. + if ( ! $this->params['update_existing'] ) { + $mapped_keys = $this->get_mapped_keys(); + $sku_column_index = absint( array_search( 'sku', $mapped_keys, true ) ); + $row_sku = isset( $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] ) ? $this->raw_data[ $this->parsing_raw_data_index ][ $sku_column_index ] : ''; + $id_from_sku = $row_sku ? wc_get_product_id_by_sku( $row_sku ) : ''; + + // If row has a SKU, make sure placeholder was not made already. + if ( $id_from_sku ) { + return $id_from_sku; + } + + $product = wc_get_product_object( 'simple' ); + $product->set_name( 'Import placeholder for ' . $id ); + $product->set_status( 'importing' ); + $product->add_meta_data( '_original_id', $id, true ); + + // If row has a SKU, make sure placeholder has it too. + if ( $row_sku ) { + $product->set_sku( $row_sku ); + } + $id = $product->save(); + } + + return $id && ! is_wp_error( $id ) ? $id : 0; + } + + /** + * Parse relative comma-delineated field and return product ID. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_relative_comma_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + return array_filter( array_map( array( $this, 'parse_relative_field' ), $this->explode_values( $value ) ) ); + } + + /** + * Parse a comma-delineated field from a CSV. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_comma_field( $value ) { + if ( empty( $value ) && '0' !== $value ) { + return array(); + } + + $value = $this->unescape_data( $value ); + return array_map( 'wc_clean', $this->explode_values( $value ) ); + } + + /** + * Parse a field that is generally '1' or '0' but can be something else. + * + * @param string $value Field value. + * + * @return bool|string + */ + public function parse_bool_field( $value ) { + if ( '0' === $value ) { + return false; + } + + if ( '1' === $value ) { + return true; + } + + // Don't return explicit true or false for empty fields or values like 'notify'. + return wc_clean( $value ); + } + + /** + * Parse a float value field. + * + * @param string $value Field value. + * + * @return float|string + */ + public function parse_float_field( $value ) { + if ( '' === $value ) { + return $value; + } + + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + return floatval( $value ); + } + + /** + * Parse the stock qty field. + * + * @param string $value Field value. + * + * @return float|string + */ + public function parse_stock_quantity_field( $value ) { + if ( '' === $value ) { + return $value; + } + + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + return wc_stock_amount( $value ); + } + + /** + * Parse the tax status field. + * + * @param string $value Field value. + * + * @return string + */ + public function parse_tax_status_field( $value ) { + if ( '' === $value ) { + return $value; + } + + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) { + $value = wc_string_to_bool( $value ) ? 'taxable' : 'none'; + } + + return wc_clean( $value ); + } + + /** + * Parse a category field from a CSV. + * Categories are separated by commas and subcategories are "parent > subcategory". + * + * @param string $value Field value. + * + * @return array of arrays with "parent" and "name" keys. + */ + public function parse_categories_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + $row_terms = $this->explode_values( $value ); + $categories = array(); + + foreach ( $row_terms as $row_term ) { + $parent = null; + $_terms = array_map( 'trim', explode( '>', $row_term ) ); + $total = count( $_terms ); + + foreach ( $_terms as $index => $_term ) { + // Don't allow users without capabilities to create new categories. + if ( ! current_user_can( 'manage_product_terms' ) ) { + break; + } + + $term = wp_insert_term( $_term, 'product_cat', array( 'parent' => intval( $parent ) ) ); + + if ( is_wp_error( $term ) ) { + if ( $term->get_error_code() === 'term_exists' ) { + // When term exists, error data should contain existing term id. + $term_id = $term->get_error_data(); + } else { + break; // We cannot continue on any other error. + } + } else { + // New term. + $term_id = $term['term_id']; + } + + // Only requires assign the last category. + if ( ( 1 + $index ) === $total ) { + $categories[] = $term_id; + } else { + // Store parent to be able to insert or query categories based in parent ID. + $parent = $term_id; + } + } + } + + return $categories; + } + + /** + * Parse a tag field from a CSV. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_tags_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + $value = $this->unescape_data( $value ); + $names = $this->explode_values( $value ); + $tags = array(); + + foreach ( $names as $name ) { + $term = get_term_by( 'name', $name, 'product_tag' ); + + if ( ! $term || is_wp_error( $term ) ) { + $term = (object) wp_insert_term( $name, 'product_tag' ); + } + + if ( ! is_wp_error( $term ) ) { + $tags[] = $term->term_id; + } + } + + return $tags; + } + + /** + * Parse a tag field from a CSV with space separators. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_tags_spaces_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + $value = $this->unescape_data( $value ); + $names = $this->explode_values( $value, ' ' ); + $tags = array(); + + foreach ( $names as $name ) { + $term = get_term_by( 'name', $name, 'product_tag' ); + + if ( ! $term || is_wp_error( $term ) ) { + $term = (object) wp_insert_term( $name, 'product_tag' ); + } + + if ( ! is_wp_error( $term ) ) { + $tags[] = $term->term_id; + } + } + + return $tags; + } + + /** + * Parse a shipping class field from a CSV. + * + * @param string $value Field value. + * + * @return int + */ + public function parse_shipping_class_field( $value ) { + if ( empty( $value ) ) { + return 0; + } + + $term = get_term_by( 'name', $value, 'product_shipping_class' ); + + if ( ! $term || is_wp_error( $term ) ) { + $term = (object) wp_insert_term( $value, 'product_shipping_class' ); + } + + if ( is_wp_error( $term ) ) { + return 0; + } + + return $term->term_id; + } + + /** + * Parse images list from a CSV. Images can be filenames or URLs. + * + * @param string $value Field value. + * + * @return array + */ + public function parse_images_field( $value ) { + if ( empty( $value ) ) { + return array(); + } + + $images = array(); + $separator = apply_filters( 'woocommerce_product_import_image_separator', ',' ); + + foreach ( $this->explode_values( $value, $separator ) as $image ) { + if ( stristr( $image, '://' ) ) { + $images[] = esc_url_raw( $image ); + } else { + $images[] = sanitize_file_name( $image ); + } + } + + return $images; + } + + /** + * Parse dates from a CSV. + * Dates requires the format YYYY-MM-DD and time is optional. + * + * @param string $value Field value. + * + * @return string|null + */ + public function parse_date_field( $value ) { + if ( empty( $value ) ) { + return null; + } + + if ( preg_match( '/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])([ 01-9:]*)$/', $value ) ) { + // Don't include the time if the field had time in it. + return current( explode( ' ', $value ) ); + } + + return null; + } + + /** + * Parse backorders from a CSV. + * + * @param string $value Field value. + * + * @return string + */ + public function parse_backorders_field( $value ) { + if ( empty( $value ) ) { + return 'no'; + } + + $value = $this->parse_bool_field( $value ); + + if ( 'notify' === $value ) { + return 'notify'; + } elseif ( is_bool( $value ) ) { + return $value ? 'yes' : 'no'; + } + + return 'no'; + } + + /** + * Just skip current field. + * + * By default is applied wc_clean() to all not listed fields + * in self::get_formatting_callback(), use this method to skip any formatting. + * + * @param string $value Field value. + * + * @return string + */ + public function parse_skip_field( $value ) { + return $value; + } + + /** + * Parse download file urls, we should allow shortcodes here. + * + * Allow shortcodes if present, othersiwe esc_url the value. + * + * @param string $value Field value. + * + * @return string + */ + public function parse_download_file_field( $value ) { + // Absolute file paths. + if ( 0 === strpos( $value, 'http' ) ) { + return esc_url_raw( $value ); + } + // Relative and shortcode paths. + return wc_clean( $value ); + } + + /** + * Parse an int value field + * + * @param int $value field value. + * + * @return int + */ + public function parse_int_field( $value ) { + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + return intval( $value ); + } + + /** + * Parse a description value field + * + * @param string $description field value. + * + * @return string + */ + public function parse_description_field( $description ) { + $parts = explode( "\\\\n", $description ); + foreach ( $parts as $key => $part ) { + $parts[ $key ] = str_replace( '\n', "\n", $part ); + } + + return implode( '\\\n', $parts ); + } + + /** + * Parse the published field. 1 is published, 0 is private, -1 is draft. + * Alternatively, 'true' can be used for published and 'false' for draft. + * + * @param string $value Field value. + * + * @return float|string + */ + public function parse_published_field( $value ) { + if ( '' === $value ) { + return $value; + } + + // Remove the ' prepended to fields that start with - if needed. + $value = $this->unescape_data( $value ); + + if ( 'true' === strtolower( $value ) || 'false' === strtolower( $value ) ) { + return wc_string_to_bool( $value ) ? 1 : -1; + } + + return floatval( $value ); + } + + /** + * Deprecated get formatting callback method. + * + * @deprecated 4.3.0 + * @return array + */ + protected function get_formating_callback() { + return $this->get_formatting_callback(); + } + + /** + * Get formatting callback. + * + * @since 4.3.0 + * @return array + */ + protected function get_formatting_callback() { + + /** + * Columns not mentioned here will get parsed with 'wc_clean'. + * column_name => callback. + */ + $data_formatting = array( + 'id' => array( $this, 'parse_id_field' ), + 'type' => array( $this, 'parse_comma_field' ), + 'published' => array( $this, 'parse_published_field' ), + 'featured' => array( $this, 'parse_bool_field' ), + 'date_on_sale_from' => array( $this, 'parse_date_field' ), + 'date_on_sale_to' => array( $this, 'parse_date_field' ), + 'name' => array( $this, 'parse_skip_field' ), + 'short_description' => array( $this, 'parse_description_field' ), + 'description' => array( $this, 'parse_description_field' ), + 'manage_stock' => array( $this, 'parse_bool_field' ), + 'low_stock_amount' => array( $this, 'parse_stock_quantity_field' ), + 'backorders' => array( $this, 'parse_backorders_field' ), + 'stock_status' => array( $this, 'parse_bool_field' ), + 'sold_individually' => array( $this, 'parse_bool_field' ), + 'width' => array( $this, 'parse_float_field' ), + 'length' => array( $this, 'parse_float_field' ), + 'height' => array( $this, 'parse_float_field' ), + 'weight' => array( $this, 'parse_float_field' ), + 'reviews_allowed' => array( $this, 'parse_bool_field' ), + 'purchase_note' => 'wp_filter_post_kses', + 'price' => 'wc_format_decimal', + 'regular_price' => 'wc_format_decimal', + 'stock_quantity' => array( $this, 'parse_stock_quantity_field' ), + 'category_ids' => array( $this, 'parse_categories_field' ), + 'tag_ids' => array( $this, 'parse_tags_field' ), + 'tag_ids_spaces' => array( $this, 'parse_tags_spaces_field' ), + 'shipping_class_id' => array( $this, 'parse_shipping_class_field' ), + 'images' => array( $this, 'parse_images_field' ), + 'parent_id' => array( $this, 'parse_relative_field' ), + 'grouped_products' => array( $this, 'parse_relative_comma_field' ), + 'upsell_ids' => array( $this, 'parse_relative_comma_field' ), + 'cross_sell_ids' => array( $this, 'parse_relative_comma_field' ), + 'download_limit' => array( $this, 'parse_int_field' ), + 'download_expiry' => array( $this, 'parse_int_field' ), + 'product_url' => 'esc_url_raw', + 'menu_order' => 'intval', + 'tax_status' => array( $this, 'parse_tax_status_field' ), + ); + + /** + * Match special column names. + */ + $regex_match_data_formatting = array( + '/attributes:value*/' => array( $this, 'parse_comma_field' ), + '/attributes:visible*/' => array( $this, 'parse_bool_field' ), + '/attributes:taxonomy*/' => array( $this, 'parse_bool_field' ), + '/downloads:url*/' => array( $this, 'parse_download_file_field' ), + '/meta:*/' => 'wp_kses_post', // Allow some HTML in meta fields. + ); + + $callbacks = array(); + + // Figure out the parse function for each column. + foreach ( $this->get_mapped_keys() as $index => $heading ) { + $callback = 'wc_clean'; + + if ( isset( $data_formatting[ $heading ] ) ) { + $callback = $data_formatting[ $heading ]; + } else { + foreach ( $regex_match_data_formatting as $regex => $callback ) { + if ( preg_match( $regex, $heading ) ) { + $callback = $callback; + break; + } + } + } + + $callbacks[] = $callback; + } + + return apply_filters( 'woocommerce_product_importer_formatting_callbacks', $callbacks, $this ); + } + + /** + * Check if strings starts with determined word. + * + * @param string $haystack Complete sentence. + * @param string $needle Excerpt. + * + * @return bool + */ + protected function starts_with( $haystack, $needle ) { + return substr( $haystack, 0, strlen( $needle ) ) === $needle; + } + + /** + * Expand special and internal data into the correct formats for the product CRUD. + * + * @param array $data Data to import. + * + * @return array + */ + protected function expand_data( $data ) { + $data = apply_filters( 'woocommerce_product_importer_pre_expand_data', $data ); + + // Images field maps to image and gallery id fields. + if ( isset( $data['images'] ) ) { + $images = $data['images']; + $data['raw_image_id'] = array_shift( $images ); + + if ( ! empty( $images ) ) { + $data['raw_gallery_image_ids'] = $images; + } + unset( $data['images'] ); + } + + // Type, virtual and downloadable are all stored in the same column. + if ( isset( $data['type'] ) ) { + $data['type'] = array_map( 'strtolower', $data['type'] ); + $data['virtual'] = in_array( 'virtual', $data['type'], true ); + $data['downloadable'] = in_array( 'downloadable', $data['type'], true ); + + // Convert type to string. + $data['type'] = current( array_diff( $data['type'], array( 'virtual', 'downloadable' ) ) ); + + if ( ! $data['type'] ) { + $data['type'] = 'simple'; + } + } + + // Status is mapped from a special published field. + if ( isset( $data['published'] ) ) { + $statuses = array( + -1 => 'draft', + 0 => 'private', + 1 => 'publish', + ); + $data['status'] = isset( $statuses[ $data['published'] ] ) ? $statuses[ $data['published'] ] : 'draft'; + + // Fix draft status of variations. + if ( isset( $data['type'] ) && 'variation' === $data['type'] && -1 === $data['published'] ) { + $data['status'] = 'publish'; + } + + unset( $data['published'] ); + } + + if ( isset( $data['stock_quantity'] ) ) { + if ( '' === $data['stock_quantity'] ) { + $data['manage_stock'] = false; + $data['stock_status'] = isset( $data['stock_status'] ) ? $data['stock_status'] : true; + } else { + $data['manage_stock'] = true; + } + } + + // Stock is bool or 'backorder'. + if ( isset( $data['stock_status'] ) ) { + if ( 'backorder' === $data['stock_status'] ) { + $data['stock_status'] = 'onbackorder'; + } else { + $data['stock_status'] = $data['stock_status'] ? 'instock' : 'outofstock'; + } + } + + // Prepare grouped products. + if ( isset( $data['grouped_products'] ) ) { + $data['children'] = $data['grouped_products']; + unset( $data['grouped_products'] ); + } + + // Tag ids. + if ( isset( $data['tag_ids_spaces'] ) ) { + $data['tag_ids'] = $data['tag_ids_spaces']; + unset( $data['tag_ids_spaces'] ); + } + + // Handle special column names which span multiple columns. + $attributes = array(); + $downloads = array(); + $meta_data = array(); + + foreach ( $data as $key => $value ) { + if ( $this->starts_with( $key, 'attributes:name' ) ) { + if ( ! empty( $value ) ) { + $attributes[ str_replace( 'attributes:name', '', $key ) ]['name'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:value' ) ) { + $attributes[ str_replace( 'attributes:value', '', $key ) ]['value'] = $value; + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:taxonomy' ) ) { + $attributes[ str_replace( 'attributes:taxonomy', '', $key ) ]['taxonomy'] = wc_string_to_bool( $value ); + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:visible' ) ) { + $attributes[ str_replace( 'attributes:visible', '', $key ) ]['visible'] = wc_string_to_bool( $value ); + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'attributes:default' ) ) { + if ( ! empty( $value ) ) { + $attributes[ str_replace( 'attributes:default', '', $key ) ]['default'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'downloads:id' ) ) { + if ( ! empty( $value ) ) { + $downloads[ str_replace( 'downloads:id', '', $key ) ]['id'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'downloads:name' ) ) { + if ( ! empty( $value ) ) { + $downloads[ str_replace( 'downloads:name', '', $key ) ]['name'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'downloads:url' ) ) { + if ( ! empty( $value ) ) { + $downloads[ str_replace( 'downloads:url', '', $key ) ]['url'] = $value; + } + unset( $data[ $key ] ); + + } elseif ( $this->starts_with( $key, 'meta:' ) ) { + $meta_data[] = array( + 'key' => str_replace( 'meta:', '', $key ), + 'value' => $value, + ); + unset( $data[ $key ] ); + } + } + + if ( ! empty( $attributes ) ) { + // Remove empty attributes and clear indexes. + foreach ( $attributes as $attribute ) { + if ( empty( $attribute['name'] ) ) { + continue; + } + + $data['raw_attributes'][] = $attribute; + } + } + + if ( ! empty( $downloads ) ) { + $data['downloads'] = array(); + + foreach ( $downloads as $key => $file ) { + if ( empty( $file['url'] ) ) { + continue; + } + + $data['downloads'][] = array( + 'download_id' => isset( $file['id'] ) ? $file['id'] : null, + 'name' => $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['url'] ), + 'file' => $file['url'], + ); + } + } + + if ( ! empty( $meta_data ) ) { + $data['meta_data'] = $meta_data; + } + + return $data; + } + + /** + * Map and format raw data to known fields. + */ + protected function set_parsed_data() { + $parse_functions = $this->get_formatting_callback(); + $mapped_keys = $this->get_mapped_keys(); + $use_mb = function_exists( 'mb_convert_encoding' ); + + // Parse the data. + foreach ( $this->raw_data as $row_index => $row ) { + // Skip empty rows. + if ( ! count( array_filter( $row ) ) ) { + continue; + } + + $this->parsing_raw_data_index = $row_index; + + $data = array(); + + do_action( 'woocommerce_product_importer_before_set_parsed_data', $row, $mapped_keys ); + + foreach ( $row as $id => $value ) { + // Skip ignored columns. + if ( empty( $mapped_keys[ $id ] ) ) { + continue; + } + + // Convert UTF8. + if ( $use_mb ) { + $encoding = mb_detect_encoding( $value, mb_detect_order(), true ); + if ( $encoding ) { + $value = mb_convert_encoding( $value, 'UTF-8', $encoding ); + } else { + $value = mb_convert_encoding( $value, 'UTF-8', 'UTF-8' ); + } + } else { + $value = wp_check_invalid_utf8( $value, true ); + } + + $data[ $mapped_keys[ $id ] ] = call_user_func( $parse_functions[ $id ], $value ); + } + + /** + * Filter product importer parsed data. + * + * @param array $parsed_data Parsed data. + * @param WC_Product_Importer $importer Importer instance. + */ + $this->parsed_data[] = apply_filters( 'woocommerce_product_importer_parsed_data', $this->expand_data( $data ), $this ); + } + } + + /** + * Get a string to identify the row from parsed data. + * + * @param array $parsed_data Parsed data. + * + * @return string + */ + protected function get_row_id( $parsed_data ) { + $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; + $sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : ''; + $name = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : ''; + $row_data = array(); + + if ( $name ) { + $row_data[] = $name; + } + if ( $id ) { + /* translators: %d: product ID */ + $row_data[] = sprintf( __( 'ID %d', 'woocommerce' ), $id ); + } + if ( $sku ) { + /* translators: %s: product SKU */ + $row_data[] = sprintf( __( 'SKU %s', 'woocommerce' ), $sku ); + } + + return implode( ', ', $row_data ); + } + + /** + * Process importer. + * + * Do not import products with IDs or SKUs that already exist if option + * update existing is false, and likewise, if updating products, do not + * process rows which do not exist if an ID/SKU is provided. + * + * @return array + */ + public function import() { + $this->start_time = time(); + $index = 0; + $update_existing = $this->params['update_existing']; + $data = array( + 'imported' => array(), + 'failed' => array(), + 'updated' => array(), + 'skipped' => array(), + ); + + foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) { + do_action( 'woocommerce_product_import_before_import', $parsed_data ); + + $id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0; + $sku = isset( $parsed_data['sku'] ) ? $parsed_data['sku'] : ''; + $id_exists = false; + $sku_exists = false; + + if ( $id ) { + $product = wc_get_product( $id ); + $id_exists = $product && 'importing' !== $product->get_status(); + } + + if ( $sku ) { + $id_from_sku = wc_get_product_id_by_sku( $sku ); + $product = $id_from_sku ? wc_get_product( $id_from_sku ) : false; + $sku_exists = $product && 'importing' !== $product->get_status(); + } + + if ( $id_exists && ! $update_existing ) { + $data['skipped'][] = new WP_Error( + 'woocommerce_product_importer_error', + esc_html__( 'A product with this ID already exists.', 'woocommerce' ), + array( + 'id' => $id, + 'row' => $this->get_row_id( $parsed_data ), + ) + ); + continue; + } + + if ( $sku_exists && ! $update_existing ) { + $data['skipped'][] = new WP_Error( + 'woocommerce_product_importer_error', + esc_html__( 'A product with this SKU already exists.', 'woocommerce' ), + array( + 'sku' => esc_attr( $sku ), + 'row' => $this->get_row_id( $parsed_data ), + ) + ); + continue; + } + + if ( $update_existing && ( isset( $parsed_data['id'] ) || isset( $parsed_data['sku'] ) ) && ! $id_exists && ! $sku_exists ) { + $data['skipped'][] = new WP_Error( + 'woocommerce_product_importer_error', + esc_html__( 'No matching product exists to update.', 'woocommerce' ), + array( + 'id' => $id, + 'sku' => esc_attr( $sku ), + 'row' => $this->get_row_id( $parsed_data ), + ) + ); + continue; + } + + $result = $this->process_item( $parsed_data ); + + if ( is_wp_error( $result ) ) { + $result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) ); + $data['failed'][] = $result; + } elseif ( $result['updated'] ) { + $data['updated'][] = $result['id']; + } else { + $data['imported'][] = $result['id']; + } + + $index ++; + + if ( $this->params['prevent_timeouts'] && ( $this->time_exceeded() || $this->memory_exceeded() ) ) { + $this->file_position = $this->file_positions[ $index ]; + break; + } + } + + return $data; + } +} diff --git a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php new file mode 100644 index 0000000..8a78efb --- /dev/null +++ b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php @@ -0,0 +1,172 @@ +database_prefix = $database_prefix; + } + + /** + * Fetches the path that the database should be stored. + * + * @return string The local database path. + */ + public function get_database_path() { + $uploads_dir = wp_upload_dir(); + + $database_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/'; + if ( ! empty( $this->database_prefix ) ) { + $database_path .= $this->database_prefix . '-'; + } + $database_path .= self::DATABASE . self::DATABASE_EXTENSION; + + /** + * Filter the geolocation database storage path. + * + * @param string $database_path The path to the database. + * @param int $version Deprecated since 3.4.0. + * @deprecated 3.9.0 + */ + $database_path = apply_filters_deprecated( + 'woocommerce_geolocation_local_database_path', + array( $database_path, 2 ), + '3.9.0', + 'woocommerce_maxmind_geolocation_database_path' + ); + + /** + * Filter the geolocation database storage path. + * + * @since 3.9.0 + * @param string $database_path The path to the database. + */ + return apply_filters( 'woocommerce_maxmind_geolocation_database_path', $database_path ); + } + + /** + * Fetches the database from the MaxMind service. + * + * @param string $license_key The license key to be used when downloading the database. + * @return string|WP_Error The path to the database file or an error if invalid. + */ + public function download_database( $license_key ) { + $download_uri = add_query_arg( + array( + 'edition_id' => self::DATABASE, + 'license_key' => urlencode( wc_clean( $license_key ) ), + 'suffix' => 'tar.gz', + ), + 'https://download.maxmind.com/app/geoip_download' + ); + + // Needed for the download_url call right below. + require_once ABSPATH . 'wp-admin/includes/file.php'; + + $tmp_archive_path = download_url( esc_url_raw( $download_uri ) ); + if ( is_wp_error( $tmp_archive_path ) ) { + // Transform the error into something more informative. + $error_data = $tmp_archive_path->get_error_data(); + if ( isset( $error_data['code'] ) ) { + switch ( $error_data['code'] ) { + case 401: + return new WP_Error( + 'woocommerce_maxmind_geolocation_database_license_key', + __( 'The MaxMind license key is invalid. If you have recently created this key, you may need to wait for it to become active.', 'woocommerce' ) + ); + } + } + + return new WP_Error( 'woocommerce_maxmind_geolocation_database_download', __( 'Failed to download the MaxMind database.', 'woocommerce' ) ); + } + + // Extract the database from the archive. + try { + $file = new PharData( $tmp_archive_path ); + + $tmp_database_path = trailingslashit( dirname( $tmp_archive_path ) ) . trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION; + + $file->extractTo( + dirname( $tmp_archive_path ), + trailingslashit( $file->current()->getFilename() ) . self::DATABASE . self::DATABASE_EXTENSION, + true + ); + } catch ( Exception $exception ) { + return new WP_Error( 'woocommerce_maxmind_geolocation_database_archive', $exception->getMessage() ); + } finally { + // Remove the archive since we only care about a single file in it. + unlink( $tmp_archive_path ); + } + + return $tmp_database_path; + } + + /** + * Fetches the ISO country code associated with an IP address. + * + * @param string $ip_address The IP address to find the country code for. + * @return string The country code for the IP address, or empty if not found. + */ + public function get_iso_country_code_for_ip( $ip_address ) { + $country_code = ''; + + if ( ! class_exists( 'MaxMind\Db\Reader' ) ) { + wc_get_logger()->notice( __( 'Missing MaxMind Reader library!', 'woocommerce' ), array( 'source' => 'maxmind-geolocation' ) ); + return $country_code; + } + + $database_path = $this->get_database_path(); + if ( ! file_exists( $database_path ) ) { + return $country_code; + } + + try { + $reader = new MaxMind\Db\Reader( $database_path ); + $data = $reader->get( $ip_address ); + + if ( isset( $data['country']['iso_code'] ) ) { + $country_code = $data['country']['iso_code']; + } + + $reader->close(); + } catch ( Exception $e ) { + wc_get_logger()->notice( $e->getMessage(), array( 'source' => 'maxmind-geolocation' ) ); + } + + return $country_code; + } +} diff --git a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php new file mode 100644 index 0000000..43c3c26 --- /dev/null +++ b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php @@ -0,0 +1,292 @@ +id = 'maxmind_geolocation'; + $this->method_title = __( 'MaxMind Geolocation', 'woocommerce' ); + $this->method_description = __( 'An integration for utilizing MaxMind to do Geolocation lookups. Please note that this integration will only do country lookups.', 'woocommerce' ); + + /** + * Supports overriding the database service to be used. + * + * @since 3.9.0 + * @return mixed|null The geolocation database service. + */ + $this->database_service = apply_filters( 'woocommerce_maxmind_geolocation_database_service', null ); + if ( null === $this->database_service ) { + $this->database_service = new WC_Integration_MaxMind_Database_Service( $this->get_database_prefix() ); + } + + $this->init_form_fields(); + $this->init_settings(); + + // Bind to the save action for the settings. + add_action( 'woocommerce_update_options_integration_' . $this->id, array( $this, 'process_admin_options' ) ); + + // Trigger notice if license key is missing. + add_action( 'update_option_woocommerce_default_customer_address', array( $this, 'display_missing_license_key_notice' ), 1000, 2 ); + + /** + * Allows for the automatic database update to be disabled. + * + * @deprecated 3.9.0 + * @return bool Whether or not the database should be updated periodically. + */ + $bind_updater = apply_filters_deprecated( + 'woocommerce_geolocation_update_database_periodically', + array( true ), + '3.9.0', + 'woocommerce_maxmind_geolocation_update_database_periodically' + ); + + /** + * Allows for the automatic database update to be disabled. + * Note that MaxMind's TOS requires that the databases be updated or removed periodically. + * + * @since 3.9.0 + * @param bool $bind_updater Whether or not the database should be updated periodically. + */ + $bind_updater = apply_filters( 'woocommerce_maxmind_geolocation_update_database_periodically', $bind_updater ); + + // Bind to the scheduled updater action. + if ( $bind_updater ) { + add_action( 'woocommerce_geoip_updater', array( $this, 'update_database' ) ); + } + + // Bind to the geolocation filter for MaxMind database lookups. + add_filter( 'woocommerce_get_geolocation', array( $this, 'get_geolocation' ), 10, 2 ); + } + + /** + * Override the normal options so we can print the database file path to the admin, + */ + public function admin_options() { + parent::admin_options(); + + include dirname( __FILE__ ) . '/views/html-admin-options.php'; + } + + /** + * Initializes the settings fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'license_key' => array( + 'title' => __( 'MaxMind License Key', 'woocommerce' ), + 'type' => 'password', + 'description' => sprintf( + /* translators: %1$s: Documentation URL */ + __( + 'The key that will be used when dealing with MaxMind Geolocation services. You can read how to generate one in MaxMind Geolocation Integration documentation.', + 'woocommerce' + ), + 'https://docs.woocommerce.com/document/maxmind-geolocation-integration/' + ), + 'desc_tip' => false, + 'default' => '', + ), + ); + } + + /** + * Get database service. + * + * @return WC_Integration_MaxMind_Database_Service|null + */ + public function get_database_service() { + return $this->database_service; + } + + /** + * Checks to make sure that the license key is valid. + * + * @param string $key The key of the field. + * @param mixed $value The value of the field. + * @return mixed + * @throws Exception When the license key is invalid. + */ + public function validate_license_key_field( $key, $value ) { + // Trim whitespaces and strip slashes. + $value = $this->validate_password_field( $key, $value ); + + // Empty license keys have no need test downloading a database. + if ( empty( $value ) ) { + return $value; + } + + // Check the license key by attempting to download the Geolocation database. + $tmp_database_path = $this->database_service->download_database( $value ); + if ( is_wp_error( $tmp_database_path ) ) { + WC_Admin_Settings::add_error( $tmp_database_path->get_error_message() ); + + // Throw an exception to keep from changing this value. This will prevent + // users from accidentally losing their license key, which cannot + // be viewed again after generating. + throw new Exception( $tmp_database_path->get_error_message() ); + } + + // We may as well put this archive to good use, now that we've downloaded one. + self::update_database( $tmp_database_path ); + + // Remove missing license key notice. + $this->remove_missing_license_key_notice(); + + return $value; + } + + /** + * Updates the database used for geolocation queries. + * + * @param string|null $new_database_path The path to the new database file. Null will fetch a new archive. + */ + public function update_database( $new_database_path = null ) { + // Allow us to easily interact with the filesystem. + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + global $wp_filesystem; + + // Remove any existing archives to comply with the MaxMind TOS. + $target_database_path = $this->database_service->get_database_path(); + + // If there's no database path, we can't store the database. + if ( empty( $target_database_path ) ) { + return; + } + + if ( $wp_filesystem->exists( $target_database_path ) ) { + $wp_filesystem->delete( $target_database_path ); + } + + if ( isset( $new_database_path ) ) { + $tmp_database_path = $new_database_path; + } else { + // We can't download a database if there's no license key configured. + $license_key = $this->get_option( 'license_key' ); + if ( empty( $license_key ) ) { + return; + } + + $tmp_database_path = $this->database_service->download_database( $license_key ); + if ( is_wp_error( $tmp_database_path ) ) { + wc_get_logger()->notice( $tmp_database_path->get_error_message(), array( 'source' => 'maxmind-geolocation' ) ); + return; + } + } + + // Move the new database into position. + $wp_filesystem->move( $tmp_database_path, $target_database_path, true ); + $wp_filesystem->delete( dirname( $tmp_database_path ) ); + } + + /** + * Performs a geolocation lookup against the MaxMind database for the given IP address. + * + * @param array $data Geolocation data. + * @param string $ip_address The IP address to geolocate. + * @return array Geolocation including country code, state, city and postcode based on an IP address. + */ + public function get_geolocation( $data, $ip_address ) { + // WooCommerce look for headers first, and at this moment could be just enough. + if ( ! empty( $data['country'] ) ) { + return $data; + } + + if ( empty( $ip_address ) ) { + return $data; + } + + $country_code = $this->database_service->get_iso_country_code_for_ip( $ip_address ); + + return array( + 'country' => $country_code, + 'state' => '', + 'city' => '', + 'postcode' => '', + ); + } + + /** + * Fetches the prefix for the MaxMind database file. + * + * @return string + */ + private function get_database_prefix() { + $prefix = $this->get_option( 'database_prefix' ); + if ( empty( $prefix ) ) { + $prefix = wp_generate_password( 32, false ); + $this->update_option( 'database_prefix', $prefix ); + } + + return $prefix; + } + + /** + * Add missing license key notice. + */ + private function add_missing_license_key_notice() { + if ( ! class_exists( 'WC_Admin_Notices' ) ) { + include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php'; + } + WC_Admin_Notices::add_notice( 'maxmind_license_key' ); + } + + /** + * Remove missing license key notice. + */ + private function remove_missing_license_key_notice() { + if ( ! class_exists( 'WC_Admin_Notices' ) ) { + include_once WC_ABSPATH . 'includes/admin/class-wc-admin-notices.php'; + } + WC_Admin_Notices::remove_notice( 'maxmind_license_key' ); + } + + /** + * Display notice if license key is missing. + * + * @param mixed $old_value Option old value. + * @param mixed $new_value Current value. + */ + public function display_missing_license_key_notice( $old_value, $new_value ) { + if ( ! apply_filters( 'woocommerce_maxmind_geolocation_display_notices', true ) ) { + return; + } + + if ( ! in_array( $new_value, array( 'geolocation', 'geolocation_ajax' ), true ) ) { + $this->remove_missing_license_key_notice(); + return; + } + + $license_key = $this->get_option( 'license_key' ); + if ( ! empty( $license_key ) ) { + return; + } + + $this->add_missing_license_key_notice(); + } +} diff --git a/includes/integrations/maxmind-geolocation/views/html-admin-options.php b/includes/integrations/maxmind-geolocation/views/html-admin-options.php new file mode 100644 index 0000000..a027e00 --- /dev/null +++ b/includes/integrations/maxmind-geolocation/views/html-admin-options.php @@ -0,0 +1,25 @@ + + + + + + + +
    + + +
    + + +

    +
    +
    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 0000000..6e2ca1f --- /dev/null +++ b/includes/interfaces/class-wc-abstract-order-data-store-interface.php @@ -0,0 +1,50 @@ + [], 'failed' => []] + * + * @return array + */ + public function import(); + + /** + * Get file raw keys. + * + * CSV - Headers. + * XML - Element names. + * JSON - Keys + * + * @return array + */ + public function get_raw_keys(); + + /** + * Get file mapped headers. + * + * @return array + */ + public function get_mapped_keys(); + + /** + * Get raw data. + * + * @return array + */ + public function get_raw_data(); + + /** + * Get parsed data. + * + * @return array + */ + public function get_parsed_data(); + + /** + * Get file pointer position from the last read. + * + * @return int + */ + public function get_file_position(); + + /** + * Get file pointer position as a percentage of file size. + * + * @return int + */ + public function get_percent_complete(); +} diff --git a/includes/interfaces/class-wc-log-handler-interface.php b/includes/interfaces/class-wc-log-handler-interface.php new file mode 100644 index 0000000..f838281 --- /dev/null +++ b/includes/interfaces/class-wc-log-handler-interface.php @@ -0,0 +1,29 @@ +id). + * @return array + */ + public function delete_meta( &$data, $meta ); + + /** + * Add new piece of meta. + * + * @param WC_Data $data Data object. + * @param object $meta Meta object (containing ->key and ->value). + * @return int meta ID + */ + public function add_meta( &$data, $meta ); + + /** + * Update meta. + * + * @param WC_Data $data Data object. + * @param object $meta Meta object (containing ->id, ->key and ->value). + */ + public function update_meta( &$data, $meta ); +} diff --git a/includes/interfaces/class-wc-order-data-store-interface.php b/includes/interfaces/class-wc-order-data-store-interface.php new file mode 100644 index 0000000..1874517 --- /dev/null +++ b/includes/interfaces/class-wc-order-data-store-interface.php @@ -0,0 +1,137 @@ +get_id() will be set. + * + * @param WC_Order_Item $item Item object. + */ + public function save_item_data( &$item ); +} diff --git a/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 0000000..1167e83 --- /dev/null +++ b/includes/interfaces/class-wc-order-refund-data-store-interface.php @@ -0,0 +1,17 @@ +id, $return[0]->parent_id. + * + * @return array + */ + public function get_on_sale_products(); + + /** + * Returns a list of product IDs ( id as key => parent as value) that are + * featured. Uses get_posts instead of wc_get_products since we want + * some extra meta queries and ALL products (posts_per_page = -1). + * + * @return array + */ + public function get_featured_product_ids(); + + /** + * Check if product sku is found for any other product IDs. + * + * @param int $product_id Product ID. + * @param string $sku SKU. + * @return bool + */ + public function is_existing_sku( $product_id, $sku ); + + /** + * Return product ID based on SKU. + * + * @param string $sku SKU. + * @return int + */ + public function get_product_id_by_sku( $sku ); + + /** + * Returns an array of IDs of products that have sales starting soon. + * + * @return array + */ + public function get_starting_sales(); + + /** + * Returns an array of IDs of products that have sales which are due to end. + * + * @return array + */ + public function get_ending_sales(); + + /** + * Find a matching (enabled) variation within a variable product. + * + * @param WC_Product $product Variable product object. + * @param array $match_attributes Array of attributes we want to try to match. + * @return int Matching variation ID or 0. + */ + public function find_matching_product_variation( $product, $match_attributes = array() ); + + /** + * Make sure all variations have a sort order set so they can be reordered correctly. + * + * @param int $parent_id Parent ID. + */ + public function sort_all_product_variations( $parent_id ); + + /** + * Return a list of related products (using data like categories and IDs). + * + * @param array $cats_array List of categories IDs. + * @param array $tags_array List of tags IDs. + * @param array $exclude_ids Excluded IDs. + * @param int $limit Limit of results. + * @param int $product_id Product ID. + * @return array + */ + public function get_related_products( $cats_array, $tags_array, $exclude_ids, $limit, $product_id ); + + /** + * Update a product's stock amount directly. + * + * Uses queries rather than update_post_meta so we can do this in one query (to avoid stock issues). + * + * @param int $product_id_with_stock Product ID. + * @param int|null $stock_quantity Stock quantity to update to. + * @param string $operation Either set, increase or decrease. + */ + public function update_product_stock( $product_id_with_stock, $stock_quantity = null, $operation = 'set' ); + + /** + * Update a product's sale count directly. + * + * Uses queries rather than update_post_meta so we can do this in one query for performance. + * + * @param int $product_id Product ID. + * @param int|null $quantity Stock quantity to use for update. + * @param string $operation Either set, increase or decrease. + */ + public function update_product_sales( $product_id, $quantity = null, $operation = 'set' ); + + /** + * Get shipping class ID by slug. + * + * @param string $slug Shipping class slug. + * @return int|false + */ + public function get_shipping_class_id_by_slug( $slug ); + + /** + * Returns an array of products. + * + * @param array $args @see wc_get_products. + * @return array + */ + public function get_products( $args = array() ); + + /** + * Get the product type based on product ID. + * + * @param int $product_id Product ID. + * @return bool|string + */ + public function get_product_type( $product_id ); +} diff --git a/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 0000000..03b72b4 --- /dev/null +++ b/includes/interfaces/class-wc-product-variable-data-store-interface.php @@ -0,0 +1,79 @@ + '' - the name of the action that will be triggered. + * 'args' => null - the args array that will be passed with the action. + * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='. + * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='. + * 'group' => '' - the group the action belongs to. + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING. + * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID. + * 'per_page' => 5 - Number of results to return. + * 'offset' => 0. + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date'. + * 'order' => 'ASC'. + * @param string $return_format OBJECT, ARRAY_A, or ids. + * @return array + */ + public function search( $args = array(), $return_format = OBJECT ); +} diff --git a/includes/interfaces/class-wc-shipping-zone-data-store-interface.php b/includes/interfaces/class-wc-shipping-zone-data-store-interface.php new file mode 100644 index 0000000..031e404 --- /dev/null +++ b/includes/interfaces/class-wc-shipping-zone-data-store-interface.php @@ -0,0 +1,81 @@ +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; + } + } + } + + // Handle 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' ), $item ), $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 4.4.0 + * @param object $item + * @return WC_Product|bool + */ + public function get_product_from_item( $item ) { + wc_deprecated_function( 'WC_Abstract_Legacy_Order::get_product_from_item', '4.4.0', '$item->get_product()' ); + 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 3.0 + * + * @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[] = '' . esc_html( $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; + } + + /** + * Get coupon codes only. + * + * @deprecated 3.7.0 - Replaced with better named method to reflect the actual data being returned. + * @return array + */ + public function get_used_coupons() { + wc_deprecated_function( 'get_used_coupons', '3.7', 'WC_Abstract_Order::get_coupon_codes' ); + return $this->get_coupon_codes(); + } + + /** + * 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 0000000..ed7201a --- /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 0000000..97f370e --- /dev/null +++ b/includes/legacy/abstract-wc-legacy-product.php @@ -0,0 +1,692 @@ +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.' ); + // See notes in https://github.com/woocommerce/woocommerce/pull/22909#discussion_r262393401. + // Sync count first like in the original method https://github.com/woocommerce/woocommerce/blob/2.6.0/includes/abstracts/abstract-wc-product.php#L1101-L1128. + self::sync_rating_count( $post_id ); + $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(); + } + + /** + * @deprecated 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/api/class-wc-rest-legacy-coupons-controller.php b/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php new file mode 100644 index 0000000..dc88a4c --- /dev/null +++ b/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php @@ -0,0 +1,164 @@ +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/legacy/api/class-wc-rest-legacy-orders-controller.php b/includes/legacy/api/class-wc-rest-legacy-orders-controller.php new file mode 100644 index 0000000..d96849c --- /dev/null +++ b/includes/legacy/api/class-wc-rest-legacy-orders-controller.php @@ -0,0 +1,306 @@ + '_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; + $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); + $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 Order object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); + } + + /** + * Create base WC Order object. + * + * @deprecated 3.0.0 + * + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * 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 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + + $order = $this->prepare_item_for_database( $request ); + $order->set_created_via( 'rest-api' ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->calculate_totals(); + $order->save(); + + // Handle set paid. + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update order. + * + * @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/legacy/api/class-wc-rest-legacy-products-controller.php b/includes/legacy/api/class-wc-rest-legacy-products-controller.php new file mode 100644 index 0000000..4f7ce18 --- /dev/null +++ b/includes/legacy/api/class-wc-rest-legacy-products-controller.php @@ -0,0 +1,804 @@ + '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', + 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( $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/legacy/api/v1/class-wc-api-authentication.php b/includes/legacy/api/v1/class-wc-api-authentication.php new file mode 100644 index 0000000..b80b26b --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-authentication.php @@ -0,0 +1,410 @@ +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 parameters + * @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/legacy/api/v1/class-wc-api-coupons.php b/includes/legacy/api/v1/class-wc-api-coupons.php new file mode 100644 index 0000000..49036c6 --- /dev/null +++ b/includes/legacy/api/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/legacy/api/v1/class-wc-api-customers.php b/includes/legacy/api/v1/class-wc-api-customers.php new file mode 100644 index 0000000..1860815 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-customers.php @@ -0,0 +1,481 @@ + + * 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/legacy/api/v1/class-wc-api-json-handler.php b/includes/legacy/api/v1/class-wc-api-json-handler.php new file mode 100644 index 0000000..7a7de67 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-json-handler.php @@ -0,0 +1,74 @@ +api->server->send_status( 400 ); + return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) ); + } + + $jsonp_callback = $_GET['_jsonp']; + + if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) { + WC()->api->server->send_status( 400 ); + return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) ); + } + + WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); + + // Prepend '/**/' to mitigate possible JSONP Flash attacks. + // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ + return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')'; + } + + return wp_json_encode( $data ); + } +} diff --git a/includes/legacy/api/v1/class-wc-api-orders.php b/includes/legacy/api/v1/class-wc-api-orders.php new file mode 100644 index 0000000..d3ce4ad --- /dev/null +++ b/includes/legacy/api/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' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + } + + 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/legacy/api/v1/class-wc-api-products.php b/includes/legacy/api/v1/class-wc-api-products.php new file mode 100644 index 0000000..a303e74 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-products.php @@ -0,0 +1,548 @@ + + * 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 ); + } + + if ( ! is_a( $product, 'WC_Product' ) ) { + return array(); + } + + return array( + 'title' => $product->get_name(), + 'id' => $product->get_id(), + 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'downloadable' => $product->is_downloadable(), + 'virtual' => $product->is_virtual(), + 'permalink' => $product->get_permalink(), + 'sku' => $product->get_sku(), + 'price' => 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_', '', wc_attribute_taxonomy_slug( $attribute_name ) ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => ucwords( wc_attribute_taxonomy_slug( $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/legacy/api/v1/class-wc-api-reports.php b/includes/legacy/api/v1/class-wc-api-reports.php new file mode 100644 index 0000000..1f35428 --- /dev/null +++ b/includes/legacy/api/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/legacy/api/v1/class-wc-api-resource.php b/includes/legacy/api/v1/class-wc-api-resource.php new file mode 100644 index 0000000..6eeefcf --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-resource.php @@ -0,0 +1,409 @@ +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/legacy/api/v1/class-wc-api-server.php b/includes/legacy/api/v1/class-wc-api-server.php new file mode 100644 index 0000000..743eac2 --- /dev/null +++ b/includes/legacy/api/v1/class-wc-api-server.php @@ -0,0 +1,782 @@ + 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 + */ + 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() { + // @codingStandardsIgnoreStart + // $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; + // @codingStandardsIgnoreEnd + } + + /** + * 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/legacy/api/v1/class-wc-api-xml-handler.php b/includes/legacy/api/v1/class-wc-api-xml-handler.php new file mode 100644 index 0000000..7e8cc94 --- /dev/null +++ b/includes/legacy/api/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/legacy/api/v1/interface-wc-api-handler.php b/includes/legacy/api/v1/interface-wc-api-handler.php new file mode 100644 index 0000000..4e252f6 --- /dev/null +++ b/includes/legacy/api/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 parameters + * @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/legacy/api/v2/class-wc-api-coupons.php b/includes/legacy/api/v2/class-wc-api-coupons.php new file mode 100644 index 0000000..71165dc --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-coupons.php @@ -0,0 +1,575 @@ + + * + * @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 ); + do_action( 'woocommerce_new_coupon', $id ); + + $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 ); + do_action( 'woocommerce_update_coupon', $id ); + + 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/legacy/api/v2/class-wc-api-customers.php b/includes/legacy/api/v2/class-wc-api-customers.php new file mode 100644 index 0000000..7598025 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-customers.php @@ -0,0 +1,837 @@ + + * 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']; + } + + // Order by + 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 ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) { + 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 ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) { + 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/legacy/api/v2/class-wc-api-exception.php b/includes/legacy/api/v2/class-wc-api-exception.php new file mode 100644 index 0000000..5000f76 --- /dev/null +++ b/includes/legacy/api/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/legacy/api/v2/class-wc-api-json-handler.php b/includes/legacy/api/v2/class-wc-api-json-handler.php new file mode 100644 index 0000000..ee4d5e8 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-json-handler.php @@ -0,0 +1,73 @@ +api->server->send_status( 400 ); + return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) ); + } + + $jsonp_callback = $_GET['_jsonp']; + + if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) { + WC()->api->server->send_status( 400 ); + return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) ); + } + + WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); + + // Prepend '/**/' to mitigate possible JSONP Flash attacks. + // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ + return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')'; + } + + return wp_json_encode( $data ); + } +} diff --git a/includes/legacy/api/v2/class-wc-api-orders.php b/includes/legacy/api/v2/class-wc-api-orders.php new file mode 100644 index 0000000..cfc2d13 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-orders.php @@ -0,0 +1,1830 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET|POST /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET /orders/statuses + $routes[ $this->base . '/statuses' ] = array( + array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /orders/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_order' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), + ); + + # GET|POST /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//notes/ + $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( + array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), + ); + + # GET|POST /orders//refunds + $routes[ $this->base . '/(?P\d+)/refunds' ] = array( + array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//refunds/ + $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( + array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /orders/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all orders + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param string $status + * @param int $page + * @return array + */ + public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_orders( $filter ); + + $orders = array(); + + foreach ( $query->posts as $order_id ) { + + if ( ! $this->is_readable( $order_id ) ) { + continue; + } + + $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'orders' => $orders ); + } + + + /** + * Get the order for the given ID + * + * @since 2.1 + * @param int $id the order ID + * @param array $fields + * @param array $filter + * @return array|WP_Error + */ + public function get_order( $id, $fields = null, $filter = array() ) { + + // ensure order ID is valid & user has permission to read + $id = $this->validate_request( $id, $this->post_type, 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + // Get the decimal precession + $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); + $order = wc_get_order( $id ); + $order_data = array( + 'id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'status' => $order->get_status(), + 'currency' => $order->get_currency(), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'shipping_methods' => $order->get_shipping_method(), + 'payment_details' => array( + 'method_id' => $order->get_payment_method(), + 'method_title' => $order->get_payment_method_title(), + 'paid' => ! is_null( $order->get_date_paid() ), + ), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + 'note' => $order->get_customer_note(), + 'customer_ip' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'customer_id' => $order->get_user_id(), + 'view_order_url' => $order->get_view_order_url(), + 'line_items' => array(), + 'shipping_lines' => array(), + 'tax_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // add line items + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $order_data['line_items'][] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + ); + } + + // add shipping + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $order_data['shipping_lines'][] = array( + 'id' => $shipping_item_id, + 'method_id' => $shipping_item->get_method_id(), + 'method_title' => $shipping_item->get_name(), + 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), + ); + } + + // add taxes + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $order_data['tax_lines'][] = array( + 'id' => $tax->id, + 'rate_id' => $tax->rate_id, + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, $dp ), + 'compound' => (bool) $tax->is_compound, + ); + } + + // add fees + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $order_data['fee_lines'][] = array( + 'id' => $fee_item_id, + 'title' => $fee_item->get_name(), + 'tax_class' => $fee_item->get_tax_class(), + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + ); + } + + // add coupons + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $order_data['coupon_lines'][] = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item->get_code(), + 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), + ); + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.4 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + try { + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + + if ( 'any' === $status ) { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $filter['status'] = str_replace( 'wc-', '', $slug ); + $query = $this->query_orders( $filter ); + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; + } + + return array( 'count' => $order_statuses ); + + } else { + $filter['status'] = $status; + } + } + + $query = $this->query_orders( $filter ); + + return array( 'count' => (int) $query->found_posts ); + + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a list of valid order statuses + * + * Note this requires no specific permissions other than being an authenticated + * API user. Order statuses (particularly custom statuses) could be considered + * private information which is why it's not in the API index. + * + * @since 2.1 + * @return array + */ + public function get_order_statuses() { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; + } + + return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); + } + + /** + * Create an order + * + * @since 2.2 + * + * @param array $data raw order data + * + * @return array|WP_Error + */ + public function create_order( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); + + // default order args, note that status is checked for validity in wc_create_order() + $default_order_args = array( + 'status' => isset( $data['status'] ) ? $data['status'] : '', + 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, + ); + + // if creating order for existing customer + if ( ! empty( $data['customer_id'] ) ) { + + // make sure customer exists + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $default_order_args['customer_id'] = $data['customer_id']; + } + + // create the pending order + $order = $this->create_base_order( $default_order_args, $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // billing/shipping addresses + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $set_item = "set_{$line_type}"; + + foreach ( $data[ $line ] as $item ) { + + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // calculate totals and set them + $order->calculate_totals(); + + // payment method (and payment_complete() if `paid` == true) + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // method ID & title are required + if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + + // mark as paid if set + if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // set order currency + if ( isset( $data['currency'] ) ) { + + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // set order meta + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_new_order', $order->get_id() ); + + return $this->get_order( $order->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Creates new WC_Order. + * + * Requires a separate function for classes that extend WC_API_Orders. + * + * @since 2.3 + * + * @param $args array + * @param $data + * + * @return WC_Order + */ + protected function create_base_order( $args, $data ) { + return wc_create_order( $args ); + } + + /** + * Edit an order + * + * @since 2.2 + * + * @param int $id the order ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + $update_totals = false; + + $id = $this->validate_request( $id, $this->post_type, 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); + $order = wc_get_order( $id ); + + if ( empty( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $order_args = array( 'order_id' => $order->get_id() ); + + // Customer note. + if ( isset( $data['note'] ) ) { + $order_args['customer_note'] = $data['note']; + } + + // Customer ID. + if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); + } + + // Billing/shipping address. + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $update_totals = true; + + foreach ( $data[ $line ] as $item ) { + + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Payment method (and payment_complete() if `paid` == true and order needs payment). + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // Method ID. + if ( isset( $data['payment_details']['method_id'] ) ) { + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + } + + // Method title. + if ( isset( $data['payment_details']['method_title'] ) ) { + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + } + + // Mark as paid if set. + if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // Set order currency. + if ( isset( $data['currency'] ) ) { + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update order meta. + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $data['status'] ) ) { + // Refresh the order instance. + $order = wc_get_order( $order->get_id() ); + $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); + } + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_update_order', $order->get_id() ); + + return $this->get_order( $id ); + + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, $this->post_type, 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + wc_delete_shop_order_transients( $id ); + + do_action( 'woocommerce_api_delete_order', $id, $this ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + protected function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => $this->post_type, + 'post_status' => array_keys( wc_get_order_statuses() ), + ); + + // add status argument + if ( ! empty( $args['status'] ) ) { + + $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); + $statuses = explode( ',', $statuses ); + $query_args['post_status'] = $statuses; + + unset( $args['status'] ); + + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Helper method to set/update the billing & shipping addresses for + * an order + * + * @since 2.1 + * @param \WC_Order $order + * @param array $data + */ + protected function set_order_addresses( $order, $data ) { + + $address_fields = array( + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ); + + $billing_address = $shipping_address = array(); + + // billing address + if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['billing_address'][ $field ] ) ) { + $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); + } + } + + unset( $address_fields['email'] ); + unset( $address_fields['phone'] ); + } + + // shipping address + if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['shipping_address'][ $field ] ) ) { + $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); + } + } + } + + $this->update_address( $order, $billing_address, 'billing' ); + $this->update_address( $order, $shipping_address, 'shipping' ); + + // update user meta + if ( $order->get_user_id() ) { + foreach ( $billing_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); + } + foreach ( $shipping_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); + } + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Helper method to add/update order meta, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @since 2.2 + * @param int $order_id valid order ID + * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format + */ + protected function set_order_meta( $order_id, $order_meta ) { + + foreach ( $order_meta as $meta_key => $meta_value ) { + + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null + * + * Items can be deleted by setting the resource ID to null + * + * @since 2.2 + * @param array $item item provided in the request body + * @return bool true if the item resource ID is null, false otherwise + */ + protected function item_is_null( $item ) { + + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Wrapper method to create/update order items + * + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @since 2.2 + * @param \WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_API_Exception if item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = "set_{$item_type}"; + + // verify provided line item ID is associated with order + if ( 'update' === $action ) { + + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->get_id() ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Create or update a line item + * + * @since 2.2 + * @param \WC_Order $order + * @param array $item line item data + * @param string $action 'create' to add line item or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_line_item( $order, $item, $action ) { + $creating = ( 'create' === $action ); + + // product is always required + if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); + } + + // when updating, ensure product ID provided matches + if ( 'update' === $action ) { + + $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); + } + } + + if ( isset( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } elseif ( isset( $item['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $item['sku'] ); + } + + // variations must each have a key & value + $variation_id = 0; + if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { + foreach ( $item['variations'] as $key => $value ) { + if ( ! $key || ! $value ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); + } + } + $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); + } + + $product = wc_get_product( $variation_id ? $variation_id : $product_id ); + + // must be a valid WC_Product + if ( ! is_object( $product ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // quantity must be positive float + if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // quantity is required when creating + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + if ( $creating ) { + $line_item = new WC_Order_Item_Product(); + } else { + $line_item = new WC_Order_Item_Product( $item['id'] ); + } + + $line_item->set_product( $product ); + $line_item->set_order_id( $order->get_id() ); + + if ( isset( $item['quantity'] ) ) { + $line_item->set_quantity( $item['quantity'] ); + } + if ( isset( $item['total'] ) ) { + $line_item->set_total( floatval( $item['total'] ) ); + } elseif ( $creating ) { + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); + $line_item->set_total( $total ); + $line_item->set_subtotal( $total ); + } + if ( isset( $item['total_tax'] ) ) { + $line_item->set_total_tax( floatval( $item['total_tax'] ) ); + } + if ( isset( $item['subtotal'] ) ) { + $line_item->set_subtotal( floatval( $item['subtotal'] ) ); + } + if ( isset( $item['subtotal_tax'] ) ) { + $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); + } + if ( $variation_id ) { + $line_item->set_variation_id( $variation_id ); + $line_item->set_variation( $item['variations'] ); + } + + // Save or add to order. + if ( $creating ) { + $order->add_item( $line_item ); + } else { + $item_id = $line_item->save(); + + if ( ! $item_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Given a product ID & API provided variations, find the correct variation ID to use for calculation + * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass + * the cheapest variation ID but provide other information so we have to look up the variation ID. + * + * @param WC_Product $product + * @param array $variations + * + * @return int returns an ID if a valid variation was found for this product + */ + function get_variation_id( $product, $variations = array() ) { + $variation_id = null; + $variations_normalized = array(); + + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + if ( isset( $variations ) && is_array( $variations ) ) { + // start by normalizing the passed variations + foreach ( $variations as $key => $value ) { + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php + $variations_normalized[ $key ] = strtolower( $value ); + } + // now search through each product child and see if our passed variations match anything + foreach ( $product->get_children() as $variation ) { + $meta = array(); + foreach ( get_post_meta( $variation ) as $key => $value ) { + $value = $value[0]; + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); + $meta[ $key ] = strtolower( $value ); + } + // if the variation array is a part of the $meta array, we found our match + if ( $this->array_contains( $variations_normalized, $meta ) ) { + $variation_id = $variation; + break; + } + } + } + } + + return $variation_id; + } + + /** + * Utility function to see if the meta array contains data from variations + * + * @param array $needles + * @param array $haystack + * + * @return bool + */ + protected function array_contains( $needles, $haystack ) { + foreach ( $needles as $key => $value ) { + if ( $haystack[ $key ] !== $value ) { + return false; + } + } + return true; + } + + /** + * Create or update an order shipping method + * + * @since 2.2 + * @param \WC_Order $order + * @param array $shipping item data + * @param string $action 'create' to add shipping or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_shipping( $order, $shipping, $action ) { + + // total must be a positive float + if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // method ID is required + if ( ! isset( $shipping['method_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + $item = new WC_Order_Item_Shipping(); + $item->set_order_id( $order->get_id() ); + $item->set_shipping_rate( $rate ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Shipping( $shipping['id'] ); + + if ( isset( $shipping['method_id'] ) ) { + $item->set_method_id( $shipping['method_id'] ); + } + + if ( isset( $shipping['method_title'] ) ) { + $item->set_method_title( $shipping['method_title'] ); + } + + if ( isset( $shipping['total'] ) ) { + $item->set_total( floatval( $shipping['total'] ) ); + } + + $shipping_id = $item->save(); + + if ( ! $shipping_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee + * + * @since 2.2 + * @param \WC_Order $order + * @param array $fee item data + * @param string $action 'create' to add fee or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_fee( $order, $fee, $action ) { + + if ( 'create' === $action ) { + + // fee title is required + if ( ! isset( $fee['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order->get_id() ); + $item->set_name( wc_clean( $fee['title'] ) ); + $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); + + // if taxable, tax class and total are required + if ( ! empty( $fee['taxable'] ) ) { + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $item->set_tax_status( 'taxable' ); + $item->set_tax_class( $fee['tax_class'] ); + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); + } + + if ( isset( $fee['tax_data'] ) ) { + $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); + $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); + } + } + + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Fee( $fee['id'] ); + + if ( isset( $fee['title'] ) ) { + $item->set_name( wc_clean( $fee['title'] ) ); + } + + if ( isset( $fee['tax_class'] ) ) { + $item->set_tax_class( $fee['tax_class'] ); + } + + if ( isset( $fee['total'] ) ) { + $item->set_total( floatval( $fee['total'] ) ); + } + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( floatval( $fee['total_tax'] ) ); + } + + $fee_id = $item->save(); + + if ( ! $fee_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon + * + * @since 2.2 + * @param \WC_Order $order + * @param array $coupon item data + * @param string $action 'create' to add coupon or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_coupon( $order, $coupon, $action ) { + + // coupon amount must be positive float + if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // coupon code is required + if ( empty( $coupon['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $coupon['code'], + 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, + 'discount_tax' => 0, + 'order_id' => $order->get_id(), + ) ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Coupon( $coupon['id'] ); + + if ( isset( $coupon['code'] ) ) { + $item->set_code( $coupon['code'] ); + } + + if ( isset( $coupon['amount'] ) ) { + $item->set_discount( floatval( $coupon['amount'] ) ); + } + + $coupon_id = $item->save(); + + if ( ! $coupon_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $order_id, $fields = null ) { + + // ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $args = array( + 'post_id' => $order_id, + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $order_notes = array(); + + foreach ( $notes as $note ) { + + $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); + } + + /** + * Get an order note for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param string $id order note ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_note( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $order_note = array( + 'id' => $note->comment_ID, + 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order note for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @return WP_Error|array error or created note response data + */ + public function create_order_note( $order_id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); + } + + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); + + // note content is required + if ( ! isset( $data['note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); + } + + $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); + + // create the note + $note_id = $order->add_order_note( $data['note'], $is_customer_note ); + + if ( ! $note_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); + + return $this->get_order_note( $order->get_id(), $note_id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit the order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @param array $data parsed request data + * @return WP_Error|array error or edited note response data + */ + public function edit_order_note( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); + + // Note content + if ( isset( $data['note'] ) ) { + + wp_update_comment( + array( + 'comment_ID' => $note->comment_ID, + 'comment_content' => $data['note'], + ) + ); + } + + // Customer note + if ( isset( $data['customer_note'] ) ) { + + update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); + } + + do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); + + return $this->get_order_note( $order->get_id(), $note->comment_ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_note( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + // Force delete since trashed order notes could not be managed through comments list table + $result = wc_delete_order_note( $note->comment_ID ); + + if ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); + } + + do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); + + return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the order refunds for an order + * + * @since 2.2 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_refunds( $order_id, $fields = null ) { + + // Ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $refund_items = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $order_id, + 'limit' => -1, + 'return' => 'ids', + ) ); + $order_refunds = array(); + + foreach ( $refund_items as $refund_id ) { + $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); + } + + return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); + } + + /** + * Get an order refund for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param int $id + * @param string|null $fields fields to limit response to + * @param array $filter + * + * @return array|WP_Error + */ + public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + $order = wc_get_order( $order_id ); + $refund = wc_get_order( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + $line_items = array(); + + // Add line items + foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_items[] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), + 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), + 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), + ); + } + + $order_refund = array( + 'id' => $refund->get_id(), + 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), + 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), + 'reason' => $refund->get_reason(), + 'line_items' => $line_items, + ); + + return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order refund for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @param bool $api_refund do refund using a payment gateway API + * @return WP_Error|array error or created refund response data + */ + public function create_order_refund( $order_id, $data, $api_refund = true ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); + } + + $order_id = absint( $order_id ); + + if ( empty( $order_id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); + + // Refund amount is required + if ( ! isset( $data['amount'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); + } elseif ( 0 > $data['amount'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); + } + + $data['order_id'] = $order_id; + $data['refund_id'] = 0; + + // Create the refund + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_id ); + + if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit an order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @param array $data parsed request data + * @return WP_Error|array error or edited refund response data + */ + public function edit_order_refund( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure order ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); + + // Update reason + if ( isset( $data['reason'] ) ) { + $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); + + if ( is_wp_error( $updated_refund ) ) { + return $updated_refund; + } + } + + // Update refund amount + if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { + update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); + } + + do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_refund( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure refund ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + wc_delete_shop_order_transients( $order_id ); + + do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); + + return $this->delete( $refund->ID, 'refund', true ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Bulk update or insert orders + * Accepts an array with orders in the formats supported by + * WC_API_Orders->create_order() and WC_API_Orders->edit_order() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['orders'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); + } + + $data = $data['orders']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $orders = array(); + + foreach ( $data as $_order ) { + $order_id = 0; + + // Try to get the order ID + if ( isset( $_order['id'] ) ) { + $order_id = intval( $_order['id'] ); + } + + // Order exists / edit order + if ( $order_id ) { + $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); + + if ( is_wp_error( $edit ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $orders[] = $edit['order']; + } + } else { + // Order don't exists / create order + $new = $this->create_order( array( 'order' => $_order ) ); + + if ( is_wp_error( $new ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $orders[] = $new['order']; + } + } + } + + return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v2/class-wc-api-products.php b/includes/legacy/api/v2/class-wc-api-products.php new file mode 100644 index 0000000..80ee148 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-products.php @@ -0,0 +1,2312 @@ + + * 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 = wp_filter_post_kses( $data['description'] ); + } + + // Enable short description html tags. + $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; + if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { + $post_excerpt = wp_filter_post_kses( $data['short_description'] ); + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + $product = new $classname(); + + $product->set_name( wc_clean( $data['title'] ) ); + $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); + $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); + $product->set_description( isset( $data['description'] ) ? $post_content : '' ); + + // Attempts to create the new product. + $product->save(); + $id = $product->get_id(); + + // Checks for an error in the product creation + if ( 0 >= $id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); + } + + // Check for featured/gallery images, upload it and set it + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields + $product = $this->save_product_meta( $product, $data ); + $product->save(); + + // Save variations + if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } + + do_action( 'woocommerce_api_create_product', $id, $data ); + + // Clear cache/transients + wc_delete_product_transients( $id ); + + $this->server->send_status( 201 ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product + * + * @since 2.2 + * + * @param int $id the product ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product( $id, $data ) { + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + $id = $this->validate_request( $id, 'product', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); + + // Product title. + if ( isset( $data['title'] ) ) { + $product->set_name( wc_clean( $data['title'] ) ); + } + + // Product name (slug). + if ( isset( $data['name'] ) ) { + $product->set_slug( wc_clean( $data['name'] ) ); + } + + // Product status. + if ( isset( $data['status'] ) ) { + $product->set_status( wc_clean( $data['status'] ) ); + } + + // Product short description. + if ( isset( $data['short_description'] ) ) { + // Enable short description html tags. + $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); + $product->set_short_description( $post_excerpt ); + } + + // Product description. + if ( isset( $data['description'] ) ) { + // Enable description html tags. + $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); + $product->set_description( $post_content ); + } + + // Validate the product type. + if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } else { + // Just sync variations. + $product = WC_Product_Variable::sync( $product, false ); + } + } + + $product->save(); + + do_action( 'woocommerce_api_edit_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product. + * + * @since 2.2 + * + * @param int $id the product ID. + * @param bool $force true to permanently delete order, false to move to trash. + * + * @return array|WP_Error + */ + public function delete_product( $id, $force = false ) { + + $id = $this->validate_request( $id, 'product', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + do_action( 'woocommerce_api_delete_product', $id, $this ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $product->delete( true ); + $result = ! ( $product->get_id() > 0 ); + } else { + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); + } else { + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); + } + } + + /** + * Get the reviews for a product + * + * @since 2.1 + * @param int $id the product ID to get reviews for + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_product_reviews( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $comments = get_approved_comments( $id ); + $reviews = array(); + + foreach ( $comments as $comment ) { + + $reviews[] = array( + 'id' => intval( $comment->comment_ID ), + 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), + 'review' => $comment->comment_content, + 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), + 'reviewer_name' => $comment->comment_author, + 'reviewer_email' => $comment->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), + ); + } + + return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); + } + + /** + * Get the orders for a product + * + * @since 2.4.0 + * @param int $id the product ID to get orders for + * @param string fields fields to retrieve + * @param array $filter filters to include in response + * @param string $status the order status to retrieve + * @param $page $page page to retrieve + * @return array|WP_Error + */ + public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { + global $wpdb; + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $id ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $filter = array_merge( $filter, array( + 'in' => implode( ',', $order_ids ), + ) ); + + $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); + + return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); + } + + /** + * Get a listing of product categories + * + * @since 2.2 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_categories( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $product_categories = array(); + + $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); + } + + return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product category for the given ID + * + * @since 2.2 + * + * @param string $id product category term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_category( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_cat' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + // Get category display type + $display_type = get_term_meta( $term_id, 'display_type', true ); + + // Get category image + $image = ''; + if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $product_category = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => $image ? esc_url( $image ) : '', + 'count' => intval( $term->count ), + ); + + return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * 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 ); + } + + if ( ! is_a( $product, 'WC_Product' ) ) { + return array(); + } + + $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']; + $product->set_regular_price( $regular_price ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; + $product->set_sale_price( $sale_price ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $date_from = $data['sale_price_dates_from']; + } else { + $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $date_to = $data['sale_price_dates_to']; + } else { + $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; + } + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); + } + + $product->set_date_on_sale_to( $date_to ); + $product->set_date_on_sale_from( $date_from ); + + if ( $product->is_on_sale( 'edit' ) ) { + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + } + + // Product parent ID for groups + if ( isset( $data['parent_id'] ) ) { + $product->set_parent_id( absint( $data['parent_id'] ) ); + } + + // Sold Individually + if ( isset( $data['sold_individually'] ) ) { + $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); + } + + // Stock status + if ( isset( $data['in_stock'] ) ) { + $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock Data + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock + if ( isset( $data['managing_stock'] ) ) { + $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; + $product->set_manage_stock( $managing_stock ); + } else { + $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; + } + + // Backorders + if ( isset( $data['backorders'] ) ) { + if ( 'notify' == $data['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; + } + + $product->set_backorders( $backorders ); + } else { + $backorders = $product->get_backorders(); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( 'yes' == $managing_stock ) { + $product->set_backorders( $backorders ); + + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity + if ( isset( $data['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); + } + } 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'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files + * + * @since 2.2 + * @param WC_Product $product + * @param array $downloads + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Get attribute taxonomy by slug. + * + * @since 2.2 + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * Get the images for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_images( $product ) { + $images = $attachment_ids = array(); + $product_image = $product->get_image_id(); + + // Add featured image. + if ( ! empty( $product_image ) ) { + $attachment_ids[] = $product_image; + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + + $attachment_post = get_post( $attachment_id ); + + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'title' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + + $images[] = array( + 'id' => 0, + 'created_at' => $this->server->format_datetime( time() ), // Default to now. + 'updated_at' => $this->server->format_datetime( time() ), + 'src' => wc_placeholder_img_src(), + 'title' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Save product images + * + * @since 2.2 + * + * @param WC_Product $product + * @param array $images + * + * @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 + * + * @throws WC_API_Exception + */ + public function upload_product_image( $image_url ) { + $upload = wc_rest_upload_image_from_url( $image_url ); + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_product_image_upload_error', $upload->get_error_message(), 400 ); + } + + 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_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => wc_attribute_label( $attribute['name'] ), + 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get the downloads for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_downloads( $product ) { + + $downloads = array(); + + if ( $product->is_downloadable() ) { + + foreach ( $product->get_downloads() as $file_id => $file ) { + + $downloads[] = array( + 'id' => $file_id, // do not cast as int as this is a hash + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get a listing of product attributes + * + * @since 2.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' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute + * + * @since 2.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' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute + * + * @since 2.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' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get 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/legacy/api/v2/class-wc-api-reports.php b/includes/legacy/api/v2/class-wc-api-reports.php new file mode 100644 index 0000000..6a22e7a --- /dev/null +++ b/includes/legacy/api/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/legacy/api/v2/class-wc-api-resource.php b/includes/legacy/api/v2/class-wc-api-resource.php new file mode 100644 index 0000000..0bfa3f0 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-resource.php @@ -0,0 +1,466 @@ +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/legacy/api/v2/class-wc-api-server.php b/includes/legacy/api/v2/class-wc-api-server.php new file mode 100644 index 0000000..1ed69cc --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-server.php @@ -0,0 +1,775 @@ + 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 + */ + 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|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() { + // @codingStandardsIgnoreStart + // $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; + // @codingStandardsIgnoreEnd + } + + /** + * 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/legacy/api/v2/class-wc-api-webhooks.php b/includes/legacy/api/v2/class-wc-api-webhooks.php new file mode 100644 index 0000000..98e14b5 --- /dev/null +++ b/includes/legacy/api/v2/class-wc-api-webhooks.php @@ -0,0 +1,509 @@ +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['results'] as $webhook_id ) { + $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query['headers'] ); + + 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 = wc_get_webhook( $id ); + + $webhook_data = array( + 'id' => $webhook->get_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_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + ); + + 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( 'manage_woocommerce' ) ) { + 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' => $query['headers']->total ); + } 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( 'manage_woocommerce' ) ) { + 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' => 'webhook_' . wp_generate_password(), + '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' ) ) ), + ), $data, $this ); + + $webhook = new WC_Webhook(); + + $webhook->set_name( $webhook_data['post_title'] ); + $webhook->set_user_id( $webhook_data['post_author'] ); + $webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' ); + $webhook->set_topic( $data['topic'] ); + $webhook->set_delivery_url( $data['delivery_url'] ); + $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) ); + $webhook->set_api_version( 'legacy_v3' ); + $webhook->save(); + + $webhook->deliver_ping(); + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this ); + + return $this->get_webhook( $webhook->get_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 = wc_get_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->set_status( $data['status'] ); + } + + // update name + if ( ! empty( $data['name'] ) ) { + $webhook->set_name( $data['name'] ); + } + + $webhook->save(); + + do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this ); + + return $this->get_webhook( $webhook->get_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 ); + + $webhook = wc_get_webhook( $id ); + + return $webhook->delete( true ); + } + + /** + * Helper method to get webhook post objects + * + * @since 2.2 + * @param array $args Request arguments for filtering query. + * @return array + */ + private function query_webhooks( $args ) { + $args = $this->merge_query_args( array(), $args ); + + $args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) ); + + if ( empty( $args['offset'] ) ) { + $args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0; + } + + $page = $args['paged']; + unset( $args['paged'], $args['posts_per_page'] ); + + if ( isset( $args['s'] ) ) { + $args['search'] = $args['s']; + unset( $args['s'] ); + } + + // Post type to webhook status. + if ( ! empty( $args['post_status'] ) ) { + $args['status'] = $args['post_status']; + unset( $args['post_status'] ); + } + + if ( ! empty( $args['post__in'] ) ) { + $args['include'] = $args['post__in']; + unset( $args['post__in'] ); + } + + if ( ! empty( $args['date_query'] ) ) { + foreach ( $args['date_query'] as $date_query ) { + if ( 'post_date_gmt' === $date_query['column'] ) { + $args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null; + $args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null; + } elseif ( 'post_modified_gmt' === $date_query['column'] ) { + $args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null; + $args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null; + } + } + + unset( $args['date_query'] ); + } + + $args['paginate'] = true; + + // Get the webhooks. + $data_store = WC_Data_Store::load( 'webhook' ); + $results = $data_store->search_webhooks( $args ); + + // Get total items. + $headers = new stdClass; + $headers->page = $page; + $headers->total = $results->total; + $headers->is_single = $args['limit'] > $headers->total; + $headers->total_pages = $results->max_num_pages; + + return array( + 'results' => $results->webhooks, + 'headers' => $headers, + ); + } + + /** + * Get deliveries for a webhook + * + * @since 2.2 + * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. + * @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; + } + + return array( 'webhook_deliveries' => array() ); + } + + /** + * Get the delivery log for the given webhook ID and delivery ID + * + * @since 2.2 + * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. + * @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 = 0; + + if ( ! $log ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 ); + } + + return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * 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 3.3.0 + * @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 ) { + $id = absint( $id ); + + // Validate ID. + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); + } + + $webhook = wc_get_webhook( $id ); + + if ( null === $webhook ) { + return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) ); + } + + // Validate permissions. + switch ( $context ) { + + case 'read': + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); + } + break; + + case 'edit': + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); + } + break; + + case 'delete': + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); + } + break; + } + + return $id; + } +} diff --git a/includes/legacy/api/v2/interface-wc-api-handler.php b/includes/legacy/api/v2/interface-wc-api-handler.php new file mode 100644 index 0000000..94ce87d --- /dev/null +++ b/includes/legacy/api/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 parameters + * @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/legacy/api/v3/class-wc-api-coupons.php b/includes/legacy/api/v3/class-wc-api-coupons.php new file mode 100644 index 0000000..870dfd2 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-coupons.php @@ -0,0 +1,576 @@ + + * + * @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 ); + do_action( 'woocommerce_new_coupon', $id ); + + $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 ); + do_action( 'woocommerce_update_coupon', $id ); + + 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/legacy/api/v3/class-wc-api-customers.php b/includes/legacy/api/v3/class-wc-api-customers.php new file mode 100644 index 0000000..2df6fd8 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-customers.php @@ -0,0 +1,829 @@ + + * 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']; + } + + // Order by + 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 ( ! wc_rest_check_user_permissions( 'edit', $customer->ID ) ) { + 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 ( ! wc_rest_check_user_permissions( 'delete', $customer->ID ) ) { + 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/legacy/api/v3/class-wc-api-exception.php b/includes/legacy/api/v3/class-wc-api-exception.php new file mode 100644 index 0000000..5000f76 --- /dev/null +++ b/includes/legacy/api/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/legacy/api/v3/class-wc-api-json-handler.php b/includes/legacy/api/v3/class-wc-api-json-handler.php new file mode 100644 index 0000000..ee4d5e8 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-json-handler.php @@ -0,0 +1,73 @@ +api->server->send_status( 400 ); + return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_disabled', 'message' => __( 'JSONP support is disabled on this site', 'woocommerce' ) ) ) ); + } + + $jsonp_callback = $_GET['_jsonp']; + + if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) { + WC()->api->server->send_status( 400 ); + return wp_json_encode( array( array( 'code' => 'woocommerce_api_jsonp_callback_invalid', __( 'The JSONP callback function is invalid', 'woocommerce' ) ) ) ); + } + + WC()->api->server->header( 'X-Content-Type-Options', 'nosniff' ); + + // Prepend '/**/' to mitigate possible JSONP Flash attacks. + // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/ + return '/**/' . $jsonp_callback . '(' . wp_json_encode( $data ) . ')'; + } + + return wp_json_encode( $data ); + } +} diff --git a/includes/legacy/api/v3/class-wc-api-orders.php b/includes/legacy/api/v3/class-wc-api-orders.php new file mode 100644 index 0000000..2fe2b6c --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-orders.php @@ -0,0 +1,1877 @@ + + * GET /orders//notes + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET|POST /orders + $routes[ $this->base ] = array( + array( array( $this, 'get_orders' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /orders/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_orders_count' ), WC_API_Server::READABLE ), + ); + + # GET /orders/statuses + $routes[ $this->base . '/statuses' ] = array( + array( array( $this, 'get_order_statuses' ), WC_API_Server::READABLE ), + ); + + # GET|PUT|DELETE /orders/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_order' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order' ), WC_API_Server::DELETABLE ), + ); + + # GET|POST /orders//notes + $routes[ $this->base . '/(?P\d+)/notes' ] = array( + array( array( $this, 'get_order_notes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_note' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//notes/ + $routes[ $this->base . '/(?P\d+)/notes/(?P\d+)' ] = array( + array( array( $this, 'get_order_note' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_note' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_note' ), WC_API_SERVER::DELETABLE ), + ); + + # GET|POST /orders//refunds + $routes[ $this->base . '/(?P\d+)/refunds' ] = array( + array( array( $this, 'get_order_refunds' ), WC_API_Server::READABLE ), + array( array( $this, 'create_order_refund' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET|PUT|DELETE /orders//refunds/ + $routes[ $this->base . '/(?P\d+)/refunds/(?P\d+)' ] = array( + array( array( $this, 'get_order_refund' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_order_refund' ), WC_API_SERVER::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_order_refund' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /orders/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all orders + * + * @since 2.1 + * @param string $fields + * @param array $filter + * @param string $status + * @param int $page + * @return array + */ + public function get_orders( $fields = null, $filter = array(), $status = null, $page = 1 ) { + + if ( ! empty( $status ) ) { + $filter['status'] = $status; + } + + $filter['page'] = $page; + + $query = $this->query_orders( $filter ); + + $orders = array(); + + foreach ( $query->posts as $order_id ) { + + if ( ! $this->is_readable( $order_id ) ) { + continue; + } + + $orders[] = current( $this->get_order( $order_id, $fields, $filter ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'orders' => $orders ); + } + + + /** + * Get the order for the given ID. + * + * @since 2.1 + * @param int $id The order ID. + * @param array $fields Request fields. + * @param array $filter Request filters. + * @return array|WP_Error + */ + public function get_order( $id, $fields = null, $filter = array() ) { + + // Ensure order ID is valid & user has permission to read. + $id = $this->validate_request( $id, $this->post_type, 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + // Get the decimal precession. + $dp = ( isset( $filter['dp'] ) ? intval( $filter['dp'] ) : 2 ); + $order = wc_get_order( $id ); + $expand = array(); + + if ( ! empty( $filter['expand'] ) ) { + $expand = explode( ',', $filter['expand'] ); + } + + $order_data = array( + 'id' => $order->get_id(), + 'order_number' => $order->get_order_number(), + 'order_key' => $order->get_order_key(), + 'created_at' => $this->server->format_datetime( $order->get_date_created() ? $order->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $order->get_date_modified() ? $order->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'completed_at' => $this->server->format_datetime( $order->get_date_completed() ? $order->get_date_completed()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'status' => $order->get_status(), + 'currency' => $order->get_currency(), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'subtotal' => wc_format_decimal( $order->get_subtotal(), $dp ), + 'total_line_items_quantity' => $order->get_item_count(), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'total_shipping' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'total_discount' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'shipping_methods' => $order->get_shipping_method(), + 'payment_details' => array( + 'method_id' => $order->get_payment_method(), + 'method_title' => $order->get_payment_method_title(), + 'paid' => ! is_null( $order->get_date_paid() ), + ), + 'billing_address' => array( + 'first_name' => $order->get_billing_first_name(), + 'last_name' => $order->get_billing_last_name(), + 'company' => $order->get_billing_company(), + 'address_1' => $order->get_billing_address_1(), + 'address_2' => $order->get_billing_address_2(), + 'city' => $order->get_billing_city(), + 'state' => $order->get_billing_state(), + 'postcode' => $order->get_billing_postcode(), + 'country' => $order->get_billing_country(), + 'email' => $order->get_billing_email(), + 'phone' => $order->get_billing_phone(), + ), + 'shipping_address' => array( + 'first_name' => $order->get_shipping_first_name(), + 'last_name' => $order->get_shipping_last_name(), + 'company' => $order->get_shipping_company(), + 'address_1' => $order->get_shipping_address_1(), + 'address_2' => $order->get_shipping_address_2(), + 'city' => $order->get_shipping_city(), + 'state' => $order->get_shipping_state(), + 'postcode' => $order->get_shipping_postcode(), + 'country' => $order->get_shipping_country(), + ), + 'note' => $order->get_customer_note(), + 'customer_ip' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'customer_id' => $order->get_user_id(), + 'view_order_url' => $order->get_view_order_url(), + 'line_items' => array(), + 'shipping_lines' => array(), + 'tax_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_item = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item->get_total_tax(), $dp ), + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + ); + + if ( in_array( 'products', $expand ) && is_object( $product ) ) { + $_product_data = WC()->api->WC_API_Products->get_product( $product->get_id() ); + + if ( isset( $_product_data['product'] ) ) { + $line_item['product_data'] = $_product_data['product']; + } + } + + $order_data['line_items'][] = $line_item; + } + + // Add shipping. + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $order_data['shipping_lines'][] = array( + 'id' => $shipping_item_id, + 'method_id' => $shipping_item->get_method_id(), + 'method_title' => $shipping_item->get_name(), + 'total' => wc_format_decimal( $shipping_item->get_total(), $dp ), + ); + } + + // Add taxes. + foreach ( $order->get_tax_totals() as $tax_code => $tax ) { + $tax_line = array( + 'id' => $tax->id, + 'rate_id' => $tax->rate_id, + 'code' => $tax_code, + 'title' => $tax->label, + 'total' => wc_format_decimal( $tax->amount, $dp ), + 'compound' => (bool) $tax->is_compound, + ); + + if ( in_array( 'taxes', $expand ) ) { + $_rate_data = WC()->api->WC_API_Taxes->get_tax( $tax->rate_id ); + + if ( isset( $_rate_data['tax'] ) ) { + $tax_line['rate_data'] = $_rate_data['tax']; + } + } + + $order_data['tax_lines'][] = $tax_line; + } + + // Add fees. + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $order_data['fee_lines'][] = array( + 'id' => $fee_item_id, + 'title' => $fee_item->get_name(), + 'tax_class' => $fee_item->get_tax_class(), + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + ); + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item->get_code(), + 'amount' => wc_format_decimal( $coupon_item->get_discount(), $dp ), + ); + + if ( in_array( 'coupons', $expand ) ) { + $_coupon_data = WC()->api->WC_API_Coupons->get_coupon_by_code( $coupon_item->get_code() ); + + if ( ! is_wp_error( $_coupon_data ) && isset( $_coupon_data['coupon'] ) ) { + $coupon_line['coupon_data'] = $_coupon_data['coupon']; + } + } + + $order_data['coupon_lines'][] = $coupon_line; + } + + return array( 'order' => apply_filters( 'woocommerce_api_order_response', $order_data, $order, $fields, $this->server ) ); + } + + /** + * Get the total number of orders + * + * @since 2.4 + * + * @param string $status + * @param array $filter + * + * @return array|WP_Error + */ + public function get_orders_count( $status = null, $filter = array() ) { + + try { + if ( ! current_user_can( 'read_private_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_orders_count', __( 'You do not have permission to read the orders count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $status ) ) { + + if ( 'any' === $status ) { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $filter['status'] = str_replace( 'wc-', '', $slug ); + $query = $this->query_orders( $filter ); + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = (int) $query->found_posts; + } + + return array( 'count' => $order_statuses ); + + } else { + $filter['status'] = $status; + } + } + + $query = $this->query_orders( $filter ); + + return array( 'count' => (int) $query->found_posts ); + + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a list of valid order statuses + * + * Note this requires no specific permissions other than being an authenticated + * API user. Order statuses (particularly custom statuses) could be considered + * private information which is why it's not in the API index. + * + * @since 2.1 + * @return array + */ + public function get_order_statuses() { + + $order_statuses = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + $order_statuses[ str_replace( 'wc-', '', $slug ) ] = $name; + } + + return array( 'order_statuses' => apply_filters( 'woocommerce_api_order_statuses_response', $order_statuses, $this ) ); + } + + /** + * Create an order + * + * @since 2.2 + * @param array $data raw order data + * @return array|WP_Error + */ + public function create_order( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order', __( 'You do not have permission to create orders', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_data', $data, $this ); + + // default order args, note that status is checked for validity in wc_create_order() + $default_order_args = array( + 'status' => isset( $data['status'] ) ? $data['status'] : '', + 'customer_note' => isset( $data['note'] ) ? $data['note'] : null, + ); + + // if creating order for existing customer + if ( ! empty( $data['customer_id'] ) ) { + + // make sure customer exists + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + $default_order_args['customer_id'] = $data['customer_id']; + } + + // create the pending order + $order = $this->create_base_order( $default_order_args, $data ); + + if ( is_wp_error( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order', sprintf( __( 'Cannot create order: %s', 'woocommerce' ), implode( ', ', $order->get_error_messages() ) ), 400 ); + } + + // billing/shipping addresses + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $set_item = "set_{$line_type}"; + + foreach ( $data[ $line ] as $item ) { + + $this->$set_item( $order, $item, 'create' ); + } + } + } + + // set is vat exempt + if ( isset( $data['is_vat_exempt'] ) ) { + update_post_meta( $order->get_id(), '_is_vat_exempt', $data['is_vat_exempt'] ? 'yes' : 'no' ); + } + + // calculate totals and set them + $order->calculate_totals(); + + // payment method (and payment_complete() if `paid` == true) + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // method ID & title are required + if ( empty( $data['payment_details']['method_id'] ) || empty( $data['payment_details']['method_title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_payment_details', __( 'Payment method ID and title are required', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + + // mark as paid if set + if ( isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // set order currency + if ( isset( $data['currency'] ) ) { + + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // set order meta + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_create_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_new_order', $order->get_id() ); + + return $this->get_order( $order->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Creates new WC_Order. + * + * Requires a separate function for classes that extend WC_API_Orders. + * + * @since 2.3 + * + * @param $args array + * @param $data + * + * @return WC_Order + */ + protected function create_base_order( $args, $data ) { + return wc_create_order( $args ); + } + + /** + * Edit an order + * + * @since 2.2 + * @param int $id the order ID + * @param array $data + * @return array|WP_Error + */ + public function edit_order( $id, $data ) { + try { + if ( ! isset( $data['order'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order' ), 400 ); + } + + $data = $data['order']; + + $update_totals = false; + + $id = $this->validate_request( $id, $this->post_type, 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $data = apply_filters( 'woocommerce_api_edit_order_data', $data, $id, $this ); + $order = wc_get_order( $id ); + + if ( empty( $order ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $order_args = array( 'order_id' => $order->get_id() ); + + // Customer note. + if ( isset( $data['note'] ) ) { + $order_args['customer_note'] = $data['note']; + } + + // Customer ID. + if ( isset( $data['customer_id'] ) && $data['customer_id'] != $order->get_user_id() ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $data['customer_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_customer_user', $data['customer_id'] ); + } + + // Billing/shipping address. + $this->set_order_addresses( $order, $data ); + + $lines = array( + 'line_item' => 'line_items', + 'shipping' => 'shipping_lines', + 'fee' => 'fee_lines', + 'coupon' => 'coupon_lines', + ); + + foreach ( $lines as $line_type => $line ) { + + if ( isset( $data[ $line ] ) && is_array( $data[ $line ] ) ) { + + $update_totals = true; + + foreach ( $data[ $line ] as $item ) { + // Item ID is always required. + if ( ! array_key_exists( 'id', $item ) ) { + $item['id'] = null; + } + + // Create item. + if ( is_null( $item['id'] ) ) { + $this->set_item( $order, $line_type, $item, 'create' ); + } elseif ( $this->item_is_null( $item ) ) { + // Delete item. + wc_delete_order_item( $item['id'] ); + } else { + // Update item. + $this->set_item( $order, $line_type, $item, 'update' ); + } + } + } + } + + // Payment method (and payment_complete() if `paid` == true and order needs payment). + if ( isset( $data['payment_details'] ) && is_array( $data['payment_details'] ) ) { + + // Method ID. + if ( isset( $data['payment_details']['method_id'] ) ) { + update_post_meta( $order->get_id(), '_payment_method', $data['payment_details']['method_id'] ); + } + + // Method title. + if ( isset( $data['payment_details']['method_title'] ) ) { + update_post_meta( $order->get_id(), '_payment_method_title', sanitize_text_field( $data['payment_details']['method_title'] ) ); + } + + // Mark as paid if set. + if ( $order->needs_payment() && isset( $data['payment_details']['paid'] ) && true === $data['payment_details']['paid'] ) { + $order->payment_complete( isset( $data['payment_details']['transaction_id'] ) ? $data['payment_details']['transaction_id'] : '' ); + } + } + + // Set order currency. + if ( isset( $data['currency'] ) ) { + if ( ! array_key_exists( $data['currency'], get_woocommerce_currencies() ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_order_currency', __( 'Provided order currency is invalid.', 'woocommerce' ), 400 ); + } + + update_post_meta( $order->get_id(), '_order_currency', $data['currency'] ); + } + + // If items have changed, recalculate order totals. + if ( $update_totals ) { + $order->calculate_totals(); + } + + // Update order meta. + if ( isset( $data['order_meta'] ) && is_array( $data['order_meta'] ) ) { + $this->set_order_meta( $order->get_id(), $data['order_meta'] ); + } + + // Update the order post to set customer note/modified date. + wc_update_order( $order_args ); + + // Order status. + if ( ! empty( $data['status'] ) ) { + // Refresh the order instance. + $order = wc_get_order( $order->get_id() ); + $order->update_status( $data['status'], isset( $data['status_note'] ) ? $data['status_note'] : '', true ); + } + + wc_delete_shop_order_transients( $order ); + + do_action( 'woocommerce_api_edit_order', $order->get_id(), $data, $this ); + do_action( 'woocommerce_update_order', $order->get_id() ); + + return $this->get_order( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete an order + * + * @param int $id the order ID + * @param bool $force true to permanently delete order, false to move to trash + * @return array|WP_Error + */ + public function delete_order( $id, $force = false ) { + + $id = $this->validate_request( $id, $this->post_type, 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + wc_delete_shop_order_transients( $id ); + + do_action( 'woocommerce_api_delete_order', $id, $this ); + + return $this->delete( $id, 'order', ( 'true' === $force ) ); + } + + /** + * Helper method to get order post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + protected function query_orders( $args ) { + + // set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => $this->post_type, + 'post_status' => array_keys( wc_get_order_statuses() ), + ); + + // add status argument + if ( ! empty( $args['status'] ) ) { + $statuses = 'wc-' . str_replace( ',', ',wc-', $args['status'] ); + $statuses = explode( ',', $statuses ); + $query_args['post_status'] = $statuses; + + unset( $args['status'] ); + } + + if ( ! empty( $args['customer_id'] ) ) { + $query_args['meta_query'] = array( + array( + 'key' => '_customer_user', + 'value' => absint( $args['customer_id'] ), + 'compare' => '=', + ), + ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Helper method to set/update the billing & shipping addresses for + * an order + * + * @since 2.1 + * @param \WC_Order $order + * @param array $data + */ + protected function set_order_addresses( $order, $data ) { + + $address_fields = array( + 'first_name', + 'last_name', + 'company', + 'email', + 'phone', + 'address_1', + 'address_2', + 'city', + 'state', + 'postcode', + 'country', + ); + + $billing_address = $shipping_address = array(); + + // billing address + if ( isset( $data['billing_address'] ) && is_array( $data['billing_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['billing_address'][ $field ] ) ) { + $billing_address[ $field ] = wc_clean( $data['billing_address'][ $field ] ); + } + } + + unset( $address_fields['email'] ); + unset( $address_fields['phone'] ); + } + + // shipping address + if ( isset( $data['shipping_address'] ) && is_array( $data['shipping_address'] ) ) { + + foreach ( $address_fields as $field ) { + + if ( isset( $data['shipping_address'][ $field ] ) ) { + $shipping_address[ $field ] = wc_clean( $data['shipping_address'][ $field ] ); + } + } + } + + $this->update_address( $order, $billing_address, 'billing' ); + $this->update_address( $order, $shipping_address, 'shipping' ); + + // update user meta + if ( $order->get_user_id() ) { + foreach ( $billing_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'billing_' . $key, $value ); + } + foreach ( $shipping_address as $key => $value ) { + update_user_meta( $order->get_user_id(), 'shipping_' . $key, $value ); + } + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Helper method to add/update order meta, with two restrictions: + * + * 1) Only non-protected meta (no leading underscore) can be set + * 2) Meta values must be scalar (int, string, bool) + * + * @since 2.2 + * @param int $order_id valid order ID + * @param array $order_meta order meta in array( 'meta_key' => 'meta_value' ) format + */ + protected function set_order_meta( $order_id, $order_meta ) { + + foreach ( $order_meta as $meta_key => $meta_value ) { + + if ( is_string( $meta_key ) && ! is_protected_meta( $meta_key ) && is_scalar( $meta_value ) ) { + update_post_meta( $order_id, $meta_key, $meta_value ); + } + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null + * + * Items can be deleted by setting the resource ID to null + * + * @since 2.2 + * @param array $item item provided in the request body + * @return bool true if the item resource ID is null, false otherwise + */ + protected function item_is_null( $item ) { + + $keys = array( 'product_id', 'method_id', 'title', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Wrapper method to create/update order items + * + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @since 2.2 + * @param \WC_Order $order order + * @param string $item_type + * @param array $item item provided in the request body + * @param string $action either 'create' or 'update' + * @throws WC_API_Exception if item ID is not associated with order + */ + protected function set_item( $order, $item_type, $item, $action ) { + global $wpdb; + + $set_method = "set_{$item_type}"; + + // verify provided line item ID is associated with order + if ( 'update' === $action ) { + + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $item['id'] ), + absint( $order->get_id() ) + ) ); + + if ( is_null( $result ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + $this->$set_method( $order, $item, $action ); + } + + /** + * Create or update a line item + * + * @since 2.2 + * @param \WC_Order $order + * @param array $item line item data + * @param string $action 'create' to add line item or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_line_item( $order, $item, $action ) { + $creating = ( 'create' === $action ); + + // product is always required + if ( ! isset( $item['product_id'] ) && ! isset( $item['sku'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID or SKU is required', 'woocommerce' ), 400 ); + } + + // when updating, ensure product ID provided matches + if ( 'update' === $action ) { + + $item_product_id = wc_get_order_item_meta( $item['id'], '_product_id' ); + $item_variation_id = wc_get_order_item_meta( $item['id'], '_variation_id' ); + + if ( $item['product_id'] != $item_product_id && $item['product_id'] != $item_variation_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_id', __( 'Product ID provided does not match this line item', 'woocommerce' ), 400 ); + } + } + + if ( isset( $item['product_id'] ) ) { + $product_id = $item['product_id']; + } elseif ( isset( $item['sku'] ) ) { + $product_id = wc_get_product_id_by_sku( $item['sku'] ); + } + + // variations must each have a key & value + $variation_id = 0; + if ( isset( $item['variations'] ) && is_array( $item['variations'] ) ) { + foreach ( $item['variations'] as $key => $value ) { + if ( ! $key || ! $value ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_variation', __( 'The product variation is invalid', 'woocommerce' ), 400 ); + } + } + $variation_id = $this->get_variation_id( wc_get_product( $product_id ), $item['variations'] ); + } + + $product = wc_get_product( $variation_id ? $variation_id : $product_id ); + + // must be a valid WC_Product + if ( ! is_object( $product ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product', __( 'Product is invalid.', 'woocommerce' ), 400 ); + } + + // quantity must be positive float + if ( isset( $item['quantity'] ) && floatval( $item['quantity'] ) <= 0 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity must be a positive float.', 'woocommerce' ), 400 ); + } + + // quantity is required when creating + if ( $creating && ! isset( $item['quantity'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_quantity', __( 'Product quantity is required.', 'woocommerce' ), 400 ); + } + + // quantity + if ( $creating ) { + $line_item = new WC_Order_Item_Product(); + } else { + $line_item = new WC_Order_Item_Product( $item['id'] ); + } + + $line_item->set_product( $product ); + $line_item->set_order_id( $order->get_id() ); + + if ( isset( $item['quantity'] ) ) { + $line_item->set_quantity( $item['quantity'] ); + } + if ( isset( $item['total'] ) ) { + $line_item->set_total( floatval( $item['total'] ) ); + } elseif ( $creating ) { + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $line_item->get_quantity() ) ); + $line_item->set_total( $total ); + $line_item->set_subtotal( $total ); + } + if ( isset( $item['total_tax'] ) ) { + $line_item->set_total_tax( floatval( $item['total_tax'] ) ); + } + if ( isset( $item['subtotal'] ) ) { + $line_item->set_subtotal( floatval( $item['subtotal'] ) ); + } + if ( isset( $item['subtotal_tax'] ) ) { + $line_item->set_subtotal_tax( floatval( $item['subtotal_tax'] ) ); + } + if ( $variation_id ) { + $line_item->set_variation_id( $variation_id ); + $line_item->set_variation( $item['variations'] ); + } + + // Save or add to order. + if ( $creating ) { + $order->add_item( $line_item ); + } else { + $item_id = $line_item->save(); + + if ( ! $item_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_create_line_item', __( 'Cannot create line item, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Given a product ID & API provided variations, find the correct variation ID to use for calculation + * We can't just trust input from the API to pass a variation_id manually, otherwise you could pass + * the cheapest variation ID but provide other information so we have to look up the variation ID. + * + * @param WC_Product $product Product instance + * @param array $variations + * + * @return int Returns an ID if a valid variation was found for this product + */ + public function get_variation_id( $product, $variations = array() ) { + $variation_id = null; + $variations_normalized = array(); + + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + if ( isset( $variations ) && is_array( $variations ) ) { + // start by normalizing the passed variations + foreach ( $variations as $key => $value ) { + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); // from get_attributes in class-wc-api-products.php + $variations_normalized[ $key ] = strtolower( $value ); + } + // now search through each product child and see if our passed variations match anything + foreach ( $product->get_children() as $variation ) { + $meta = array(); + foreach ( get_post_meta( $variation ) as $key => $value ) { + $value = $value[0]; + $key = str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $key ) ); + $meta[ $key ] = strtolower( $value ); + } + // if the variation array is a part of the $meta array, we found our match + if ( $this->array_contains( $variations_normalized, $meta ) ) { + $variation_id = $variation; + break; + } + } + } + } + + return $variation_id; + } + + /** + * Utility function to see if the meta array contains data from variations + * + * @param array $needles + * @param array $haystack + * + * @return bool + */ + protected function array_contains( $needles, $haystack ) { + foreach ( $needles as $key => $value ) { + if ( $haystack[ $key ] !== $value ) { + return false; + } + } + return true; + } + + /** + * Create or update an order shipping method + * + * @since 2.2 + * @param \WC_Order $order + * @param array $shipping item data + * @param string $action 'create' to add shipping or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_shipping( $order, $shipping, $action ) { + + // total must be a positive float + if ( isset( $shipping['total'] ) && floatval( $shipping['total'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_total', __( 'Shipping total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // method ID is required + if ( ! isset( $shipping['method_id'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + + $rate = new WC_Shipping_Rate( $shipping['method_id'], isset( $shipping['method_title'] ) ? $shipping['method_title'] : '', isset( $shipping['total'] ) ? floatval( $shipping['total'] ) : 0, array(), $shipping['method_id'] ); + $item = new WC_Order_Item_Shipping(); + $item->set_order_id( $order->get_id() ); + $item->set_shipping_rate( $rate ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Shipping( $shipping['id'] ); + + if ( isset( $shipping['method_id'] ) ) { + $item->set_method_id( $shipping['method_id'] ); + } + + if ( isset( $shipping['method_title'] ) ) { + $item->set_method_title( $shipping['method_title'] ); + } + + if ( isset( $shipping['total'] ) ) { + $item->set_total( floatval( $shipping['total'] ) ); + } + + $shipping_id = $item->save(); + + if ( ! $shipping_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_shipping', __( 'Cannot update shipping method, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order fee + * + * @since 2.2 + * @param \WC_Order $order + * @param array $fee item data + * @param string $action 'create' to add fee or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_fee( $order, $fee, $action ) { + + if ( 'create' === $action ) { + + // fee title is required + if ( ! isset( $fee['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee title is required', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Fee(); + $item->set_order_id( $order->get_id() ); + $item->set_name( wc_clean( $fee['title'] ) ); + $item->set_total( isset( $fee['total'] ) ? floatval( $fee['total'] ) : 0 ); + + // if taxable, tax class and total are required + if ( ! empty( $fee['taxable'] ) ) { + if ( ! isset( $fee['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_fee_item', __( 'Fee tax class is required when fee is taxable.', 'woocommerce' ), 400 ); + } + + $item->set_tax_status( 'taxable' ); + $item->set_tax_class( $fee['tax_class'] ); + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( isset( $fee['total_tax'] ) ? wc_format_refund_total( $fee['total_tax'] ) : 0 ); + } + + if ( isset( $fee['tax_data'] ) ) { + $item->set_total_tax( wc_format_refund_total( array_sum( $fee['tax_data'] ) ) ); + $item->set_taxes( array_map( 'wc_format_refund_total', $fee['tax_data'] ) ); + } + } + + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Fee( $fee['id'] ); + + if ( isset( $fee['title'] ) ) { + $item->set_name( wc_clean( $fee['title'] ) ); + } + + if ( isset( $fee['tax_class'] ) ) { + $item->set_tax_class( $fee['tax_class'] ); + } + + if ( isset( $fee['total'] ) ) { + $item->set_total( floatval( $fee['total'] ) ); + } + + if ( isset( $fee['total_tax'] ) ) { + $item->set_total_tax( floatval( $fee['total_tax'] ) ); + } + + $fee_id = $item->save(); + + if ( ! $fee_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_fee', __( 'Cannot update fee, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Create or update an order coupon + * + * @since 2.2 + * @param \WC_Order $order + * @param array $coupon item data + * @param string $action 'create' to add coupon or 'update' to update it + * @throws WC_API_Exception invalid data, server error + */ + protected function set_coupon( $order, $coupon, $action ) { + + // coupon amount must be positive float + if ( isset( $coupon['amount'] ) && floatval( $coupon['amount'] ) < 0 ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_total', __( 'Coupon discount total must be a positive amount.', 'woocommerce' ), 400 ); + } + + if ( 'create' === $action ) { + + // coupon code is required + if ( empty( $coupon['code'] ) ) { + throw new WC_API_Exception( 'woocommerce_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $item = new WC_Order_Item_Coupon(); + $item->set_props( array( + 'code' => $coupon['code'], + 'discount' => isset( $coupon['amount'] ) ? floatval( $coupon['amount'] ) : 0, + 'discount_tax' => 0, + 'order_id' => $order->get_id(), + ) ); + $order->add_item( $item ); + } else { + + $item = new WC_Order_Item_Coupon( $coupon['id'] ); + + if ( isset( $coupon['code'] ) ) { + $item->set_code( $coupon['code'] ); + } + + if ( isset( $coupon['amount'] ) ) { + $item->set_discount( floatval( $coupon['amount'] ) ); + } + + $coupon_id = $item->save(); + + if ( ! $coupon_id ) { + throw new WC_API_Exception( 'woocommerce_cannot_update_order_coupon', __( 'Cannot update coupon, try again.', 'woocommerce' ), 500 ); + } + } + } + + /** + * Get the admin order notes for an order + * + * @since 2.1 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_notes( $order_id, $fields = null ) { + + // ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $args = array( + 'post_id' => $order_id, + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $order_notes = array(); + + foreach ( $notes as $note ) { + + $order_notes[] = current( $this->get_order_note( $order_id, $note->comment_ID, $fields ) ); + } + + return array( 'order_notes' => apply_filters( 'woocommerce_api_order_notes_response', $order_notes, $order_id, $fields, $notes, $this->server ) ); + } + + /** + * Get an order note for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param string $id order note ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_order_note( $order_id, $id, $fields = null ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $order_note = array( + 'id' => $note->comment_ID, + 'created_at' => $this->server->format_datetime( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + return array( 'order_note' => apply_filters( 'woocommerce_api_order_note_response', $order_note, $id, $fields, $note, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order note for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @return WP_Error|array error or created note response data + */ + public function create_order_note( $order_id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_note', __( 'You do not have permission to create order notes', 'woocommerce' ), 401 ); + } + + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + $data = apply_filters( 'woocommerce_api_create_order_note_data', $data, $order_id, $this ); + + // note content is required + if ( ! isset( $data['note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note', __( 'Order note is required', 'woocommerce' ), 400 ); + } + + $is_customer_note = ( isset( $data['customer_note'] ) && true === $data['customer_note'] ); + + // create the note + $note_id = $order->add_order_note( $data['note'], $is_customer_note ); + + if ( ! $note_id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), 500 ); + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_note', $note_id, $order_id, $this ); + + return $this->get_order_note( $order->get_id(), $note_id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit the order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @param array $data parsed request data + * @return WP_Error|array error or edited note response data + */ + public function edit_order_note( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_note'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_note_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_note' ), 400 ); + } + + $data = $data['order_note']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $order = wc_get_order( $order_id ); + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order->get_id() ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_note_data', $data, $note->comment_ID, $order->get_id(), $this ); + + // Note content + if ( isset( $data['note'] ) ) { + + wp_update_comment( + array( + 'comment_ID' => $note->comment_ID, + 'comment_content' => $data['note'], + ) + ); + } + + // Customer note + if ( isset( $data['customer_note'] ) ) { + + update_comment_meta( $note->comment_ID, 'is_customer_note', true === $data['customer_note'] ? 1 : 0 ); + } + + do_action( 'woocommerce_api_edit_order_note', $note->comment_ID, $order->get_id(), $this ); + + return $this->get_order_note( $order->get_id(), $note->comment_ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order note + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id note ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_note( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate note ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'Invalid order note ID', 'woocommerce' ), 400 ); + } + + // Ensure note ID is valid + $note = get_comment( $id ); + + if ( is_null( $note ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'An order note with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + // Ensure note ID is associated with given order + if ( $note->comment_post_ID != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_note_id', __( 'The order note ID provided is not associated with the order', 'woocommerce' ), 400 ); + } + + // Force delete since trashed order notes could not be managed through comments list table + $result = wc_delete_order_note( $note->comment_ID ); + + if ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_order_note', __( 'This order note cannot be deleted', 'woocommerce' ), 500 ); + } + + do_action( 'woocommerce_api_delete_order_note', $note->comment_ID, $order_id, $this ); + + return array( 'message' => __( 'Permanently deleted order note', 'woocommerce' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the order refunds for an order + * + * @since 2.2 + * @param string $order_id order ID + * @param string|null $fields fields to include in response + * @return array|WP_Error + */ + public function get_order_refunds( $order_id, $fields = null ) { + + // Ensure ID is valid order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $refund_items = wc_get_orders( array( + 'type' => 'shop_order_refund', + 'parent' => $order_id, + 'limit' => -1, + 'return' => 'ids', + ) ); + $order_refunds = array(); + + foreach ( $refund_items as $refund_id ) { + $order_refunds[] = current( $this->get_order_refund( $order_id, $refund_id, $fields ) ); + } + + return array( 'order_refunds' => apply_filters( 'woocommerce_api_order_refunds_response', $order_refunds, $order_id, $fields, $refund_items, $this ) ); + } + + /** + * Get an order refund for the given order ID and ID + * + * @since 2.2 + * + * @param string $order_id order ID + * @param int $id + * @param string|null $fields fields to limit response to + * @param array $filter + * + * @return array|WP_Error + */ + public function get_order_refund( $order_id, $id, $fields = null, $filter = array() ) { + try { + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'read' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + $order = wc_get_order( $order_id ); + $refund = wc_get_order( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + $line_items = array(); + + // Add line items + foreach ( $refund->get_items( 'line_item' ) as $item_id => $item ) { + $product = $item->get_product(); + $hideprefix = ( isset( $filter['all_item_meta'] ) && 'true' === $filter['all_item_meta'] ) ? null : '_'; + $item_meta = $item->get_formatted_meta_data( $hideprefix ); + + foreach ( $item_meta as $key => $values ) { + $item_meta[ $key ]->label = $values->display_key; + unset( $item_meta[ $key ]->display_key ); + unset( $item_meta[ $key ]->display_value ); + } + + $line_items[] = array( + 'id' => $item_id, + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item ), 2 ), + 'subtotal_tax' => wc_format_decimal( $item->get_subtotal_tax(), 2 ), + 'total' => wc_format_decimal( $order->get_line_total( $item ), 2 ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $item ), 2 ), + 'price' => wc_format_decimal( $order->get_item_total( $item ), 2 ), + 'quantity' => $item->get_quantity(), + 'tax_class' => $item->get_tax_class(), + 'name' => $item->get_name(), + 'product_id' => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), + 'sku' => is_object( $product ) ? $product->get_sku() : null, + 'meta' => array_values( $item_meta ), + 'refunded_item_id' => (int) $item->get_meta( 'refunded_item_id' ), + ); + } + + $order_refund = array( + 'id' => $refund->get_id(), + 'created_at' => $this->server->format_datetime( $refund->get_date_created() ? $refund->get_date_created()->getTimestamp() : 0, false, false ), + 'amount' => wc_format_decimal( $refund->get_amount(), 2 ), + 'reason' => $refund->get_reason(), + 'line_items' => $line_items, + ); + + return array( 'order_refund' => apply_filters( 'woocommerce_api_order_refund_response', $order_refund, $id, $fields, $refund, $order_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new order refund for the given order + * + * @since 2.2 + * @param string $order_id order ID + * @param array $data raw request data + * @param bool $api_refund do refund using a payment gateway API + * @return WP_Error|array error or created refund response data + */ + public function create_order_refund( $order_id, $data, $api_refund = true ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Permission check + if ( ! current_user_can( 'publish_shop_orders' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_order_refund', __( 'You do not have permission to create order refunds', 'woocommerce' ), 401 ); + } + + $order_id = absint( $order_id ); + + if ( empty( $order_id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_id', __( 'Order ID is invalid', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_create_order_refund_data', $data, $order_id, $this ); + + // Refund amount is required + if ( ! isset( $data['amount'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount is required.', 'woocommerce' ), 400 ); + } elseif ( 0 > $data['amount'] ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund', __( 'Refund amount must be positive.', 'woocommerce' ), 400 ); + } + + $data['order_id'] = $order_id; + $data['refund_id'] = 0; + + // Create the refund + $refund = wc_create_refund( $data ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + // Refund via API + if ( $api_refund ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + } + + $order = wc_get_order( $order_id ); + + if ( isset( $payment_gateways[ $order->get_payment_method() ] ) && $payment_gateways[ $order->get_payment_method() ]->supports( 'refunds' ) ) { + $result = $payment_gateways[ $order->get_payment_method() ]->process_refund( $order_id, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + return $result; + } elseif ( ! $result ) { + throw new WC_API_Exception( 'woocommerce_api_create_order_refund_api_failed', __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ), 500 ); + } + } + } + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_order_refund', $refund->get_id(), $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit an order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @param array $data parsed request data + * @return WP_Error|array error or edited refund response data + */ + public function edit_order_refund( $order_id, $id, $data ) { + try { + if ( ! isset( $data['order_refund'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_order_refund_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'order_refund' ), 400 ); + } + + $data = $data['order_refund']; + + // Validate order ID + $order_id = $this->validate_request( $order_id, $this->post_type, 'edit' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure order ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + $data = apply_filters( 'woocommerce_api_edit_order_refund_data', $data, $refund->ID, $order_id, $this ); + + // Update reason + if ( isset( $data['reason'] ) ) { + $updated_refund = wp_update_post( array( 'ID' => $refund->ID, 'post_excerpt' => $data['reason'] ) ); + + if ( is_wp_error( $updated_refund ) ) { + return $updated_refund; + } + } + + // Update refund amount + if ( isset( $data['amount'] ) && 0 < $data['amount'] ) { + update_post_meta( $refund->ID, '_refund_amount', wc_format_decimal( $data['amount'] ) ); + } + + do_action( 'woocommerce_api_edit_order_refund', $refund->ID, $order_id, $this ); + + return $this->get_order_refund( $order_id, $refund->ID ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete order refund + * + * @since 2.2 + * @param string $order_id order ID + * @param string $id refund ID + * @return WP_Error|array error or deleted message + */ + public function delete_order_refund( $order_id, $id ) { + try { + $order_id = $this->validate_request( $order_id, $this->post_type, 'delete' ); + + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + // Validate refund ID + $id = absint( $id ); + + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 400 ); + } + + // Ensure refund ID is valid + $refund = get_post( $id ); + + if ( ! $refund ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'An order refund with the provided ID could not be found.', 'woocommerce' ), 404 ); + } + + // Ensure refund ID is associated with given order + if ( $refund->post_parent != $order_id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_order_refund_id', __( 'The order refund ID provided is not associated with the order.', 'woocommerce' ), 400 ); + } + + wc_delete_shop_order_transients( $order_id ); + + do_action( 'woocommerce_api_delete_order_refund', $refund->ID, $order_id, $this ); + + return $this->delete( $refund->ID, 'refund', true ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Bulk update or insert orders + * Accepts an array with orders in the formats supported by + * WC_API_Orders->create_order() and WC_API_Orders->edit_order() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['orders'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_orders_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'orders' ), 400 ); + } + + $data = $data['orders']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'orders' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_orders_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $orders = array(); + + foreach ( $data as $_order ) { + $order_id = 0; + + // Try to get the order ID + if ( isset( $_order['id'] ) ) { + $order_id = intval( $_order['id'] ); + } + + if ( $order_id ) { + + // Order exists / edit order + $edit = $this->edit_order( $order_id, array( 'order' => $_order ) ); + + if ( is_wp_error( $edit ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $orders[] = $edit['order']; + } + } else { + + // Order don't exists / create order + $new = $this->create_order( array( 'order' => $_order ) ); + + if ( is_wp_error( $new ) ) { + $orders[] = array( + 'id' => $order_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $orders[] = $new['order']; + } + } + } + + return array( 'orders' => apply_filters( 'woocommerce_api_orders_bulk_response', $orders, $this ) ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => 400 ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-products.php b/includes/legacy/api/v3/class-wc-api-products.php new file mode 100644 index 0000000..cb224ff --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-products.php @@ -0,0 +1,3310 @@ + + * GET /products//reviews + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /products + $routes[ $this->base ] = array( + array( array( $this, 'get_products' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /products/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_products_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /products/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_product' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product' ), WC_API_Server::DELETABLE ), + ); + + # GET /products//reviews + $routes[ $this->base . '/(?P\d+)/reviews' ] = array( + array( array( $this, 'get_product_reviews' ), WC_API_Server::READABLE ), + ); + + # GET /products//orders + $routes[ $this->base . '/(?P\d+)/orders' ] = array( + array( array( $this, 'get_product_orders' ), WC_API_Server::READABLE ), + ); + + # GET/POST /products/categories + $routes[ $this->base . '/categories' ] = array( + array( array( $this, 'get_product_categories' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_category' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/categories/ + $routes[ $this->base . '/categories/(?P\d+)' ] = array( + array( array( $this, 'get_product_category' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_category' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_category' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/tags + $routes[ $this->base . '/tags' ] = array( + array( array( $this, 'get_product_tags' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_tag' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/tags/ + $routes[ $this->base . '/tags/(?P\d+)' ] = array( + array( array( $this, 'get_product_tag' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_tag' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_tag' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/shipping_classes + $routes[ $this->base . '/shipping_classes' ] = array( + array( array( $this, 'get_product_shipping_classes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_shipping_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/shipping_classes/ + $routes[ $this->base . '/shipping_classes/(?P\d+)' ] = array( + array( array( $this, 'get_product_shipping_class' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_shipping_class' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_shipping_class' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/attributes + $routes[ $this->base . '/attributes' ] = array( + array( array( $this, 'get_product_attributes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/attributes/ + $routes[ $this->base . '/attributes/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute' ), WC_API_Server::DELETABLE ), + ); + + # GET/POST /products/attributes//terms + $routes[ $this->base . '/attributes/(?P\d+)/terms' ] = array( + array( array( $this, 'get_product_attribute_terms' ), WC_API_Server::READABLE ), + array( array( $this, 'create_product_attribute_term' ), WC_API_SERVER::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET/PUT/DELETE /products/attributes//terms/ + $routes[ $this->base . '/attributes/(?P\d+)/terms/(?P\d+)' ] = array( + array( array( $this, 'get_product_attribute_term' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_product_attribute_term' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + array( array( $this, 'delete_product_attribute_term' ), WC_API_Server::DELETABLE ), + ); + + # POST|PUT /products/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all products + * + * @since 2.1 + * @param string $fields + * @param string $type + * @param array $filter + * @param int $page + * @return array + */ + public function get_products( $fields = null, $type = null, $filter = array(), $page = 1 ) { + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $filter['page'] = $page; + + $query = $this->query_products( $filter ); + + $products = array(); + + foreach ( $query->posts as $product_id ) { + + if ( ! $this->is_readable( $product_id ) ) { + continue; + } + + $products[] = current( $this->get_product( $product_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query ); + + return array( 'products' => $products ); + } + + /** + * Get the product for the given ID + * + * @since 2.1 + * @param int $id the product ID + * @param string $fields + * @return array|WP_Error + */ + public function get_product( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + // add data that applies to every product type + $product_data = $this->get_product_data( $product ); + + // add variations to variable products + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $product_data['variations'] = $this->get_variation_data( $product ); + } + + // add the parent product data to an individual variation + if ( $product->is_type( 'variation' ) && $product->get_parent_id() ) { + $product_data['parent'] = $this->get_product_data( $product->get_parent_id() ); + } + + // Add grouped products data + if ( $product->is_type( 'grouped' ) && $product->has_child() ) { + $product_data['grouped_products'] = $this->get_grouped_products_data( $product ); + } + + if ( $product->is_type( 'simple' ) ) { + $parent_id = $product->get_parent_id(); + if ( ! empty( $parent_id ) ) { + $_product = wc_get_product( $parent_id ); + $product_data['parent'] = $this->get_product_data( $_product ); + } + } + + return array( 'product' => apply_filters( 'woocommerce_api_product_response', $product_data, $product, $fields, $this->server ) ); + } + + /** + * Get the total number of products + * + * @since 2.1 + * + * @param string $type + * @param array $filter + * + * @return array|WP_Error + */ + public function get_products_count( $type = null, $filter = array() ) { + try { + if ( ! current_user_can( 'read_private_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_products_count', __( 'You do not have permission to read the products count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $type ) ) { + $filter['type'] = $type; + } + + $query = $this->query_products( $filter ); + + return array( 'count' => (int) $query->found_posts ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product. + * + * @since 2.2 + * + * @param array $data posted data + * + * @return array|WP_Error + */ + public function create_product( $data ) { + $id = 0; + + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + // Check permissions. + if ( ! current_user_can( 'publish_products' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product', __( 'You do not have permission to create products', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_data', $data, $this ); + + // Check if product title is specified. + if ( ! isset( $data['title'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_title', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'title' ), 400 ); + } + + // Check product type. + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'simple'; + } + + // Set visible visibility when not sent. + if ( ! isset( $data['catalog_visibility'] ) ) { + $data['catalog_visibility'] = 'visible'; + } + + // Validate the product type. + if ( ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Enable description html tags. + $post_content = isset( $data['description'] ) ? wc_clean( $data['description'] ) : ''; + if ( $post_content && isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) { + + $post_content = wp_filter_post_kses( $data['description'] ); + } + + // Enable short description html tags. + $post_excerpt = isset( $data['short_description'] ) ? wc_clean( $data['short_description'] ) : ''; + if ( $post_excerpt && isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) { + $post_excerpt = wp_filter_post_kses( $data['short_description'] ); + } + + $classname = WC_Product_Factory::get_classname_from_product_type( $data['type'] ); + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + $product = new $classname(); + + $product->set_name( wc_clean( $data['title'] ) ); + $product->set_status( isset( $data['status'] ) ? wc_clean( $data['status'] ) : 'publish' ); + $product->set_short_description( isset( $data['short_description'] ) ? $post_excerpt : '' ); + $product->set_description( isset( $data['description'] ) ? $post_content : '' ); + $product->set_menu_order( isset( $data['menu_order'] ) ? intval( $data['menu_order'] ) : 0 ); + + if ( ! empty( $data['name'] ) ) { + $product->set_slug( sanitize_title( $data['name'] ) ); + } + + // Attempts to create the new product. + $product->save(); + $id = $product->get_id(); + + // Checks for an error in the product creation. + if ( 0 >= $id ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product', $id->get_error_message(), 400 ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + $product->save(); + + // Save variations. + if ( isset( $data['type'] ) && 'variable' == $data['type'] && isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } + + do_action( 'woocommerce_api_create_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + $this->server->send_status( 201 ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + $this->clear_product( $id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product + * + * @since 2.2 + * + * @param int $id the product ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product( $id, $data ) { + try { + if ( ! isset( $data['product'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product' ), 400 ); + } + + $data = $data['product']; + + $id = $this->validate_request( $id, 'product', 'edit' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + $data = apply_filters( 'woocommerce_api_edit_product_data', $data, $this ); + + // Product title. + if ( isset( $data['title'] ) ) { + $product->set_name( wc_clean( $data['title'] ) ); + } + + // Product name (slug). + if ( isset( $data['name'] ) ) { + $product->set_slug( wc_clean( $data['name'] ) ); + } + + // Product status. + if ( isset( $data['status'] ) ) { + $product->set_status( wc_clean( $data['status'] ) ); + } + + // Product short description. + if ( isset( $data['short_description'] ) ) { + // Enable short description html tags. + $post_excerpt = ( isset( $data['enable_html_short_description'] ) && true === $data['enable_html_short_description'] ) ? wp_filter_post_kses( $data['short_description'] ) : wc_clean( $data['short_description'] ); + $product->set_short_description( $post_excerpt ); + } + + // Product description. + if ( isset( $data['description'] ) ) { + // Enable description html tags. + $post_content = ( isset( $data['enable_html_description'] ) && true === $data['enable_html_description'] ) ? wp_filter_post_kses( $data['description'] ) : wc_clean( $data['description'] ); + $product->set_description( $post_content ); + } + + // Validate the product type. + if ( isset( $data['type'] ) && ! in_array( wc_clean( $data['type'] ), array_keys( wc_get_product_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_type', sprintf( __( 'Invalid product type - the product type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_product_types() ) ) ), 400 ); + } + + // Menu order. + if ( isset( $data['menu_order'] ) ) { + $product->set_menu_order( intval( $data['menu_order'] ) ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $data['images'] ) ) { + $product = $this->save_product_images( $product, $data['images'] ); + } + + // Save product meta fields. + $product = $this->save_product_meta( $product, $data ); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $data['variations'] ) && is_array( $data['variations'] ) ) { + $this->save_variations( $product, $data ); + } else { + // Just sync variations. + $product = WC_Product_Variable::sync( $product, false ); + } + } + + $product->save(); + + do_action( 'woocommerce_api_edit_product', $id, $data ); + + // Clear cache/transients. + wc_delete_product_transients( $id ); + + return $this->get_product( $id ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product. + * + * @since 2.2 + * + * @param int $id the product ID. + * @param bool $force true to permanently delete order, false to move to trash. + * + * @return array|WP_Error + */ + public function delete_product( $id, $force = false ) { + + $id = $this->validate_request( $id, 'product', 'delete' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $product = wc_get_product( $id ); + + do_action( 'woocommerce_api_delete_product', $id, $this ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $product->delete( true ); + $result = ! ( $product->get_id() > 0 ); + } else { + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_api_cannot_delete_product', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product' ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), 'product' ) ); + } else { + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product' ) ); + } + } + + /** + * Get the reviews for a product + * + * @since 2.1 + * @param int $id the product ID to get reviews for + * @param string $fields fields to include in response + * @return array|WP_Error + */ + public function get_product_reviews( $id, $fields = null ) { + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $comments = get_approved_comments( $id ); + $reviews = array(); + + foreach ( $comments as $comment ) { + + $reviews[] = array( + 'id' => intval( $comment->comment_ID ), + 'created_at' => $this->server->format_datetime( $comment->comment_date_gmt ), + 'review' => $comment->comment_content, + 'rating' => get_comment_meta( $comment->comment_ID, 'rating', true ), + 'reviewer_name' => $comment->comment_author, + 'reviewer_email' => $comment->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $comment->comment_ID ), + ); + } + + return array( 'product_reviews' => apply_filters( 'woocommerce_api_product_reviews_response', $reviews, $id, $fields, $comments, $this->server ) ); + } + + /** + * Get the orders for a product + * + * @since 2.4.0 + * @param int $id the product ID to get orders for + * @param string fields fields to retrieve + * @param array $filter filters to include in response + * @param string $status the order status to retrieve + * @param $page $page page to retrieve + * @return array|WP_Error + */ + public function get_product_orders( $id, $fields = null, $filter = array(), $status = null, $page = 1 ) { + global $wpdb; + + $id = $this->validate_request( $id, 'product', 'read' ); + + if ( is_wp_error( $id ) ) { + return $id; + } + + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $id ) ); + + if ( empty( $order_ids ) ) { + return array( 'orders' => array() ); + } + + $filter = array_merge( $filter, array( + 'in' => implode( ',', $order_ids ), + ) ); + + $orders = WC()->api->WC_API_Orders->get_orders( $fields, $filter, $status, $page ); + + return array( 'orders' => apply_filters( 'woocommerce_api_product_orders_response', $orders['orders'], $id, $filter, $fields, $this->server ) ); + } + + /** + * Get a listing of product categories + * + * @since 2.2 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_categories( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $product_categories = array(); + + $terms = get_terms( 'product_cat', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_categories[] = current( $this->get_product_category( $term_id, $fields ) ); + } + + return array( 'product_categories' => apply_filters( 'woocommerce_api_product_categories_response', $product_categories, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product category for the given ID + * + * @since 2.2 + * + * @param string $id product category term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_category( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'Invalid product category ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_categories', __( 'You do not have permission to read product categories', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_cat' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_id', __( 'A product category with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + // Get category display type + $display_type = get_term_meta( $term_id, 'display_type', true ); + + // Get category image + $image = ''; + if ( $image_id = get_term_meta( $term_id, 'thumbnail_id', true ) ) { + $image = wp_get_attachment_url( $image_id ); + } + + $product_category = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => $image ? esc_url( $image ) : '', + 'count' => intval( $term->count ), + ); + + return array( 'product_category' => apply_filters( 'woocommerce_api_product_category_response', $product_category, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product category. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product category if succeed, otherwise WP_Error + * will be returned + */ + public function create_product_category( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_category'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_category_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_category' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_category', __( 'You do not have permission to create product categories', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + 'parent' => 0, + 'display' => 'default', + 'image' => '', + ); + + $data = wp_parse_args( $data['product_category'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_category_data', $data, $this ); + + // Check parent. + $data['parent'] = absint( $data['parent'] ); + if ( $data['parent'] ) { + $parent = get_term_by( 'id', $data['parent'], 'product_cat' ); + if ( ! $parent ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_category_parent', __( 'Product category parent is invalid', 'woocommerce' ), 400 ); + } + } + + // If value of image is numeric, assume value as image_id. + $image = $data['image']; + $image_id = 0; + if ( is_numeric( $image ) ) { + $image_id = absint( $image ); + } elseif ( ! empty( $image ) ) { + $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); + $image_id = $this->set_product_category_image_as_attachment( $upload ); + } + + $insert = wp_insert_term( $data['name'], 'product_cat', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_category', $insert->get_error_message(), 400 ); + } + + $id = $insert['term_id']; + + update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_term_meta( $id, 'thumbnail_id', $image_id ); + } + + do_action( 'woocommerce_api_create_product_category', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_category( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product category. + * + * @since 2.5.0 + * @param int $id Product category term ID + * @param array $data Posted data + * @return array|WP_Error Product category if succeed, otherwise WP_Error + * will be returned + */ + public function edit_product_category( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_category'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_category', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_category' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_category']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_category', __( 'You do not have permission to edit product categories', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_category_data', $data, $this ); + $category = $this->get_product_category( $id ); + + if ( is_wp_error( $category ) ) { + return $category; + } + + if ( isset( $data['image'] ) ) { + $image_id = 0; + + // If value of image is numeric, assume value as image_id. + $image = $data['image']; + if ( is_numeric( $image ) ) { + $image_id = absint( $image ); + } elseif ( ! empty( $image ) ) { + $upload = $this->upload_product_category_image( esc_url_raw( $image ) ); + $image_id = $this->set_product_category_image_as_attachment( $upload ); + } + + // In case client supplies invalid image or wants to unset category image. + if ( ! wp_attachment_is_image( $image_id ) ) { + $image_id = ''; + } + } + + $update = wp_update_term( $id, 'product_cat', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_catgory', __( 'Could not edit the category', 'woocommerce' ), 400 ); + } + + if ( ! empty( $data['display'] ) ) { + update_term_meta( $id, 'display_type', 'default' === $data['display'] ? '' : sanitize_text_field( $data['display'] ) ); + } + + if ( isset( $image_id ) ) { + update_term_meta( $id, 'thumbnail_id', $image_id ); + } + + do_action( 'woocommerce_api_edit_product_category', $id, $data ); + + return $this->get_product_category( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product category. + * + * @since 2.5.0 + * @param int $id Product category term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_category( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_category', __( 'You do not have permission to delete product category', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_cat' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_category', __( 'Could not delete the category', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_category', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_category' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product tags. + * + * @since 2.5.0 + * + * @param string|null $fields Fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_tags( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); + } + + $product_tags = array(); + + $terms = get_terms( 'product_tag', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_tags[] = current( $this->get_product_tag( $term_id, $fields ) ); + } + + return array( 'product_tags' => apply_filters( 'woocommerce_api_product_tags_response', $product_tags, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product tag for the given ID. + * + * @since 2.5.0 + * + * @param string $id Product tag term ID + * @param string|null $fields Fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_tag( $id, $fields = null ) { + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'Invalid product tag ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_tags', __( 'You do not have permission to read product tags', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_tag' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_tag_id', __( 'A product tag with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + $tag = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'description' => $term->description, + 'count' => intval( $term->count ), + ); + + return array( 'product_tag' => apply_filters( 'woocommerce_api_product_tag_response', $tag, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product tag. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product tag if succeed, otherwise WP_Error + * will be returned + */ + public function create_product_tag( $data ) { + try { + if ( ! isset( $data['product_tag'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_tag_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_tag' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_tag', __( 'You do not have permission to create product tags', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + ); + + $data = wp_parse_args( $data['product_tag'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_tag_data', $data, $this ); + + $insert = wp_insert_term( $data['name'], 'product_tag', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_tag', $insert->get_error_message(), 400 ); + } + $id = $insert['term_id']; + + do_action( 'woocommerce_api_create_product_tag', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_tag( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product tag. + * + * @since 2.5.0 + * @param int $id Product tag term ID + * @param array $data Posted data + * @return array|WP_Error Product tag if succeed, otherwise WP_Error + * will be returned + */ + public function edit_product_tag( $id, $data ) { + try { + if ( ! isset( $data['product_tag'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_tag', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_tag' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_tag']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_tag', __( 'You do not have permission to edit product tags', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_tag_data', $data, $this ); + $tag = $this->get_product_tag( $id ); + + if ( is_wp_error( $tag ) ) { + return $tag; + } + + $update = wp_update_term( $id, 'product_tag', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_tag', __( 'Could not edit the tag', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_tag', $id, $data ); + + return $this->get_product_tag( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product tag. + * + * @since 2.5.0 + * @param int $id Product tag term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_tag( $id ) { + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_tag', __( 'You do not have permission to delete product tag', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_tag' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_tag', __( 'Could not delete the tag', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_tag', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_tag' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Helper method to get product post objects + * + * @since 2.1 + * @param array $args request arguments for filtering query + * @return WP_Query + */ + private function query_products( $args ) { + + // Set base query arguments + $query_args = array( + 'fields' => 'ids', + 'post_type' => 'product', + 'post_status' => 'publish', + 'meta_query' => array(), + ); + + // Taxonomy query to filter products by type, category, tag, shipping class, and + // attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies_arg_map = array( + 'product_type' => 'type', + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Add attribute taxonomy names into the map. + foreach ( wc_get_attribute_taxonomy_names() as $attribute_name ) { + $taxonomies_arg_map[ $attribute_name ] = $attribute_name; + } + + // Set tax_query for each passed arg. + foreach ( $taxonomies_arg_map as $tax_name => $arg ) { + if ( ! empty( $args[ $arg ] ) ) { + $terms = explode( ',', $args[ $arg ] ); + + $tax_query[] = array( + 'taxonomy' => $tax_name, + 'field' => 'slug', + 'terms' => $terms, + ); + + unset( $args[ $arg ] ); + } + } + + if ( ! empty( $tax_query ) ) { + $query_args['tax_query'] = $tax_query; + } + + // Filter by specific sku + if ( ! empty( $args['sku'] ) ) { + if ( ! is_array( $query_args['meta_query'] ) ) { + $query_args['meta_query'] = array(); + } + + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => $args['sku'], + 'compare' => '=', + ); + + $query_args['post_type'] = array( 'product', 'product_variation' ); + } + + $query_args = $this->merge_query_args( $query_args, $args ); + + return new WP_Query( $query_args ); + } + + /** + * Get standard product data that applies to every product type + * + * @since 2.1 + * @param WC_Product|int $product + * + * @return array + */ + private function get_product_data( $product ) { + if ( is_numeric( $product ) ) { + $product = wc_get_product( $product ); + } + + if ( ! is_a( $product, 'WC_Product' ) ) { + return array(); + } + + return array( + 'title' => $product->get_name(), + 'id' => $product->get_id(), + 'created_at' => $this->server->format_datetime( $product->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $product->get_date_modified(), false, true ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'downloadable' => $product->is_downloadable(), + 'virtual' => $product->is_virtual(), + 'permalink' => $product->get_permalink(), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : null, + 'price_html' => $product->get_price_html(), + 'taxable' => $product->is_taxable(), + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'managing_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'purchaseable' => $product->is_purchasable(), + 'featured' => $product->is_featured(), + 'visible' => $product->is_visible(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'on_sale' => $product->is_on_sale(), + 'product_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'weight' => $product->get_weight() ? $product->get_weight() : null, + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $product->get_shipping_class_id() ) ? $product->get_shipping_class_id() : null, + 'description' => wpautop( do_shortcode( $product->get_description() ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'parent_id' => $product->get_parent_id(), + 'categories' => wc_get_object_terms( $product->get_id(), 'product_cat', 'name' ), + 'tags' => wc_get_object_terms( $product->get_id(), 'product_tag', 'name' ), + 'images' => $this->get_images( $product ), + 'featured_src' => wp_get_attachment_url( get_post_thumbnail_id( $product->get_id() ) ), + 'attributes' => $this->get_attributes( $product ), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'download_type' => 'standard', + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'total_sales' => $product->get_total_sales(), + 'variations' => array(), + 'parent' => array(), + 'grouped_products' => array(), + 'menu_order' => $this->get_product_menu_order( $product ), + ); + } + + /** + * Get product menu order. + * + * @since 2.5.3 + * @param WC_Product $product + * @return int + */ + private function get_product_menu_order( $product ) { + $menu_order = $product->get_menu_order(); + + return apply_filters( 'woocommerce_api_product_menu_order', $menu_order, $product ); + } + + /** + * Get an individual variation's data. + * + * @since 2.1 + * @param WC_Product $product + * @return array + */ + private function get_variation_data( $product ) { + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + $variation = wc_get_product( $child_id ); + + if ( ! $variation || ! $variation->exists() ) { + continue; + } + + $variations[] = array( + 'id' => $variation->get_id(), + 'created_at' => $this->server->format_datetime( $variation->get_date_created(), false, true ), + 'updated_at' => $this->server->format_datetime( $variation->get_date_modified(), false, true ), + 'downloadable' => $variation->is_downloadable(), + 'virtual' => $variation->is_virtual(), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price() ? $variation->get_sale_price() : null, + 'taxable' => $variation->is_taxable(), + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'managing_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'purchaseable' => $variation->is_purchasable(), + 'visible' => $variation->variation_is_visible(), + 'on_sale' => $variation->is_on_sale(), + 'weight' => $variation->get_weight() ? $variation->get_weight() : null, + 'dimensions' => array( + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + 'unit' => get_option( 'woocommerce_dimension_unit' ), + ), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => ( 0 !== $variation->get_shipping_class_id() ) ? $variation->get_shipping_class_id() : null, + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => (int) $product->get_download_limit(), + 'download_expiry' => (int) $product->get_download_expiry(), + ); + } + + return $variations; + } + + /** + * Get grouped products data + * + * @since 2.5.0 + * @param WC_Product $product + * + * @return array + */ + private function get_grouped_products_data( $product ) { + $products = array(); + + foreach ( $product->get_children() as $child_id ) { + $_product = wc_get_product( $child_id ); + + if ( ! $_product || ! $_product->exists() ) { + continue; + } + + $products[] = $this->get_product_data( $_product ); + + } + + return $products; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * + * @param WC_Product $product + * @param WP_REST_Request $request + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + // Update default attributes options setting. + if ( isset( $request['default_attribute'] ) ) { + $request['default_attributes'] = $request['default_attribute']; + } + + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $default_attr_key => $default_attr ) { + if ( ! isset( $default_attr['name'] ) ) { + continue; + } + + $taxonomy = sanitize_title( $default_attr['name'] ); + + if ( isset( $default_attr['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $default_attr['slug'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + + if ( $_attribute['is_variation'] ) { + $value = ''; + + if ( isset( $default_attr['option'] ) ) { + if ( $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters + $value = sanitize_title( trim( stripslashes( $default_attr['option'] ) ) ); + } else { + $value = wc_clean( trim( stripslashes( $default_attr['option'] ) ) ); + } + } + + if ( $value ) { + $default_attributes[ $taxonomy ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Save product meta. + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + * @throws WC_API_Exception + */ + protected function save_product_meta( $product, $data ) { + global $wpdb; + + // Virtual. + if ( isset( $data['virtual'] ) ) { + $product->set_virtual( $data['virtual'] ); + } + + // Tax status. + if ( isset( $data['tax_status'] ) ) { + $product->set_tax_status( wc_clean( $data['tax_status'] ) ); + } + + // Tax Class. + if ( isset( $data['tax_class'] ) ) { + $product->set_tax_class( wc_clean( $data['tax_class'] ) ); + } + + // Catalog Visibility. + if ( isset( $data['catalog_visibility'] ) ) { + $product->set_catalog_visibility( wc_clean( $data['catalog_visibility'] ) ); + } + + // Purchase Note. + if ( isset( $data['purchase_note'] ) ) { + $product->set_purchase_note( wc_clean( $data['purchase_note'] ) ); + } + + // Featured Product. + if ( isset( $data['featured'] ) ) { + $product->set_featured( $data['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $data ); + + // SKU. + if ( isset( $data['sku'] ) ) { + $sku = $product->get_sku(); + $new_sku = wc_clean( $data['sku'] ); + + if ( '' == $new_sku ) { + $product->set_sku( '' ); + } elseif ( $new_sku !== $sku ) { + if ( ! empty( $new_sku ) ) { + $unique_sku = wc_product_has_unique_sku( $product->get_id(), $new_sku ); + if ( ! $unique_sku ) { + throw new WC_API_Exception( 'woocommerce_api_product_sku_already_exists', __( 'The SKU already exists on another product.', 'woocommerce' ), 400 ); + } else { + $product->set_sku( $new_sku ); + } + } else { + $product->set_sku( '' ); + } + } + } + + // Attributes. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + + foreach ( $data['attributes'] as $attribute ) { + $is_taxonomy = 0; + $taxonomy = 0; + + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $attribute_slug = sanitize_title( $attribute['name'] ); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + $attribute_slug = sanitize_title( $attribute['slug'] ); + } + + if ( $taxonomy ) { + $is_taxonomy = 1; + } + + if ( $is_taxonomy ) { + + $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute['name'] ); + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + // Update post terms + if ( taxonomy_exists( $taxonomy ) ) { + wp_set_object_terms( $product->get_id(), $values, $taxonomy ); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $taxonomy ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Array based. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + + // Text based, separate by pipe. + } else { + $values = array_map( 'wc_clean', explode( WC_DELIMITER, $attribute['options'] ) ); + } + + // Custom attribute - Add attribute to array and set the values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute['name'] ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? absint( $attribute['position'] ) : 0 ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + + uasort( $attributes, 'wc_product_attribute_uasort_comparison' ); + + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ) ) ) { + + // Variable and grouped products have no prices. + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + + } else { + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $regular_price = ( '' === $data['regular_price'] ) ? '' : $data['regular_price']; + $product->set_regular_price( $regular_price ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $sale_price = ( '' === $data['sale_price'] ) ? '' : $data['sale_price']; + $product->set_sale_price( $sale_price ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $date_from = $data['sale_price_dates_from']; + } else { + $date_from = $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : ''; + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $date_to = $data['sale_price_dates_to']; + } else { + $date_to = $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : ''; + } + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp', true ) ); + } + + $product->set_date_on_sale_to( $date_to ); + $product->set_date_on_sale_from( $date_from ); + + if ( $product->is_on_sale( 'edit' ) ) { + $product->set_price( $product->get_sale_price( 'edit' ) ); + } else { + $product->set_price( $product->get_regular_price( 'edit' ) ); + } + } + + // Product parent ID for groups. + if ( isset( $data['parent_id'] ) ) { + $product->set_parent_id( absint( $data['parent_id'] ) ); + } + + // Sold Individually. + if ( isset( $data['sold_individually'] ) ) { + $product->set_sold_individually( true === $data['sold_individually'] ? 'yes' : '' ); + } + + // Stock status. + if ( isset( $data['in_stock'] ) ) { + $stock_status = ( true === $data['in_stock'] ) ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + + if ( '' === $stock_status ) { + $stock_status = 'instock'; + } + } + + // Stock Data. + if ( 'yes' == get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $data['managing_stock'] ) ) { + $managing_stock = ( true === $data['managing_stock'] ) ? 'yes' : 'no'; + $product->set_manage_stock( $managing_stock ); + } else { + $managing_stock = $product->get_manage_stock() ? 'yes' : 'no'; + } + + // Backorders. + if ( isset( $data['backorders'] ) ) { + if ( 'notify' === $data['backorders'] ) { + $backorders = 'notify'; + } else { + $backorders = ( true === $data['backorders'] ) ? 'yes' : 'no'; + } + + $product->set_backorders( $backorders ); + } else { + $backorders = $product->get_backorders(); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( 'yes' == $managing_stock ) { + $product->set_backorders( $backorders ); + + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $data['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $data['stock_quantity'] ) ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_backorders( $backorders ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $data['upsell_ids'] ) ) { + $upsells = array(); + $ids = $data['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + + $product->set_upsell_ids( $upsells ); + } else { + $product->set_upsell_ids( array() ); + } + } + + // Cross sells. + if ( isset( $data['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $data['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + + $product->set_cross_sell_ids( $crosssells ); + } else { + $product->set_cross_sell_ids( array() ); + } + } + + // Product categories. + if ( isset( $data['categories'] ) && is_array( $data['categories'] ) ) { + $product->set_category_ids( $data['categories'] ); + } + + // Product tags. + if ( isset( $data['tags'] ) && is_array( $data['tags'] ) ) { + $product->set_tag_ids( $data['tags'] ); + } + + // Downloadable. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = ( true === $data['downloadable'] ) ? 'yes' : 'no'; + $product->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $product->get_downloadable() ? 'yes' : 'no'; + } + + // Downloadable options. + if ( 'yes' == $is_downloadable ) { + + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $product->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $product->set_download_expiry( $data['download_expiry'] ); + } + } + + // Product url. + if ( $product->is_type( 'external' ) ) { + if ( isset( $data['product_url'] ) ) { + $product->set_product_url( $data['product_url'] ); + } + + if ( isset( $data['button_text'] ) ) { + $product->set_button_text( $data['button_text'] ); + } + } + + // Reviews allowed. + if ( isset( $data['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $data['reviews_allowed'] ); + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $data ); + } + + // Do action for product type + do_action( 'woocommerce_api_process_product_meta_' . $product->get_type(), $product->get_id(), $data ); + + return $product; + } + + /** + * Save variations. + * + * @since 2.2 + * + * @param WC_Product $product + * @param array $request + * + * @return bool + * @throws WC_API_Exception + */ + protected function save_variations( $product, $request ) { + global $wpdb; + + $id = $product->get_id(); + $variations = $request['variations']; + $attributes = $product->get_attributes(); + + foreach ( $variations as $menu_order => $data ) { + $variation_id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; + $variation = new WC_Product_Variation( $variation_id ); + + // Create initial name and status. + if ( ! $variation->get_slug() ) { + /* translators: 1: variation id 2: product name */ + $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); + $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); + } + + // Parent ID. + $variation->set_parent_id( $product->get_id() ); + + // Menu order. + $variation->set_menu_order( $menu_order ); + + // Status. + if ( isset( $data['visible'] ) ) { + $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { + $image = current( $data['image'] ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->save_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $is_downloadable = $data['downloadable']; + $variation->set_downloadable( $is_downloadable ); + } else { + $is_downloadable = $variation->get_downloadable(); + } + + // Downloads. + if ( $is_downloadable ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + $manage_stock = (bool) $variation->get_manage_stock(); + if ( isset( $data['managing_stock'] ) ) { + $manage_stock = $data['managing_stock']; + } + $variation->set_manage_stock( $manage_stock ); + + $stock_status = $variation->get_stock_status(); + if ( isset( $data['in_stock'] ) ) { + $stock_status = true === $data['in_stock'] ? 'instock' : 'outofstock'; + } + $variation->set_stock_status( $stock_status ); + + $backorders = $variation->get_backorders(); + if ( isset( $data['backorders'] ) ) { + $backorders = $data['backorders']; + } + $variation->set_backorders( $backorders ); + + if ( $manage_stock ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['sale_price_dates_from'] ) ) { + $variation->set_date_on_sale_from( $data['sale_price_dates_from'] ); + } + + if ( isset( $data['sale_price_dates_to'] ) ) { + $variation->set_date_on_sale_to( $data['sale_price_dates_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Description. + if ( isset( $data['description'] ) ) { + $variation->set_description( wp_kses_post( $data['description'] ) ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $_attributes = array(); + + foreach ( $data['attributes'] as $attribute_key => $attribute ) { + if ( ! isset( $attribute['name'] ) ) { + continue; + } + + $taxonomy = 0; + $_attribute = array(); + + if ( isset( $attribute['slug'] ) ) { + $taxonomy = $this->get_attribute_taxonomy_by_slug( $attribute['slug'] ); + } + + if ( ! $taxonomy ) { + $taxonomy = sanitize_title( $attribute['name'] ); + } + + if ( isset( $attributes[ $taxonomy ] ) ) { + $_attribute = $attributes[ $taxonomy ]; + } + + if ( isset( $_attribute['is_variation'] ) && $_attribute['is_variation'] ) { + $_attribute_key = sanitize_title( $_attribute['name'] ); + + if ( isset( $_attribute['is_taxonomy'] ) && $_attribute['is_taxonomy'] ) { + // Don't use wc_clean as it destroys sanitized characters. + $_attribute_value = isset( $attribute['option'] ) ? sanitize_title( stripslashes( $attribute['option'] ) ) : ''; + } else { + $_attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + } + + $_attributes[ $_attribute_key ] = $_attribute_value; + } + } + + $variation->set_attributes( $_attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_api_save_product_variation', $variation_id, $menu_order, $variation ); + } + + return true; + } + + /** + * Save product shipping data + * + * @since 2.2 + * @param WC_Product $product + * @param array $data + * @return WC_Product + */ + private function save_product_shipping_data( $product, $data ) { + if ( isset( $data['weight'] ) ) { + $product->set_weight( '' === $data['weight'] ? '' : wc_format_decimal( $data['weight'] ) ); + } + + // Product dimensions + if ( isset( $data['dimensions'] ) ) { + // Height + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( '' === $data['dimensions']['height'] ? '' : wc_format_decimal( $data['dimensions']['height'] ) ); + } + + // Width + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( '' === $data['dimensions']['width'] ? '' : wc_format_decimal( $data['dimensions']['width'] ) ); + } + + // Length + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( '' === $data['dimensions']['length'] ? '' : wc_format_decimal( $data['dimensions']['length'] ) ); + } + } + + // Virtual + if ( isset( $data['virtual'] ) ) { + $virtual = ( true === $data['virtual'] ) ? 'yes' : 'no'; + + if ( 'yes' == $virtual ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } + } + + // Shipping class + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files + * + * @since 2.2 + * @param WC_Product $product + * @param array $downloads + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + private function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() does not require a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( isset( $file['url'] ) ) { + $file['file'] = $file['url']; + } + + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Get attribute taxonomy by slug. + * + * @since 2.2 + * @param string $slug + * @return string|null + */ + private function get_attribute_taxonomy_by_slug( $slug ) { + $taxonomy = null; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $key => $tax ) { + if ( $slug == $tax->attribute_name ) { + $taxonomy = 'pa_' . $tax->attribute_name; + + break; + } + } + + return $taxonomy; + } + + /** + * Get the images for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_images( $product ) { + $images = $attachment_ids = array(); + $product_image = $product->get_image_id(); + + // Add featured image. + if ( ! empty( $product_image ) ) { + $attachment_ids[] = $product_image; + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + + $attachment_post = get_post( $attachment_id ); + + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'created_at' => $this->server->format_datetime( $attachment_post->post_date_gmt ), + 'updated_at' => $this->server->format_datetime( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'title' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + + $images[] = array( + 'id' => 0, + 'created_at' => $this->server->format_datetime( time() ), // Default to now. + 'updated_at' => $this->server->format_datetime( time() ), + 'src' => wc_placeholder_img_src(), + 'title' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Save product images. + * + * @since 2.2 + * @param WC_Product $product + * @param array $images + * @throws WC_API_Exception + * @return WC_Product + */ + protected function save_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + if ( isset( $image['position'] ) && 0 == $image['position'] ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $product->set_image_id( $attachment_id ); + } else { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = $this->upload_product_image( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_upload_product_image', $upload->get_error_message(), 400 ); + } + + $attachment_id = $this->set_product_image_as_attachment( $upload, $product->get_id() ); + } + + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) && $attachment_id ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image title if present. + if ( ! empty( $image['title'] ) && $attachment_id ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['title'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Upload image from URL + * + * @since 2.2 + * @param string $image_url + * @return int|WP_Error attachment id + */ + public function upload_product_image( $image_url ) { + return $this->upload_image_from_url( $image_url, 'product_image' ); + } + + /** + * Upload product category image from URL. + * + * @since 2.5.0 + * @param string $image_url + * @return int|WP_Error attachment id + */ + public function upload_product_category_image( $image_url ) { + return $this->upload_image_from_url( $image_url, 'product_category_image' ); + } + + /** + * Upload image from URL. + * + * @throws WC_API_Exception + * + * @since 2.5.0 + * @param string $image_url + * @param string $upload_for + * @return array + */ + protected function upload_image_from_url( $image_url, $upload_for = 'product_image' ) { + $upload = wc_rest_upload_image_from_url( $image_url ); + if ( is_wp_error( $upload ) ) { + throw new WC_API_Exception( 'woocommerce_api_' . $upload_for . '_upload_error', $upload->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_uploaded_image_from_url', $upload, $image_url, $upload_for ); + + return $upload; + } + + /** + * Sets product image as attachment and returns the attachment ID. + * + * @since 2.2 + * @param array $upload + * @param int $id + * @return int + */ + protected function set_product_image_as_attachment( $upload, $id ) { + return $this->set_uploaded_image_as_attachment( $upload, $id ); + } + + /** + * Sets uploaded category image as attachment and returns the attachment ID. + * + * @since 2.5.0 + * @param integer $upload Upload information from wp_upload_bits + * @return int Attachment ID + */ + protected function set_product_category_image_as_attachment( $upload ) { + return $this->set_uploaded_image_as_attachment( $upload ); + } + + /** + * Set uploaded image as attachment. + * + * @since 2.5.0 + * @param array $upload Upload information from wp_upload_bits + * @param int $id Post ID. Default to 0. + * @return int Attachment ID + */ + protected function set_uploaded_image_as_attachment( $upload, $id = 0 ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( $image_meta = @wp_read_image_metadata( $upload['file'] ) ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = wc_clean( $image_meta['title'] ); + } + if ( trim( $image_meta['caption'] ) ) { + $content = wc_clean( $image_meta['caption'] ); + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title, + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; + } + + /** + * Get attribute options. + * + * @param int $product_id + * @param array $attribute + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_attributes( $product ) { + + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + + // variation attributes + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + + // taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_` + $attributes[] = array( + 'name' => wc_attribute_label( str_replace( 'attribute_', '', $attribute_name ), $product ), + 'slug' => str_replace( 'attribute_', '', wc_attribute_taxonomy_slug( $attribute_name ) ), + 'option' => $attribute, + ); + } + } else { + + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'name' => wc_attribute_label( $attribute['name'], $product ), + 'slug' => wc_attribute_taxonomy_slug( $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get the downloads for a product or product variation + * + * @since 2.1 + * @param WC_Product|WC_Product_Variation $product + * @return array + */ + private function get_downloads( $product ) { + + $downloads = array(); + + if ( $product->is_downloadable() ) { + + foreach ( $product->get_downloads() as $file_id => $file ) { + + $downloads[] = array( + 'id' => $file_id, // do not cast as int as this is a hash + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get a listing of product attributes + * + * @since 2.5.0 + * + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attributes( $fields = null ) { + try { + // Permissions check. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $product_attributes = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $attribute_taxonomies as $attribute ) { + $product_attributes[] = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + } + + return array( 'product_attributes' => apply_filters( 'woocommerce_api_product_attributes_response', $product_attributes, $attribute_taxonomies, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute for the given ID + * + * @since 2.5.0 + * + * @param string $id product attribute term ID + * @param string|null $fields fields to limit response to + * + * @return array|WP_Error + */ + public function get_product_attribute( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attributes', __( 'You do not have permission to read product attributes', 'woocommerce' ), 401 ); + } + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $product_attribute = array( + 'id' => intval( $attribute->attribute_id ), + 'name' => $attribute->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $attribute->attribute_name ), + 'type' => $attribute->attribute_type, + 'order_by' => $attribute->attribute_orderby, + 'has_archives' => (bool) $attribute->attribute_public, + ); + + return array( 'product_attribute' => apply_filters( 'woocommerce_api_product_attribute_response', $product_attribute, $id, $fields, $attribute, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Validate attribute data. + * + * @since 2.5.0 + * @param string $name + * @param string $slug + * @param string $type + * @param string $order_by + * @param bool $new_data + * @return bool + * @throws WC_API_Exception + */ + protected function validate_attribute_data( $name, $slug, $type, $order_by, $new_data = true ) { + if ( empty( $name ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + if ( strlen( $slug ) >= 28 ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), 400 ); + } + + // Validate the attribute type + if ( ! in_array( wc_clean( $type ), array_keys( wc_get_attribute_types() ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_type', sprintf( __( 'Invalid product attribute type - the product attribute type must be any of these: %s', 'woocommerce' ), implode( ', ', array_keys( wc_get_attribute_types() ) ) ), 400 ); + } + + // Validate the attribute order by + if ( ! in_array( wc_clean( $order_by ), array( 'menu_order', 'name', 'name_num', 'id' ) ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_order_by', sprintf( __( 'Invalid product attribute order_by type - the product attribute order_by type must be any of these: %s', 'woocommerce' ), implode( ', ', array( 'menu_order', 'name', 'name_num', 'id' ) ) ), 400 ); + } + + return true; + } + + /** + * Create a new product attribute. + * + * @since 2.5.0 + * + * @param array $data Posted data. + * + * @return array|WP_Error + */ + public function create_product_attribute( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $data = $data['product_attribute']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_data', $data, $this ); + + if ( ! isset( $data['name'] ) ) { + $data['name'] = ''; + } + + // Set the attribute slug. + if ( ! isset( $data['slug'] ) ) { + $data['slug'] = wc_sanitize_taxonomy_name( stripslashes( $data['name'] ) ); + } else { + $data['slug'] = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ) ); + } + + // Set attribute type when not sent. + if ( ! isset( $data['type'] ) ) { + $data['type'] = 'select'; + } + + // Set order by when not sent. + if ( ! isset( $data['order_by'] ) ) { + $data['order_by'] = 'menu_order'; + } + + // Validate the attribute data. + $this->validate_attribute_data( $data['name'], $data['slug'], $data['type'], $data['order_by'], true ); + + $insert = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $data['name'], + 'attribute_name' => $data['slug'], + 'attribute_type' => $data['type'], + 'attribute_orderby' => $data['order_by'], + 'attribute_public' => isset( $data['has_archives'] ) && true === $data['has_archives'] ? 1 : 0, + ), + array( '%s', '%s', '%s', '%s', '%d' ) + ); + + // Checks for an error in the product creation. + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $insert->get_error_message(), 400 ); + } + + $id = $wpdb->insert_id; + + do_action( 'woocommerce_api_create_product_attribute', $id, $data ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute. + * + * @since 2.5.0 + * + * @param int $id the attribute ID. + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_data', $data, $this ); + $attribute = $this->get_product_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $attribute_name = isset( $data['name'] ) ? $data['name'] : $attribute['product_attribute']['name']; + $attribute_type = isset( $data['type'] ) ? $data['type'] : $attribute['product_attribute']['type']; + $attribute_order_by = isset( $data['order_by'] ) ? $data['order_by'] : $attribute['product_attribute']['order_by']; + + if ( isset( $data['slug'] ) ) { + $attribute_slug = wc_sanitize_taxonomy_name( stripslashes( $data['slug'] ) ); + } else { + $attribute_slug = $attribute['product_attribute']['slug']; + } + $attribute_slug = preg_replace( '/^pa\_/', '', $attribute_slug ); + + if ( isset( $data['has_archives'] ) ) { + $attribute_public = true === $data['has_archives'] ? 1 : 0; + } else { + $attribute_public = $attribute['product_attribute']['has_archives']; + } + + // Validate the attribute data. + $this->validate_attribute_data( $attribute_name, $attribute_slug, $attribute_type, $attribute_order_by, false ); + + $update = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( + 'attribute_label' => $attribute_name, + 'attribute_name' => $attribute_slug, + 'attribute_type' => $attribute_type, + 'attribute_orderby' => $attribute_order_by, + 'attribute_public' => $attribute_public, + ), + array( 'attribute_id' => $id ), + array( '%s', '%s', '%s', '%s', '%d' ), + array( '%d' ) + ); + + // Checks for an error in the product creation. + if ( false === $update ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute', __( 'Could not edit the attribute', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute', $id, $data ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return $this->get_product_attribute( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute. + * + * @since 2.5.0 + * + * @param int $id the product attribute ID. + * + * @return array|WP_Error + */ + public function delete_product_attribute( $id ) { + global $wpdb; + + try { + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute', __( 'You do not have permission to delete product attributes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + $attribute_name = $wpdb->get_var( $wpdb->prepare( " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_null( $attribute_name ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $deleted = $wpdb->delete( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + array( 'attribute_id' => $id ), + array( '%d' ) + ); + + if ( false === $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute', __( 'Could not delete the attribute', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name( $attribute_name ); + + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + do_action( 'woocommerce_attribute_deleted', $id, $attribute_name, $taxonomy ); + do_action( 'woocommerce_api_delete_product_attribute', $id, $this ); + + // Clear transients. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product attribute terms. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param string|null $fields Fields to limit response to. + * + * @return array|WP_Error + */ + public function get_product_attribute_terms( $attribute_id, $fields = null ) { + try { + // Permissions check. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); + } + + $attribute_id = absint( $attribute_id ); + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); + $attribute_terms = array(); + + foreach ( $terms as $term ) { + $attribute_terms[] = array( + 'id' => $term->term_id, + 'slug' => $term->slug, + 'name' => $term->name, + 'count' => $term->count, + ); + } + + return array( 'product_attribute_terms' => apply_filters( 'woocommerce_api_product_attribute_terms_response', $attribute_terms, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product attribute term for the given ID. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param string $id Product attribute term ID. + * @param string|null $fields Fields to limit response to. + * + * @return array|WP_Error + */ + public function get_product_attribute_term( $attribute_id, $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + $attribute_id = absint( $attribute_id ); + + // Validate ID + if ( empty( $id ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'Invalid product attribute ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_attribute_terms', __( 'You do not have permission to read product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term = get_term( $id, $taxonomy ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_term_id', __( 'A product attribute term with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $attribute_term = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'count' => $term->count, + ); + + return array( 'product_attribute_term' => apply_filters( 'woocommerce_api_product_attribute_response', $attribute_term, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param array $data Posted data. + * + * @return array|WP_Error + */ + public function create_product_attribute_term( $attribute_id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute_term'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); + } + + $data = $data['product_attribute_term']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_attribute', __( 'You do not have permission to create product attributes', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $data = apply_filters( 'woocommerce_api_create_product_attribute_term_data', $data, $this ); + + // Check if attribute term name is specified. + if ( ! isset( $data['name'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + $args = array(); + + // Set the attribute term slug. + if ( isset( $data['slug'] ) ) { + $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); + } + + $term = wp_insert_term( $data['name'], $taxonomy, $args ); + + // Checks for an error in the term creation. + if ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_attribute', $term->get_error_message(), 400 ); + } + + $id = $term['term_id']; + + do_action( 'woocommerce_api_create_product_attribute_term', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_attribute_term( $attribute_id, $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param int $id the attribute ID. + * @param array $data + * + * @return array|WP_Error + */ + public function edit_product_attribute_term( $attribute_id, $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_attribute_term'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_attribute_term_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_attribute_term' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_attribute_term']; + + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_attribute', __( 'You do not have permission to edit product attributes', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_attribute_term_data', $data, $this ); + + $args = array(); + + // Update name. + if ( isset( $data['name'] ) ) { + $args['name'] = wc_clean( wp_unslash( $data['name'] ) ); + } + + // Update slug. + if ( isset( $data['slug'] ) ) { + $args['slug'] = sanitize_title( wp_unslash( $data['slug'] ) ); + } + + $term = wp_update_term( $id, $taxonomy, $args ); + + if ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_attribute_term', $term->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_edit_product_attribute_term', $id, $data ); + + return $this->get_product_attribute_term( $attribute_id, $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product attribute term. + * + * @since 2.5.0 + * + * @param int $attribute_id Attribute ID. + * @param int $id the product attribute ID. + * + * @return array|WP_Error + */ + public function delete_product_attribute_term( $attribute_id, $id ) { + global $wpdb; + + try { + // Check permissions. + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_attribute_term', __( 'You do not have permission to delete product attribute terms', 'woocommerce' ), 401 ); + } + + $taxonomy = wc_attribute_taxonomy_name_by_id( $attribute_id ); + + if ( ! $taxonomy ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_attribute_id', __( 'A product attribute with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $id = absint( $id ); + $term = wp_delete_term( $id, $taxonomy ); + + if ( ! $term ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), 'product_attribute_term' ), 500 ); + } elseif ( is_wp_error( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_attribute_term', $term->get_error_message(), 400 ); + } + + do_action( 'woocommerce_api_delete_product_attribute_term', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_attribute' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Clear product + * + * @param int $product_id + */ + protected function clear_product( $product_id ) { + if ( ! is_numeric( $product_id ) || 0 >= $product_id ) { + return; + } + + // Delete product attachments + $attachments = get_children( array( + 'post_parent' => $product_id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product + $product = wc_get_product( $product_id ); + $product->delete( true ); + } + + /** + * Bulk update or insert products + * Accepts an array with products in the formats supported by + * WC_API_Products->create_product() and WC_API_Products->edit_product() + * + * @since 2.4.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + + try { + if ( ! isset( $data['products'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_products_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'products' ), 400 ); + } + + $data = $data['products']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'products' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_products_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $products = array(); + + foreach ( $data as $_product ) { + $product_id = 0; + $product_sku = ''; + + // Try to get the product ID + if ( isset( $_product['id'] ) ) { + $product_id = intval( $_product['id'] ); + } + + if ( ! $product_id && isset( $_product['sku'] ) ) { + $product_sku = wc_clean( $_product['sku'] ); + $product_id = wc_get_product_id_by_sku( $product_sku ); + } + + if ( $product_id ) { + + // Product exists / edit product + $edit = $this->edit_product( $product_id, array( 'product' => $_product ) ); + + if ( is_wp_error( $edit ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $products[] = $edit['product']; + } + } else { + + // Product don't exists / create product + $new = $this->create_product( array( 'product' => $_product ) ); + + if ( is_wp_error( $new ) ) { + $products[] = array( + 'id' => $product_id, + 'sku' => $product_sku, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $products[] = $new['product']; + } + } + } + + return array( 'products' => apply_filters( 'woocommerce_api_products_bulk_response', $products, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a listing of product shipping classes. + * + * @since 2.5.0 + * @param string|null $fields Fields to limit response to + * @return array|WP_Error List of product shipping classes if succeed, + * otherwise WP_Error will be returned + */ + public function get_product_shipping_classes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); + } + + $product_shipping_classes = array(); + + $terms = get_terms( 'product_shipping_class', array( 'hide_empty' => false, 'fields' => 'ids' ) ); + + foreach ( $terms as $term_id ) { + $product_shipping_classes[] = current( $this->get_product_shipping_class( $term_id, $fields ) ); + } + + return array( 'product_shipping_classes' => apply_filters( 'woocommerce_api_product_shipping_classes_response', $product_shipping_classes, $terms, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the product shipping class for the given ID. + * + * @since 2.5.0 + * @param string $id Product shipping class term ID + * @param string|null $fields Fields to limit response to + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function get_product_shipping_class( $id, $fields = null ) { + try { + $id = absint( $id ); + if ( ! $id ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'Invalid product shipping class ID', 'woocommerce' ), 400 ); + } + + // Permissions check + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_product_shipping_classes', __( 'You do not have permission to read product shipping classes', 'woocommerce' ), 401 ); + } + + $term = get_term( $id, 'product_shipping_class' ); + + if ( is_wp_error( $term ) || is_null( $term ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_id', __( 'A product shipping class with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $term_id = intval( $term->term_id ); + + $product_shipping_class = array( + 'id' => $term_id, + 'name' => $term->name, + 'slug' => $term->slug, + 'parent' => $term->parent, + 'description' => $term->description, + 'count' => intval( $term->count ), + ); + + return array( 'product_shipping_class' => apply_filters( 'woocommerce_api_product_shipping_class_response', $product_shipping_class, $id, $fields, $term, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a new product shipping class. + * + * @since 2.5.0 + * @param array $data Posted data + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function create_product_shipping_class( $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_shipping_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_product_shipping_class', __( 'You do not have permission to create product shipping classes', 'woocommerce' ), 401 ); + } + + $defaults = array( + 'name' => '', + 'slug' => '', + 'description' => '', + 'parent' => 0, + ); + + $data = wp_parse_args( $data['product_shipping_class'], $defaults ); + $data = apply_filters( 'woocommerce_api_create_product_shipping_class_data', $data, $this ); + + // Check parent. + $data['parent'] = absint( $data['parent'] ); + if ( $data['parent'] ) { + $parent = get_term_by( 'id', $data['parent'], 'product_shipping_class' ); + if ( ! $parent ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_product_shipping_class_parent', __( 'Product shipping class parent is invalid', 'woocommerce' ), 400 ); + } + } + + $insert = wp_insert_term( $data['name'], 'product_shipping_class', $data ); + if ( is_wp_error( $insert ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_create_product_shipping_class', $insert->get_error_message(), 400 ); + } + + $id = $insert['term_id']; + + do_action( 'woocommerce_api_create_product_shipping_class', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_product_shipping_class( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a product shipping class. + * + * @since 2.5.0 + * @param int $id Product shipping class term ID + * @param array $data Posted data + * @return array|WP_Error Product shipping class if succeed, otherwise + * WP_Error will be returned + */ + public function edit_product_shipping_class( $id, $data ) { + global $wpdb; + + try { + if ( ! isset( $data['product_shipping_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_product_shipping_class', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'product_shipping_class' ), 400 ); + } + + $id = absint( $id ); + $data = $data['product_shipping_class']; + + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_product_shipping_class', __( 'You do not have permission to edit product shipping classes', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_edit_product_shipping_class_data', $data, $this ); + $shipping_class = $this->get_product_shipping_class( $id ); + + if ( is_wp_error( $shipping_class ) ) { + return $shipping_class; + } + + $update = wp_update_term( $id, 'product_shipping_class', $data ); + if ( is_wp_error( $update ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_edit_product_shipping_class', __( 'Could not edit the shipping class', 'woocommerce' ), 400 ); + } + + do_action( 'woocommerce_api_edit_product_shipping_class', $id, $data ); + + return $this->get_product_shipping_class( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a product shipping class. + * + * @since 2.5.0 + * @param int $id Product shipping class term ID + * @return array|WP_Error Success message if succeed, otherwise WP_Error + * will be returned + */ + public function delete_product_shipping_class( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_product_terms' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_product_shipping_class', __( 'You do not have permission to delete product shipping classes', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + $deleted = wp_delete_term( $id, 'product_shipping_class' ); + if ( ! $deleted || is_wp_error( $deleted ) ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_product_shipping_class', __( 'Could not delete the shipping class', 'woocommerce' ), 401 ); + } + + do_action( 'woocommerce_api_delete_product_shipping_class', $id, $this ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'product_shipping_class' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-reports.php b/includes/legacy/api/v3/class-wc-api-reports.php new file mode 100644 index 0000000..c5f5c66 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-reports.php @@ -0,0 +1,330 @@ +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 true; + } + + return new WP_Error( + 'woocommerce_api_user_cannot_read_report', + __( 'You do not have permission to read this report', 'woocommerce' ), + array( 'status' => 401 ) + ); + } +} diff --git a/includes/legacy/api/v3/class-wc-api-resource.php b/includes/legacy/api/v3/class-wc-api-resource.php new file mode 100644 index 0000000..f321b9c --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-resource.php @@ -0,0 +1,471 @@ +server = $server; + + // automatically register routes for sub-classes + add_filter( 'woocommerce_api_endpoints', array( $this, 'register_routes' ) ); + + // maybe add meta to top-level resource responses + foreach ( array( 'order', 'coupon', 'customer', 'product', 'report' ) as $resource ) { + add_filter( "woocommerce_api_{$resource}_response", array( $this, 'maybe_add_meta' ), 15, 2 ); + } + + $response_names = array( + 'order', + 'coupon', + 'customer', + 'product', + 'report', + 'customer_orders', + 'customer_downloads', + 'order_note', + 'order_refund', + 'product_reviews', + 'product_category', + 'tax', + 'tax_class', + ); + + foreach ( $response_names as $name ) { + + /** + * Remove fields from responses when requests specify certain fields + * note these are hooked at a later priority so data added via + * filters (e.g. customer data to the order response) still has the + * fields filtered properly + */ + add_filter( "woocommerce_api_{$name}_response", array( $this, 'filter_response_fields' ), 20, 3 ); + } + } + + /** + * Validate the request by checking: + * + * 1) the ID is a valid integer + * 2) the ID returns a valid post object and matches the provided post type + * 3) the current user has the proper permissions to read/edit/delete the post + * + * @since 2.1 + * @param string|int $id the post ID + * @param string $type the post type, either `shop_order`, `shop_coupon`, or `product` + * @param string $context the context of the request, either `read`, `edit` or `delete` + * @return int|WP_Error valid post ID or WP_Error if any of the checks fails + */ + protected function validate_request( $id, $type, $context ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type || 'shop_webhook' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + $id = absint( $id ); + + // Validate ID + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); + } + + // Only custom post types have per-post type/permission checks + if ( 'customer' !== $type ) { + + $post = get_post( $id ); + + if ( null === $post ) { + return new WP_Error( "woocommerce_api_no_{$resource_name}_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), $resource_name, $id ), array( 'status' => 404 ) ); + } + + // For checking permissions, product variations are the same as the product post type + $post_type = ( 'product_variation' === $post->post_type ) ? 'product' : $post->post_type; + + // Validate post type + if ( $type !== $post_type ) { + return new WP_Error( "woocommerce_api_invalid_{$resource_name}", sprintf( __( 'Invalid %s', 'woocommerce' ), $resource_name ), array( 'status' => 404 ) ); + } + + // Validate permissions + switch ( $context ) { + + case 'read': + if ( ! $this->is_readable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_read_{$resource_name}", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'edit': + if ( ! $this->is_editable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_edit_{$resource_name}", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + + case 'delete': + if ( ! $this->is_deletable( $post ) ) { + return new WP_Error( "woocommerce_api_user_cannot_delete_{$resource_name}", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), $resource_name ), array( 'status' => 401 ) ); + } + break; + } + } + + return $id; + } + + /** + * Add common request arguments to argument list before WP_Query is run + * + * @since 2.1 + * @param array $base_args required arguments for the query (e.g. `post_type`, etc) + * @param array $request_args arguments provided in the request + * @return array + */ + protected function merge_query_args( $base_args, $request_args ) { + + $args = array(); + + // date + if ( ! empty( $request_args['created_at_min'] ) || ! empty( $request_args['created_at_max'] ) || ! empty( $request_args['updated_at_min'] ) || ! empty( $request_args['updated_at_max'] ) ) { + + $args['date_query'] = array(); + + // resources created after specified date + if ( ! empty( $request_args['created_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'after' => $this->server->parse_datetime( $request_args['created_at_min'] ), 'inclusive' => true ); + } + + // resources created before specified date + if ( ! empty( $request_args['created_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_date_gmt', 'before' => $this->server->parse_datetime( $request_args['created_at_max'] ), 'inclusive' => true ); + } + + // resources updated after specified date + if ( ! empty( $request_args['updated_at_min'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'after' => $this->server->parse_datetime( $request_args['updated_at_min'] ), 'inclusive' => true ); + } + + // resources updated before specified date + if ( ! empty( $request_args['updated_at_max'] ) ) { + $args['date_query'][] = array( 'column' => 'post_modified_gmt', 'before' => $this->server->parse_datetime( $request_args['updated_at_max'] ), 'inclusive' => true ); + } + } + + // search + if ( ! empty( $request_args['q'] ) ) { + $args['s'] = $request_args['q']; + } + + // resources per response + if ( ! empty( $request_args['limit'] ) ) { + $args['posts_per_page'] = $request_args['limit']; + } + + // resource offset + if ( ! empty( $request_args['offset'] ) ) { + $args['offset'] = $request_args['offset']; + } + + // order (ASC or DESC, ASC by default) + if ( ! empty( $request_args['order'] ) ) { + $args['order'] = $request_args['order']; + } + + // orderby + if ( ! empty( $request_args['orderby'] ) ) { + $args['orderby'] = $request_args['orderby']; + + // allow sorting by meta value + if ( ! empty( $request_args['orderby_meta_key'] ) ) { + $args['meta_key'] = $request_args['orderby_meta_key']; + } + } + + // allow post status change + if ( ! empty( $request_args['post_status'] ) ) { + $args['post_status'] = $request_args['post_status']; + unset( $request_args['post_status'] ); + } + + // filter by a list of post id + if ( ! empty( $request_args['in'] ) ) { + $args['post__in'] = explode( ',', $request_args['in'] ); + unset( $request_args['in'] ); + } + + // exclude by a list of post id + if ( ! empty( $request_args['not_in'] ) ) { + $args['post__not_in'] = explode( ',', $request_args['not_in'] ); + unset( $request_args['not_in'] ); + } + + // resource page + $args['paged'] = ( isset( $request_args['page'] ) ) ? absint( $request_args['page'] ) : 1; + + $args = apply_filters( 'woocommerce_api_query_args', $args, $request_args ); + + return array_merge( $base_args, $args ); + } + + /** + * Add meta to resources when requested by the client. Meta is added as a top-level + * `_meta` attribute (e.g. `order_meta`) as a list of key/value pairs + * + * @since 2.1 + * @param array $data the resource data + * @param object $resource the resource object (e.g WC_Order) + * @return mixed + */ + public function maybe_add_meta( $data, $resource ) { + + if ( isset( $this->server->params['GET']['filter']['meta'] ) && 'true' === $this->server->params['GET']['filter']['meta'] && is_object( $resource ) ) { + + // don't attempt to add meta more than once + if ( preg_grep( '/[a-z]+_meta/', array_keys( $data ) ) ) { + return $data; + } + + // define the top-level property name for the meta + switch ( get_class( $resource ) ) { + + case 'WC_Order': + $meta_name = 'order_meta'; + break; + + case 'WC_Coupon': + $meta_name = 'coupon_meta'; + break; + + case 'WP_User': + $meta_name = 'customer_meta'; + break; + + default: + $meta_name = 'product_meta'; + break; + } + + if ( is_a( $resource, 'WP_User' ) ) { + + // customer meta + $meta = (array) get_user_meta( $resource->ID ); + + } else { + + // coupon/order/product meta + $meta = (array) get_post_meta( $resource->get_id() ); + } + + foreach ( $meta as $meta_key => $meta_value ) { + + // don't add hidden meta by default + if ( ! is_protected_meta( $meta_key ) ) { + $data[ $meta_name ][ $meta_key ] = maybe_unserialize( $meta_value[0] ); + } + } + } + + return $data; + } + + /** + * Restrict the fields included in the response if the request specified certain only certain fields should be returned + * + * @since 2.1 + * @param array $data the response data + * @param object $resource the object that provided the response data, e.g. WC_Coupon or WC_Order + * @param array|string the requested list of fields to include in the response + * @return array response data + */ + public function filter_response_fields( $data, $resource, $fields ) { + + if ( ! is_array( $data ) || empty( $fields ) ) { + return $data; + } + + $fields = explode( ',', $fields ); + $sub_fields = array(); + + // get sub fields + foreach ( $fields as $field ) { + + if ( false !== strpos( $field, '.' ) ) { + + list( $name, $value ) = explode( '.', $field ); + + $sub_fields[ $name ] = $value; + } + } + + // iterate through top-level fields + foreach ( $data as $data_field => $data_value ) { + + // if a field has sub-fields and the top-level field has sub-fields to filter + if ( is_array( $data_value ) && in_array( $data_field, array_keys( $sub_fields ) ) ) { + + // iterate through each sub-field + foreach ( $data_value as $sub_field => $sub_field_value ) { + + // remove non-matching sub-fields + if ( ! in_array( $sub_field, $sub_fields ) ) { + unset( $data[ $data_field ][ $sub_field ] ); + } + } + } else { + + // remove non-matching top-level fields + if ( ! in_array( $data_field, $fields ) ) { + unset( $data[ $data_field ] ); + } + } + } + + return $data; + } + + /** + * Delete a given resource + * + * @since 2.1 + * @param int $id the resource ID + * @param string $type the resource post type, or `customer` + * @param bool $force true to permanently delete resource, false to move to trash (not supported for `customer`) + * @return array|WP_Error + */ + protected function delete( $id, $type, $force = false ) { + + if ( 'shop_order' === $type || 'shop_coupon' === $type ) { + $resource_name = str_replace( 'shop_', '', $type ); + } else { + $resource_name = $type; + } + + if ( 'customer' === $type ) { + + $result = wp_delete_user( $id ); + + if ( $result ) { + return array( 'message' => __( 'Permanently deleted customer', 'woocommerce' ) ); + } else { + return new WP_Error( 'woocommerce_api_cannot_delete_customer', __( 'The customer cannot be deleted', 'woocommerce' ), array( 'status' => 500 ) ); + } + } else { + + // delete order/coupon/product/webhook + $result = ( $force ) ? wp_delete_post( $id, true ) : wp_trash_post( $id ); + + if ( ! $result ) { + return new WP_Error( "woocommerce_api_cannot_delete_{$resource_name}", sprintf( __( 'This %s cannot be deleted', 'woocommerce' ), $resource_name ), array( 'status' => 500 ) ); + } + + if ( $force ) { + return array( 'message' => sprintf( __( 'Permanently deleted %s', 'woocommerce' ), $resource_name ) ); + + } else { + + $this->server->send_status( '202' ); + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), $resource_name ) ); + } + } + } + + + /** + * Checks if the given post is readable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_readable( $post ) { + + return $this->check_permission( $post, 'read' ); + } + + /** + * Checks if the given post is editable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_editable( $post ) { + + return $this->check_permission( $post, 'edit' ); + + } + + /** + * Checks if the given post is deletable by the current user + * + * @since 2.1 + * @see WC_API_Resource::check_permission() + * @param WP_Post|int $post + * @return bool + */ + protected function is_deletable( $post ) { + + return $this->check_permission( $post, 'delete' ); + } + + /** + * Checks the permissions for the current user given a post and context + * + * @since 2.1 + * @param WP_Post|int $post + * @param string $context the type of permission to check, either `read`, `write`, or `delete` + * @return bool true if the current user has the permissions to perform the context on the post + */ + private function check_permission( $post, $context ) { + $permission = false; + + if ( ! is_a( $post, 'WP_Post' ) ) { + $post = get_post( $post ); + } + + if ( is_null( $post ) ) { + return $permission; + } + + $post_type = get_post_type_object( $post->post_type ); + + if ( 'read' === $context ) { + $permission = 'revision' !== $post->post_type && current_user_can( $post_type->cap->read_private_posts, $post->ID ); + } elseif ( 'edit' === $context ) { + $permission = current_user_can( $post_type->cap->edit_post, $post->ID ); + } elseif ( 'delete' === $context ) { + $permission = current_user_can( $post_type->cap->delete_post, $post->ID ); + } + + return apply_filters( 'woocommerce_api_check_permission', $permission, $context, $post, $post_type ); + } +} diff --git a/includes/legacy/api/v3/class-wc-api-server.php b/includes/legacy/api/v3/class-wc-api-server.php new file mode 100644 index 0000000..44305dc --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-server.php @@ -0,0 +1,777 @@ + 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 + */ + 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() { + // @codingStandardsIgnoreStart + // $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; + // @codingStandardsIgnoreEnd + } + + /** + * 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/legacy/api/v3/class-wc-api-taxes.php b/includes/legacy/api/v3/class-wc-api-taxes.php new file mode 100644 index 0000000..887a793 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-taxes.php @@ -0,0 +1,649 @@ + + * + * @since 2.1 + * @param array $routes + * @return array + */ + public function register_routes( $routes ) { + + # GET/POST /taxes + $routes[ $this->base ] = array( + array( array( $this, 'get_taxes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_tax' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /taxes/count + $routes[ $this->base . '/count' ] = array( + array( array( $this, 'get_taxes_count' ), WC_API_Server::READABLE ), + ); + + # GET/PUT/DELETE /taxes/ + $routes[ $this->base . '/(?P\d+)' ] = array( + array( array( $this, 'get_tax' ), WC_API_Server::READABLE ), + array( array( $this, 'edit_tax' ), WC_API_SERVER::EDITABLE | WC_API_SERVER::ACCEPT_DATA ), + array( array( $this, 'delete_tax' ), WC_API_SERVER::DELETABLE ), + ); + + # GET/POST /taxes/classes + $routes[ $this->base . '/classes' ] = array( + array( array( $this, 'get_tax_classes' ), WC_API_Server::READABLE ), + array( array( $this, 'create_tax_class' ), WC_API_Server::CREATABLE | WC_API_Server::ACCEPT_DATA ), + ); + + # GET /taxes/classes/count + $routes[ $this->base . '/classes/count' ] = array( + array( array( $this, 'get_tax_classes_count' ), WC_API_Server::READABLE ), + ); + + # GET /taxes/classes/ + $routes[ $this->base . '/classes/(?P\w[\w\s\-]*)' ] = array( + array( array( $this, 'delete_tax_class' ), WC_API_SERVER::DELETABLE ), + ); + + # POST|PUT /taxes/bulk + $routes[ $this->base . '/bulk' ] = array( + array( array( $this, 'bulk' ), WC_API_Server::EDITABLE | WC_API_Server::ACCEPT_DATA ), + ); + + return $routes; + } + + /** + * Get all taxes + * + * @since 2.5.0 + * + * @param string $fields + * @param array $filter + * @param string $class + * @param int $page + * + * @return array + */ + public function get_taxes( $fields = null, $filter = array(), $class = null, $page = 1 ) { + if ( ! empty( $class ) ) { + $filter['tax_rate_class'] = $class; + } + + $filter['page'] = $page; + + $query = $this->query_tax_rates( $filter ); + + $taxes = array(); + + foreach ( $query['results'] as $tax ) { + $taxes[] = current( $this->get_tax( $tax->tax_rate_id, $fields ) ); + } + + // Set pagination headers + $this->server->add_pagination_headers( $query['headers'] ); + + return array( 'taxes' => $taxes ); + } + + /** + * Get the tax for the given ID + * + * @since 2.5.0 + * + * @param int $id The tax ID + * @param string $fields fields to include in response + * + * @return array|WP_Error + */ + public function get_tax( $id, $fields = null ) { + global $wpdb; + + try { + $id = absint( $id ); + + // Permissions check + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax', __( 'You do not have permission to read tax rate', 'woocommerce' ), 401 ); + } + + // Get tax rate details + $tax = WC_Tax::_get_tax_rate( $id ); + + if ( is_wp_error( $tax ) || empty( $tax ) ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_tax_id', __( 'A tax rate with the provided ID could not be found', 'woocommerce' ), 404 ); + } + + $tax_data = array( + 'id' => (int) $tax['tax_rate_id'], + 'country' => $tax['tax_rate_country'], + 'state' => $tax['tax_rate_state'], + 'postcode' => '', + 'city' => '', + 'rate' => $tax['tax_rate'], + 'name' => $tax['tax_rate_name'], + 'priority' => (int) $tax['tax_rate_priority'], + 'compound' => (bool) $tax['tax_rate_compound'], + 'shipping' => (bool) $tax['tax_rate_shipping'], + 'order' => (int) $tax['tax_rate_order'], + 'class' => $tax['tax_rate_class'] ? $tax['tax_rate_class'] : 'standard', + ); + + // Get locales from a tax rate + $locales = $wpdb->get_results( $wpdb->prepare( " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", $id ) ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $tax_data[ $locale->location_type ] = $locale->location_code; + } + } + + return array( 'tax' => apply_filters( 'woocommerce_api_tax_response', $tax_data, $tax, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a tax + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_tax( $data ) { + try { + if ( ! isset( $data['tax'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax', __( 'You do not have permission to create tax rates', 'woocommerce' ), 401 ); + } + + $data = apply_filters( 'woocommerce_api_create_tax_data', $data['tax'], $this ); + + $tax_data = array( + 'tax_rate_country' => '', + 'tax_rate_state' => '', + 'tax_rate' => '', + 'tax_rate_name' => '', + 'tax_rate_priority' => 1, + 'tax_rate_compound' => 0, + 'tax_rate_shipping' => 1, + 'tax_rate_order' => 0, + 'tax_rate_class' => '', + ); + + foreach ( $tax_data as $key => $value ) { + $new_key = str_replace( 'tax_rate_', '', $key ); + $new_key = 'tax_rate' === $new_key ? 'rate' : $new_key; + + if ( isset( $data[ $new_key ] ) ) { + if ( in_array( $new_key, array( 'compound', 'shipping' ) ) ) { + $tax_data[ $key ] = $data[ $new_key ] ? 1 : 0; + } else { + $tax_data[ $key ] = $data[ $new_key ]; + } + } + } + + // Create tax rate + $id = WC_Tax::_insert_tax_rate( $tax_data ); + + // Add locales + if ( ! empty( $data['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); + } + + if ( ! empty( $data['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); + } + + do_action( 'woocommerce_api_create_tax', $id, $data ); + + $this->server->send_status( 201 ); + + return $this->get_tax( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Edit a tax + * + * @since 2.5.0 + * + * @param int $id The tax ID + * @param array $data + * + * @return array|WP_Error + */ + public function edit_tax( $id, $data ) { + try { + if ( ! isset( $data['tax'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_data', sprintf( __( 'No %1$s data specified to edit %1$s', 'woocommerce' ), 'tax' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_edit_tax', __( 'You do not have permission to edit tax rates', 'woocommerce' ), 401 ); + } + + $data = $data['tax']; + + // Get current tax rate data + $tax = $this->get_tax( $id ); + + if ( is_wp_error( $tax ) ) { + $error_data = $tax->get_error_data(); + throw new WC_API_Exception( $tax->get_error_code(), $tax->get_error_message(), $error_data['status'] ); + } + + $current_data = $tax['tax']; + $data = apply_filters( 'woocommerce_api_edit_tax_data', $data, $this ); + $tax_data = array(); + $default_fields = array( + 'tax_rate_country', + 'tax_rate_state', + 'tax_rate', + 'tax_rate_name', + 'tax_rate_priority', + 'tax_rate_compound', + 'tax_rate_shipping', + 'tax_rate_order', + 'tax_rate_class', + ); + + foreach ( $data as $key => $value ) { + $new_key = 'rate' === $key ? 'tax_rate' : 'tax_rate_' . $key; + + // Check if the key is valid + if ( ! in_array( $new_key, $default_fields ) ) { + continue; + } + + // Test new data against current data + if ( $value === $current_data[ $key ] ) { + continue; + } + + // Fix compound and shipping values + if ( in_array( $key, array( 'compound', 'shipping' ) ) ) { + $value = $value ? 1 : 0; + } + + $tax_data[ $new_key ] = $value; + } + + // Update tax rate + WC_Tax::_update_tax_rate( $id, $tax_data ); + + // Update locales + if ( ! empty( $data['postcode'] ) && $current_data['postcode'] != $data['postcode'] ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $data['postcode'] ) ); + } + + if ( ! empty( $data['city'] ) && $current_data['city'] != $data['city'] ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $data['city'] ) ); + } + + do_action( 'woocommerce_api_edit_tax_rate', $id, $data ); + + return $this->get_tax( $id ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a tax + * + * @since 2.5.0 + * + * @param int $id The tax ID + * + * @return array|WP_Error + */ + public function delete_tax( $id ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax', __( 'You do not have permission to delete tax rates', 'woocommerce' ), 401 ); + } + + $id = absint( $id ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax', __( 'Could not delete the tax rate', 'woocommerce' ), 401 ); + } + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of taxes + * + * @since 2.5.0 + * + * @param string $class + * @param array $filter + * + * @return array|WP_Error + */ + public function get_taxes_count( $class = null, $filter = array() ) { + try { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_taxes_count', __( 'You do not have permission to read the taxes count', 'woocommerce' ), 401 ); + } + + if ( ! empty( $class ) ) { + $filter['tax_rate_class'] = $class; + } + + $query = $this->query_tax_rates( $filter, true ); + + return array( 'count' => (int) $query['headers']->total ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Helper method to get tax rates objects + * + * @since 2.5.0 + * + * @param array $args + * @param bool $count_only + * + * @return array + */ + protected function query_tax_rates( $args, $count_only = false ) { + global $wpdb; + + $results = ''; + + // Set args + $args = $this->merge_query_args( $args, array() ); + + $query = " + SELECT tax_rate_id + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE 1 = 1 + "; + + // Filter by tax class + if ( ! empty( $args['tax_rate_class'] ) ) { + $tax_rate_class = 'standard' !== $args['tax_rate_class'] ? sanitize_title( $args['tax_rate_class'] ) : ''; + $query .= " AND tax_rate_class = '$tax_rate_class'"; + } + + // Order tax rates + $order_by = ' ORDER BY tax_rate_order'; + + // Pagination + $per_page = isset( $args['posts_per_page'] ) ? $args['posts_per_page'] : get_option( 'posts_per_page' ); + $offset = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $per_page : 0; + $pagination = sprintf( ' LIMIT %d, %d', $offset, $per_page ); + + if ( ! $count_only ) { + $results = $wpdb->get_results( $query . $order_by . $pagination ); + } + + $wpdb->get_results( $query ); + $headers = new stdClass; + $headers->page = $args['paged']; + $headers->total = (int) $wpdb->num_rows; + $headers->is_single = $per_page > $headers->total; + $headers->total_pages = ceil( $headers->total / $per_page ); + + return array( + 'results' => $results, + 'headers' => $headers, + ); + } + + /** + * Bulk update or insert taxes + * Accepts an array with taxes in the formats supported by + * WC_API_Taxes->create_tax() and WC_API_Taxes->edit_tax() + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function bulk( $data ) { + try { + if ( ! isset( $data['taxes'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_taxes_data', sprintf( __( 'No %1$s data specified to create/edit %1$s', 'woocommerce' ), 'taxes' ), 400 ); + } + + $data = $data['taxes']; + $limit = apply_filters( 'woocommerce_api_bulk_limit', 100, 'taxes' ); + + // Limit bulk operation + if ( count( $data ) > $limit ) { + throw new WC_API_Exception( 'woocommerce_api_taxes_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), 413 ); + } + + $taxes = array(); + + foreach ( $data as $_tax ) { + $tax_id = 0; + + // Try to get the tax rate ID + if ( isset( $_tax['id'] ) ) { + $tax_id = intval( $_tax['id'] ); + } + + if ( $tax_id ) { + + // Tax rate exists / edit tax rate + $edit = $this->edit_tax( $tax_id, array( 'tax' => $_tax ) ); + + if ( is_wp_error( $edit ) ) { + $taxes[] = array( + 'id' => $tax_id, + 'error' => array( 'code' => $edit->get_error_code(), 'message' => $edit->get_error_message() ), + ); + } else { + $taxes[] = $edit['tax']; + } + } else { + + // Tax rate don't exists / create tax rate + $new = $this->create_tax( array( 'tax' => $_tax ) ); + + if ( is_wp_error( $new ) ) { + $taxes[] = array( + 'id' => $tax_id, + 'error' => array( 'code' => $new->get_error_code(), 'message' => $new->get_error_message() ), + ); + } else { + $taxes[] = $new['tax']; + } + } + } + + return array( 'taxes' => apply_filters( 'woocommerce_api_taxes_bulk_response', $taxes, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get all tax classes + * + * @since 2.5.0 + * + * @param string $fields + * + * @return array|WP_Error + */ + public function get_tax_classes( $fields = null ) { + try { + // Permissions check + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes', __( 'You do not have permission to read tax classes', 'woocommerce' ), 401 ); + } + + $tax_classes = array(); + + // Add standard class + $tax_classes[] = array( + 'slug' => 'standard', + 'name' => __( 'Standard rate', 'woocommerce' ), + ); + + $classes = WC_Tax::get_tax_classes(); + + foreach ( $classes as $class ) { + $tax_classes[] = apply_filters( 'woocommerce_api_tax_class_response', array( + 'slug' => sanitize_title( $class ), + 'name' => $class, + ), $class, $fields, $this ); + } + + return array( 'tax_classes' => apply_filters( 'woocommerce_api_tax_classes_response', $tax_classes, $classes, $fields, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a tax class. + * + * @since 2.5.0 + * + * @param array $data + * + * @return array|WP_Error + */ + public function create_tax_class( $data ) { + try { + if ( ! isset( $data['tax_class'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_data', sprintf( __( 'No %1$s data specified to create %1$s', 'woocommerce' ), 'tax_class' ), 400 ); + } + + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_create_tax_class', __( 'You do not have permission to create tax classes', 'woocommerce' ), 401 ); + } + + $data = $data['tax_class']; + + if ( empty( $data['name'] ) ) { + throw new WC_API_Exception( 'woocommerce_api_missing_tax_class_name', sprintf( __( 'Missing parameter %s', 'woocommerce' ), 'name' ), 400 ); + } + + $name = sanitize_text_field( $data['name'] ); + $tax_class = WC_Tax::create_tax_class( $name ); + + if ( is_wp_error( $tax_class ) ) { + return new WP_Error( 'woocommerce_api_' . $tax_class->get_error_code(), $tax_class->get_error_message(), 401 ); + } + + do_action( 'woocommerce_api_create_tax_class', $tax_class['slug'], $data ); + + $this->server->send_status( 201 ); + + return array( + 'tax_class' => $tax_class, + ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a tax class + * + * @since 2.5.0 + * + * @param int $slug The tax class slug + * + * @return array|WP_Error + */ + public function delete_tax_class( $slug ) { + global $wpdb; + + try { + // Check permissions + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_delete_tax_class', __( 'You do not have permission to delete tax classes', 'woocommerce' ), 401 ); + } + + $slug = sanitize_title( $slug ); + $tax_class = WC_Tax::get_tax_class_by( 'slug', $slug ); + $deleted = WC_Tax::delete_tax_class_by( 'slug', $slug ); + + if ( is_wp_error( $deleted ) || ! $deleted ) { + throw new WC_API_Exception( 'woocommerce_api_cannot_delete_tax_class', __( 'Could not delete the tax class', 'woocommerce' ), 401 ); + } + + return array( 'message' => sprintf( __( 'Deleted %s', 'woocommerce' ), 'tax_class' ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the total number of tax classes + * + * @since 2.5.0 + * + * @return array|WP_Error + */ + public function get_tax_classes_count() { + try { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + throw new WC_API_Exception( 'woocommerce_api_user_cannot_read_tax_classes_count', __( 'You do not have permission to read the tax classes count', 'woocommerce' ), 401 ); + } + + $total = count( WC_Tax::get_tax_classes() ) + 1; // +1 for Standard Rate + + return array( 'count' => $total ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } +} diff --git a/includes/legacy/api/v3/class-wc-api-webhooks.php b/includes/legacy/api/v3/class-wc-api-webhooks.php new file mode 100644 index 0000000..98e14b5 --- /dev/null +++ b/includes/legacy/api/v3/class-wc-api-webhooks.php @@ -0,0 +1,509 @@ +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['results'] as $webhook_id ) { + $webhooks[] = current( $this->get_webhook( $webhook_id, $fields ) ); + } + + $this->server->add_pagination_headers( $query['headers'] ); + + 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 = wc_get_webhook( $id ); + + $webhook_data = array( + 'id' => $webhook->get_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_date_created() ? $webhook->get_date_created()->getTimestamp() : 0, false, false ), // API gives UTC times. + 'updated_at' => $this->server->format_datetime( $webhook->get_date_modified() ? $webhook->get_date_modified()->getTimestamp() : 0, false, false ), // API gives UTC times. + ); + + 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( 'manage_woocommerce' ) ) { + 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' => $query['headers']->total ); + } 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( 'manage_woocommerce' ) ) { + 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' => 'webhook_' . wp_generate_password(), + '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' ) ) ), + ), $data, $this ); + + $webhook = new WC_Webhook(); + + $webhook->set_name( $webhook_data['post_title'] ); + $webhook->set_user_id( $webhook_data['post_author'] ); + $webhook->set_status( 'publish' === $webhook_data['post_status'] ? 'active' : 'disabled' ); + $webhook->set_topic( $data['topic'] ); + $webhook->set_delivery_url( $data['delivery_url'] ); + $webhook->set_secret( ! empty( $data['secret'] ) ? $data['secret'] : wp_generate_password( 50, true, true ) ); + $webhook->set_api_version( 'legacy_v3' ); + $webhook->save(); + + $webhook->deliver_ping(); + + // HTTP 201 Created + $this->server->send_status( 201 ); + + do_action( 'woocommerce_api_create_webhook', $webhook->get_id(), $this ); + + return $this->get_webhook( $webhook->get_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 = wc_get_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->set_status( $data['status'] ); + } + + // update name + if ( ! empty( $data['name'] ) ) { + $webhook->set_name( $data['name'] ); + } + + $webhook->save(); + + do_action( 'woocommerce_api_edit_webhook', $webhook->get_id(), $this ); + + return $this->get_webhook( $webhook->get_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 ); + + $webhook = wc_get_webhook( $id ); + + return $webhook->delete( true ); + } + + /** + * Helper method to get webhook post objects + * + * @since 2.2 + * @param array $args Request arguments for filtering query. + * @return array + */ + private function query_webhooks( $args ) { + $args = $this->merge_query_args( array(), $args ); + + $args['limit'] = isset( $args['posts_per_page'] ) ? intval( $args['posts_per_page'] ) : intval( get_option( 'posts_per_page' ) ); + + if ( empty( $args['offset'] ) ) { + $args['offset'] = 1 < $args['paged'] ? ( $args['paged'] - 1 ) * $args['limit'] : 0; + } + + $page = $args['paged']; + unset( $args['paged'], $args['posts_per_page'] ); + + if ( isset( $args['s'] ) ) { + $args['search'] = $args['s']; + unset( $args['s'] ); + } + + // Post type to webhook status. + if ( ! empty( $args['post_status'] ) ) { + $args['status'] = $args['post_status']; + unset( $args['post_status'] ); + } + + if ( ! empty( $args['post__in'] ) ) { + $args['include'] = $args['post__in']; + unset( $args['post__in'] ); + } + + if ( ! empty( $args['date_query'] ) ) { + foreach ( $args['date_query'] as $date_query ) { + if ( 'post_date_gmt' === $date_query['column'] ) { + $args['after'] = isset( $date_query['after'] ) ? $date_query['after'] : null; + $args['before'] = isset( $date_query['before'] ) ? $date_query['before'] : null; + } elseif ( 'post_modified_gmt' === $date_query['column'] ) { + $args['modified_after'] = isset( $date_query['after'] ) ? $date_query['after'] : null; + $args['modified_before'] = isset( $date_query['before'] ) ? $date_query['before'] : null; + } + } + + unset( $args['date_query'] ); + } + + $args['paginate'] = true; + + // Get the webhooks. + $data_store = WC_Data_Store::load( 'webhook' ); + $results = $data_store->search_webhooks( $args ); + + // Get total items. + $headers = new stdClass; + $headers->page = $page; + $headers->total = $results->total; + $headers->is_single = $args['limit'] > $headers->total; + $headers->total_pages = $results->max_num_pages; + + return array( + 'results' => $results->webhooks, + 'headers' => $headers, + ); + } + + /** + * Get deliveries for a webhook + * + * @since 2.2 + * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. + * @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; + } + + return array( 'webhook_deliveries' => array() ); + } + + /** + * Get the delivery log for the given webhook ID and delivery ID + * + * @since 2.2 + * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. + * @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 = 0; + + if ( ! $log ) { + throw new WC_API_Exception( 'woocommerce_api_invalid_webhook_delivery_id', __( 'Invalid webhook delivery.', 'woocommerce' ), 400 ); + } + + return array( 'webhook_delivery' => apply_filters( 'woocommerce_api_webhook_delivery_response', array(), $id, $fields, $log, $webhook_id, $this ) ); + } catch ( WC_API_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * 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 3.3.0 + * @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 ) { + $id = absint( $id ); + + // Validate ID. + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_api_invalid_webhook_id", sprintf( __( 'Invalid %s ID', 'woocommerce' ), $type ), array( 'status' => 404 ) ); + } + + $webhook = wc_get_webhook( $id ); + + if ( null === $webhook ) { + return new WP_Error( "woocommerce_api_no_webhook_found", sprintf( __( 'No %1$s found with the ID equal to %2$s', 'woocommerce' ), 'webhook', $id ), array( 'status' => 404 ) ); + } + + // Validate permissions. + switch ( $context ) { + + case 'read': + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( "woocommerce_api_user_cannot_read_webhook", sprintf( __( 'You do not have permission to read this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); + } + break; + + case 'edit': + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( "woocommerce_api_user_cannot_edit_webhook", sprintf( __( 'You do not have permission to edit this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); + } + break; + + case 'delete': + if ( ! current_user_can( 'manage_woocommerce' ) ) { + return new WP_Error( "woocommerce_api_user_cannot_delete_webhook", sprintf( __( 'You do not have permission to delete this %s', 'woocommerce' ), 'webhook' ), array( 'status' => 401 ) ); + } + break; + } + + return $id; + } +} diff --git a/includes/legacy/api/v3/interface-wc-api-handler.php b/includes/legacy/api/v3/interface-wc-api-handler.php new file mode 100644 index 0000000..94ce87d --- /dev/null +++ b/includes/legacy/api/v3/interface-wc-api-handler.php @@ -0,0 +1,47 @@ +query_vars['wc-api-version'] = $_GET['wc-api-version']; + } + + if ( ! empty( $_GET['wc-api-route'] ) ) { + $wp->query_vars['wc-api-route'] = $_GET['wc-api-route']; + } + + // REST API request. + if ( ! empty( $wp->query_vars['wc-api-version'] ) && ! empty( $wp->query_vars['wc-api-route'] ) ) { + + wc_maybe_define_constant( 'WC_API_REQUEST', true ); + wc_maybe_define_constant( 'WC_API_REQUEST_VERSION', absint( $wp->query_vars['wc-api-version'] ) ); + + // Legacy v1 API request. + if ( 1 === WC_API_REQUEST_VERSION ) { + $this->handle_v1_rest_api_request(); + } elseif ( 2 === WC_API_REQUEST_VERSION ) { + $this->handle_v2_rest_api_request(); + } else { + $this->includes(); + + $this->server = new WC_API_Server( $wp->query_vars['wc-api-route'] ); + + // load API resource classes. + $this->register_resources( $this->server ); + + // Fire off the request. + $this->server->serve_request(); + } + + exit; + } + } + + /** + * Include required files for REST API request. + * + * @since 2.1 + * @deprecated 2.6.0 + */ + public function includes() { + + // API server / response handlers. + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-exception.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-json-handler.php' ); + + // Authentication. + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-taxes.php' ); + include_once( dirname( __FILE__ ) . '/api/v3/class-wc-api-webhooks.php' ); + + // Allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + } + + /** + * Register available API resources. + * + * @since 2.1 + * @deprecated 2.6.0 + * @param WC_API_Server $server the REST server. + */ + public function register_resources( $server ) { + + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Coupons', + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Reports', + 'WC_API_Taxes', + 'WC_API_Webhooks', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $server ); + } + } + + + /** + * Handle legacy v1 REST API requests. + * + * @since 2.2 + * @deprecated 2.6.0 + */ + private function handle_v1_rest_api_request() { + + // Include legacy required files for v1 REST API request. + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-json-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-xml-handler.php' ); + + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v1/class-wc-api-reports.php' ); + + // Allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + + $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); + + // Register available resources for legacy v1 REST API request. + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Coupons', + 'WC_API_Reports', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $this->server ); + } + + // Fire off the request. + $this->server->serve_request(); + } + + /** + * Handle legacy v2 REST API requests. + * + * @since 2.4 + * @deprecated 2.6.0 + */ + private function handle_v2_rest_api_request() { + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-exception.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-server.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/interface-wc-api-handler.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-json-handler.php' ); + + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-authentication.php' ); + $this->authentication = new WC_API_Authentication(); + + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-resource.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-coupons.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-customers.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-orders.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-products.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-reports.php' ); + include_once( dirname( __FILE__ ) . '/api/v2/class-wc-api-webhooks.php' ); + + // allow plugins to load other response handlers or resource classes. + do_action( 'woocommerce_api_loaded' ); + + $this->server = new WC_API_Server( $GLOBALS['wp']->query_vars['wc-api-route'] ); + + // Register available resources for legacy v2 REST API request. + $api_classes = apply_filters( 'woocommerce_api_classes', + array( + 'WC_API_Customers', + 'WC_API_Orders', + 'WC_API_Products', + 'WC_API_Coupons', + 'WC_API_Reports', + 'WC_API_Webhooks', + ) + ); + + foreach ( $api_classes as $api_class ) { + $this->$api_class = new $api_class( $this->server ); + } + + // Fire off the request. + $this->server->serve_request(); + } + + /** + * Rest API Init. + * + * @deprecated 3.7.0 - REST API clases autoload. + */ + public function rest_api_init() {} + + /** + * Include REST API classes. + * + * @deprecated 3.7.0 - REST API clases autoload. + */ + public function rest_api_includes() { + $this->rest_api_init(); + } + /** + * Register REST API routes. + * + * @deprecated 3.7.0 + */ + public function register_rest_routes() { + wc_deprecated_function( 'WC_Legacy_API::register_rest_routes', '3.7.0', '' ); + $this->register_wp_admin_settings(); + } +} diff --git a/includes/legacy/class-wc-legacy-cart.php b/includes/legacy/class-wc-legacy-cart.php new file mode 100644 index 0000000..94a6544 --- /dev/null +++ b/includes/legacy/class-wc-legacy-cart.php @@ -0,0 +1,446 @@ + 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(), + ); + + /** + * Contains an array of coupon usage counts after they have been applied. + * + * @deprecated 3.2.0 + * @var array + */ + public $coupon_applied_count = array(); + + /** + * Map legacy variables. + * + * @param string $name Property name. + * @param mixed $value Value to set. + */ + public function __isset( $name ) { + $legacy_keys = array_merge( + array( + 'dp', + 'prices_include_tax', + 'round_at_subtotal', + 'cart_contents_total', + 'total', + 'subtotal', + 'subtotal_ex_tax', + 'tax_total', + 'fee_total', + 'discount_cart', + 'discount_cart_tax', + 'shipping_total', + 'shipping_tax_total', + 'display_totals_ex_tax', + 'display_cart_ex_tax', + 'cart_contents_weight', + 'cart_contents_count', + 'coupons', + 'taxes', + 'shipping_taxes', + 'coupon_discount_amounts', + 'coupon_discount_tax_amounts', + 'fees', + 'tax', + 'discount_total', + 'tax_display_cart', + ), + is_array( $this->cart_session_data ) ? array_keys( $this->cart_session_data ) : array() + ); + + if ( in_array( $name, $legacy_keys, true ) ) { + return true; + } + + return false; + } + + /** + * Magic getters. + * + * If you add/remove cases here please update $legacy_keys in __isset accordingly. + * + * @param string $name Property name. + * @return mixed + */ + public function &__get( $name ) { + $value = ''; + + switch ( $name ) { + case 'dp' : + $value = wc_get_price_decimals(); + break; + case 'prices_include_tax' : + $value = wc_prices_include_tax(); + break; + case 'round_at_subtotal' : + $value = 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ); + break; + case 'cart_contents_total' : + $value = $this->get_cart_contents_total(); + break; + case 'total' : + $value = $this->get_total( 'edit' ); + break; + case 'subtotal' : + $value = $this->get_subtotal() + $this->get_subtotal_tax(); + break; + case 'subtotal_ex_tax' : + $value = $this->get_subtotal(); + break; + case 'tax_total' : + $value = $this->get_fee_tax() + $this->get_cart_contents_tax(); + break; + case 'fee_total' : + $value = $this->get_fee_total(); + break; + case 'discount_cart' : + $value = $this->get_discount_total(); + break; + case 'discount_cart_tax' : + $value = $this->get_discount_tax(); + break; + case 'shipping_total' : + $value = $this->get_shipping_total(); + break; + case 'shipping_tax_total' : + $value = $this->get_shipping_tax(); + break; + case 'display_totals_ex_tax' : + case 'display_cart_ex_tax' : + $value = ! $this->display_prices_including_tax(); + break; + case 'cart_contents_weight' : + $value = $this->get_cart_contents_weight(); + break; + case 'cart_contents_count' : + $value = $this->get_cart_contents_count(); + break; + case 'coupons' : + $value = $this->get_coupons(); + break; + + // Arrays returned by reference to allow modification without notices. TODO: Remove in 4.0. + case 'taxes' : + wc_deprecated_function( 'WC_Cart->taxes', '3.2', sprintf( 'getters (%s) and setters (%s)', 'WC_Cart::get_cart_contents_taxes()', 'WC_Cart::set_cart_contents_taxes()' ) ); + $value = &$this->totals[ 'cart_contents_taxes' ]; + break; + case 'shipping_taxes' : + wc_deprecated_function( 'WC_Cart->shipping_taxes', '3.2', sprintf( 'getters (%s) and setters (%s)', 'WC_Cart::get_shipping_taxes()', 'WC_Cart::set_shipping_taxes()' ) ); + $value = &$this->totals[ 'shipping_taxes' ]; + break; + case 'coupon_discount_amounts' : + $value = &$this->coupon_discount_totals; + break; + case 'coupon_discount_tax_amounts' : + $value = &$this->coupon_discount_tax_totals; + break; + case 'fees' : + wc_deprecated_function( 'WC_Cart->fees', '3.2', sprintf( 'the fees API (%s)', 'WC_Cart::get_fees' ) ); + + // Grab fees from the new API. + $new_fees = $this->fees_api()->get_fees(); + + // Add new fees to the legacy prop so it can be adjusted via legacy property. + $this->fees = $new_fees; + + // Return by reference. + $value = &$this->fees; + break; + // Deprecated args. TODO: Remove in 4.0. + case 'tax' : + wc_deprecated_argument( 'WC_Cart->tax', '2.3', 'Use WC_Tax directly' ); + $this->tax = new WC_Tax(); + $value = $this->tax; + break; + 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/' ); + $value = 0; + break; + case 'tax_display_cart': + wc_deprecated_argument( 'WC_Cart->tax_display_cart', '4.4', 'Use WC_Cart->get_tax_price_display_mode() instead.' ); + $value = $this->get_tax_price_display_mode(); + break; + } + return $value; + } + + /** + * Map legacy variables to setters. + * + * @param string $name Property name. + * @param mixed $value Value to set. + */ + public function __set( $name, $value ) { + switch ( $name ) { + case 'cart_contents_total' : + $this->set_cart_contents_total( $value ); + break; + case 'total' : + $this->set_total( $value ); + break; + case 'subtotal' : + $this->set_subtotal( $value ); + break; + case 'subtotal_ex_tax' : + $this->set_subtotal( $value ); + break; + case 'tax_total' : + $this->set_cart_contents_tax( $value ); + $this->set_fee_tax( 0 ); + break; + case 'taxes' : + $this->set_cart_contents_taxes( $value ); + break; + case 'shipping_taxes' : + $this->set_shipping_taxes( $value ); + break; + case 'fee_total' : + $this->set_fee_total( $value ); + break; + case 'discount_cart' : + $this->set_discount_total( $value ); + break; + case 'discount_cart_tax' : + $this->set_discount_tax( $value ); + break; + case 'shipping_total' : + $this->set_shipping_total( $value ); + break; + case 'shipping_tax_total' : + $this->set_shipping_tax( $value ); + break; + case 'coupon_discount_amounts' : + $this->set_coupon_discount_totals( $value ); + break; + case 'coupon_discount_tax_amounts' : + $this->set_coupon_discount_tax_totals( $value ); + break; + case 'fees' : + wc_deprecated_function( 'WC_Cart->fees', '3.2', sprintf( 'the fees API (%s)', 'WC_Cart::add_fee' ) ); + $this->fees = $value; + break; + default : + $this->$name = $value; + break; + } + } + + /** + * Methods moved to session class in 3.2.0. + */ + public function get_cart_from_session() { $this->session->get_cart_from_session(); } + public function maybe_set_cart_cookies() { $this->session->maybe_set_cart_cookies(); } + public function set_session() { $this->session->set_session(); } + public function get_cart_for_session() { return $this->session->get_cart_for_session(); } + public function persistent_cart_update() { $this->session->persistent_cart_update(); } + public function persistent_cart_destroy() { $this->session->persistent_cart_destroy(); } + + /** + * Get the total of all cart discounts. + * + * @return float + */ + public function get_cart_discount_total() { + return $this->get_discount_total(); + } + + /** + * 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 $this->get_discount_tax(); + } + + /** + * Renamed for consistency. + * + * @param string $coupon_code + * @return bool True if the coupon is applied, false if it does not exist or cannot be applied. + */ + public function add_discount( $coupon_code ) { + return $this->apply_coupon( $coupon_code ); + } + /** + * Remove taxes. + * + * @deprecated 3.2.0 Taxes are never calculated if customer is tax except making this function unused. + */ + public function remove_taxes() { + wc_deprecated_function( 'WC_Cart::remove_taxes', '3.2', '' ); + } + /** + * Init. + * + * @deprecated 3.2.0 Session is loaded via hooks rather than directly. + */ + public function init() { + wc_deprecated_function( 'WC_Cart::init', '3.2', '' ); + $this->get_cart_from_session(); + } + + /** + * Function to apply discounts to a product and get the discounted price (before tax is applied). + * + * @deprecated 3.2.0 Calculation and coupon logic is handled in WC_Cart_Totals. + * @param mixed $values Cart item. + * @param mixed $price Price of item. + * @param bool $add_totals Legacy. + * @return float price + */ + public function get_discounted_price( $values, $price, $add_totals = false ) { + wc_deprecated_function( 'WC_Cart::get_discounted_price', '3.2', '' ); + + $cart_item_key = $values['key']; + $cart_item = $this->cart_contents[ $cart_item_key ]; + + return $cart_item['line_total']; + } + + /** + * 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() { + wc_deprecated_function( 'WC_Cart::get_cart_url', '2.5', 'wc_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() { + wc_deprecated_function( 'WC_Cart::get_checkout_url', '2.5', 'wc_get_checkout_url' ); + return wc_get_checkout_url(); + } + + /** + * 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() { + wc_deprecated_function( 'WC_Cart::ship_to_billing_address_only', '2.5', 'wc_ship_to_billing_address_only' ); + return wc_ship_to_billing_address_only(); + } + + /** + * Coupons enabled function. Filterable. + * + * @deprecated 2.5.0 + * @return bool + */ + public function coupons_enabled() { + wc_deprecated_function( 'WC_Legacy_Cart::coupons_enabled', '2.5.0', 'wc_coupons_enabled' ); + return wc_coupons_enabled(); + } + + /** + * Gets the total (product) discount amount - these are applied before tax. + * + * @deprecated 2.3.0 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 2.3.0 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 2.3.0 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 2.3.0 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 2.3.0 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/legacy/class-wc-legacy-coupon.php b/includes/legacy/class-wc-legacy-coupon.php new file mode 100644 index 0000000..95b3be4 --- /dev/null +++ b/includes/legacy/class-wc-legacy-coupon.php @@ -0,0 +1,204 @@ +get_id(); + break; + case 'exists' : + $value = $this->get_id() > 0; + 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 0000000..37ed592 --- /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 0000000..3c1c3a0 --- /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/legacy/class-wc-legacy-webhook.php b/includes/legacy/class-wc-legacy-webhook.php new file mode 100644 index 0000000..d1e7034 --- /dev/null +++ b/includes/legacy/class-wc-legacy-webhook.php @@ -0,0 +1,129 @@ +get_id(); + break; + case 'status' : + $value = $this->get_status(); + break; + case 'post_data' : + $value = null; + break; + case 'delivery_url' : + $value = $this->get_delivery_url(); + break; + case 'secret' : + $value = $this->get_secret(); + break; + case 'topic' : + $value = $this->get_topic(); + break; + case 'hooks' : + $value = $this->get_hooks(); + break; + case 'resource' : + $value = $this->get_resource(); + break; + case 'event' : + $value = $this->get_event(); + break; + case 'failure_count' : + $value = $this->get_failure_count(); + break; + case 'api_version' : + $value = $this->get_api_version(); + break; + + default : + $value = ''; + break; + } // End switch(). + + return $value; + } + + /** + * Get the post data for the webhook. + * + * @deprecated 3.2.0 + * @since 2.2 + * @return null|WP_Post + */ + public function get_post_data() { + wc_deprecated_function( 'WC_Webhook::get_post_data', '3.2' ); + + return null; + } + + /** + * Update the webhook status. + * + * @deprecated 3.2.0 + * @since 2.2.0 + * @param string $status Status to set. + */ + public function update_status( $status ) { + wc_deprecated_function( 'WC_Webhook::update_status', '3.2', 'WC_Webhook::set_status' ); + + $this->set_status( $status ); + $this->save(); + } +} diff --git a/includes/libraries/class-wc-eval-math.php b/includes/libraries/class-wc-eval-math.php new file mode 100644 index 0000000..11d8509 --- /dev/null +++ b/includes/libraries/class-wc-eval-math.php @@ -0,0 +1,405 @@ + 2.71, 'pi' => 3.14 ); + + /** + * User-defined functions. + * + * @var array + */ + public static $f = array(); + + /** + * Constants. + * + * @var array + */ + public static $vb = array( 'e', 'pi' ); + + /** + * Built-in functions. + * + * @var array + */ + public static $fb = array(); + + /** + * Evaluate maths string. + * + * @param string $expr + * @return mixed + */ + public static function evaluate( $expr ) { + self::$last_error = null; + $expr = trim( $expr ); + if ( substr( $expr, -1, 1 ) == ';' ) { + $expr = substr( $expr, 0, strlen( $expr ) -1 ); // strip semicolons at the end + } + // =============== + // is it a variable assignment? + if ( preg_match( '/^\s*([a-z]\w*)\s*=\s*(.+)$/', $expr, $matches ) ) { + if ( in_array( $matches[1], self::$vb ) ) { // make sure we're not assigning to a constant + return self::trigger( "cannot assign to constant '$matches[1]'" ); + } + if ( ( $tmp = self::pfx( self::nfx( $matches[2] ) ) ) === false ) { + return false; // get the result and make sure it's good + } + self::$v[ $matches[1] ] = $tmp; // if so, stick it in the variable array + return self::$v[ $matches[1] ]; // and return the resulting value + // =============== + // is it a function assignment? + } elseif ( preg_match( '/^\s*([a-z]\w*)\s*\(\s*([a-z]\w*(?:\s*,\s*[a-z]\w*)*)\s*\)\s*=\s*(.+)$/', $expr, $matches ) ) { + $fnn = $matches[1]; // get the function name + if ( in_array( $matches[1], self::$fb ) ) { // make sure it isn't built in + return self::trigger( "cannot redefine built-in function '$matches[1]()'" ); + } + $args = explode( ",", preg_replace( "/\s+/", "", $matches[2] ) ); // get the arguments + if ( ( $stack = self::nfx( $matches[3] ) ) === false ) { + return false; // see if it can be converted to postfix + } + $stack_size = count( $stack ); + for ( $i = 0; $i < $stack_size; $i++ ) { // freeze the state of the non-argument variables + $token = $stack[ $i ]; + if ( preg_match( '/^[a-z]\w*$/', $token ) and ! in_array( $token, $args ) ) { + if ( array_key_exists( $token, self::$v ) ) { + $stack[ $i ] = self::$v[ $token ]; + } else { + return self::trigger( "undefined variable '$token' in function definition" ); + } + } + } + self::$f[ $fnn ] = array( 'args' => $args, 'func' => $stack ); + return true; + // =============== + } else { + return self::pfx( self::nfx( $expr ) ); // straight up evaluation, woo + } + } + + /** + * Convert infix to postfix notation. + * + * @param string $expr + * + * @return array|string + */ + private static function nfx( $expr ) { + + $index = 0; + $stack = new WC_Eval_Math_Stack; + $output = array(); // postfix form of expression, to be passed to pfx() + $expr = trim( $expr ); + + $ops = array( '+', '-', '*', '/', '^', '_' ); + $ops_r = array( '+' => 0, '-' => 0, '*' => 0, '/' => 0, '^' => 1 ); // right-associative operator? + $ops_p = array( '+' => 0, '-' => 0, '*' => 1, '/' => 1, '_' => 1, '^' => 2 ); // operator precedence + + $expecting_op = false; // we use this in syntax-checking the expression + // and determining when a - is a negation + if ( preg_match( "/[^\w\s+*^\/()\.,-]/", $expr, $matches ) ) { // make sure the characters are all good + return self::trigger( "illegal character '{$matches[0]}'" ); + } + + while ( 1 ) { // 1 Infinite Loop ;) + $op = substr( $expr, $index, 1 ); // get the first character at the current index + // find out if we're currently at the beginning of a number/variable/function/parenthesis/operand + $ex = preg_match( '/^([A-Za-z]\w*\(?|\d+(?:\.\d*)?|\.\d+|\()/', substr( $expr, $index ), $match ); + // =============== + if ( '-' === $op and ! $expecting_op ) { // is it a negation instead of a minus? + $stack->push( '_' ); // put a negation on the stack + $index++; + } elseif ( '_' === $op ) { // we have to explicitly deny this, because it's legal on the stack + return self::trigger( "illegal character '_'" ); // but not in the input expression + // =============== + } elseif ( ( in_array( $op, $ops ) or $ex ) and $expecting_op ) { // are we putting an operator on the stack? + if ( $ex ) { // are we expecting an operator but have a number/variable/function/opening parenthesis? + $op = '*'; + $index--; // it's an implicit multiplication + } + // heart of the algorithm: + while ( $stack->count > 0 and ( $o2 = $stack->last() ) and in_array( $o2, $ops ) and ( $ops_r[ $op ] ? $ops_p[ $op ] < $ops_p[ $o2 ] : $ops_p[ $op ] <= $ops_p[ $o2 ] ) ) { + $output[] = $stack->pop(); // pop stuff off the stack into the output + } + // many thanks: https://en.wikipedia.org/wiki/Reverse_Polish_notation#The_algorithm_in_detail + $stack->push( $op ); // finally put OUR operator onto the stack + $index++; + $expecting_op = false; + // =============== + } elseif ( ')' === $op && $expecting_op ) { // ready to close a parenthesis? + while ( ( $o2 = $stack->pop() ) != '(' ) { // pop off the stack back to the last ( + if ( is_null( $o2 ) ) { + return self::trigger( "unexpected ')'" ); + } else { + $output[] = $o2; + } + } + if ( preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) { // did we just close a function? + $fnn = $matches[1]; // get the function name + $arg_count = $stack->pop(); // see how many arguments there were (cleverly stored on the stack, thank you) + $output[] = $stack->pop(); // pop the function and push onto the output + if ( in_array( $fnn, self::$fb ) ) { // check the argument count + if ( $arg_count > 1 ) { + return self::trigger( "too many arguments ($arg_count given, 1 expected)" ); + } + } elseif ( array_key_exists( $fnn, self::$f ) ) { + if ( count( self::$f[ $fnn ]['args'] ) != $arg_count ) { + return self::trigger( "wrong number of arguments ($arg_count given, " . count( self::$f[ $fnn ]['args'] ) . " expected)" ); + } + } else { // did we somehow push a non-function on the stack? this should never happen + return self::trigger( "internal error" ); + } + } + $index++; + // =============== + } elseif ( ',' === $op and $expecting_op ) { // did we just finish a function argument? + while ( ( $o2 = $stack->pop() ) != '(' ) { + if ( is_null( $o2 ) ) { + return self::trigger( "unexpected ','" ); // oops, never had a ( + } else { + $output[] = $o2; // pop the argument expression stuff and push onto the output + } + } + // make sure there was a function + if ( ! preg_match( "/^([A-Za-z]\w*)\($/", $stack->last( 2 ), $matches ) ) { + return self::trigger( "unexpected ','" ); + } + $stack->push( $stack->pop() + 1 ); // increment the argument count + $stack->push( '(' ); // put the ( back on, we'll need to pop back to it again + $index++; + $expecting_op = false; + // =============== + } elseif ( '(' === $op and ! $expecting_op ) { + $stack->push( '(' ); // that was easy + $index++; + // =============== + } elseif ( $ex and ! $expecting_op ) { // do we now have a function/variable/number? + $expecting_op = true; + $val = $match[1]; + if ( preg_match( "/^([A-Za-z]\w*)\($/", $val, $matches ) ) { // may be func, or variable w/ implicit multiplication against parentheses... + if ( in_array( $matches[1], self::$fb ) or array_key_exists( $matches[1], self::$f ) ) { // it's a func + $stack->push( $val ); + $stack->push( 1 ); + $stack->push( '(' ); + $expecting_op = false; + } else { // it's a var w/ implicit multiplication + $val = $matches[1]; + $output[] = $val; + } + } else { // it's a plain old var or num + $output[] = $val; + } + $index += strlen( $val ); + // =============== + } elseif ( ')' === $op ) { // miscellaneous error checking + return self::trigger( "unexpected ')'" ); + } elseif ( in_array( $op, $ops ) and ! $expecting_op ) { + return self::trigger( "unexpected operator '$op'" ); + } else { // I don't even want to know what you did to get here + return self::trigger( "an unexpected error occurred" ); + } + if ( strlen( $expr ) == $index ) { + if ( in_array( $op, $ops ) ) { // did we end with an operator? bad. + return self::trigger( "operator '$op' lacks operand" ); + } else { + break; + } + } + while ( substr( $expr, $index, 1 ) == ' ' ) { // step the index past whitespace (pretty much turns whitespace + $index++; // into implicit multiplication if no operator is there) + } + } + while ( ! is_null( $op = $stack->pop() ) ) { // pop everything off the stack and push onto output + if ( '(' === $op ) { + return self::trigger( "expecting ')'" ); // if there are (s on the stack, ()s were unbalanced + } + $output[] = $op; + } + return $output; + } + + /** + * Evaluate postfix notation. + * + * @param mixed $tokens + * @param array $vars + * + * @return mixed + */ + private static function pfx( $tokens, $vars = array() ) { + if ( false == $tokens ) { + return false; + } + $stack = new WC_Eval_Math_Stack; + + foreach ( $tokens as $token ) { // nice and easy + // if the token is a binary operator, pop two values off the stack, do the operation, and push the result back on + if ( in_array( $token, array( '+', '-', '*', '/', '^' ) ) ) { + if ( is_null( $op2 = $stack->pop() ) ) { + return self::trigger( "internal error" ); + } + if ( is_null( $op1 = $stack->pop() ) ) { + return self::trigger( "internal error" ); + } + switch ( $token ) { + case '+': + $stack->push( $op1 + $op2 ); + break; + case '-': + $stack->push( $op1 - $op2 ); + break; + case '*': + $stack->push( $op1 * $op2 ); + break; + case '/': + if ( 0 == $op2 ) { + return self::trigger( 'division by zero' ); + } + $stack->push( $op1 / $op2 ); + break; + case '^': + $stack->push( pow( $op1, $op2 ) ); + break; + } + // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on + } elseif ( '_' === $token ) { + $stack->push( -1 * $stack->pop() ); + // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on + } elseif ( ! preg_match( "/^([a-z]\w*)\($/", $token, $matches ) ) { + if ( is_numeric( $token ) ) { + $stack->push( $token ); + } elseif ( array_key_exists( $token, self::$v ) ) { + $stack->push( self::$v[ $token ] ); + } elseif ( array_key_exists( $token, $vars ) ) { + $stack->push( $vars[ $token ] ); + } else { + return self::trigger( "undefined variable '$token'" ); + } + } + } + // when we're out of tokens, the stack should have a single element, the final result + if ( 1 != $stack->count ) { + return self::trigger( "internal error" ); + } + return $stack->pop(); + } + + /** + * Trigger an error, but nicely, if need be. + * + * @param string $msg + * + * @return bool + */ + private static function trigger( $msg ) { + self::$last_error = $msg; + if ( ! Constants::is_true( 'DOING_AJAX' ) && Constants::is_true( 'WP_DEBUG' ) ) { + echo "\nError found in:"; + self::debugPrintCallingFunction(); + trigger_error( $msg, E_USER_WARNING ); + } + return false; + } + + /** + * Prints the file name, function name, and + * line number which called your function + * (not this function, then one that called + * it to begin with) + */ + private static function debugPrintCallingFunction() { + $file = 'n/a'; + $func = 'n/a'; + $line = 'n/a'; + $debugTrace = debug_backtrace(); + if ( isset( $debugTrace[1] ) ) { + $file = $debugTrace[1]['file'] ? $debugTrace[1]['file'] : 'n/a'; + $line = $debugTrace[1]['line'] ? $debugTrace[1]['line'] : 'n/a'; + } + if ( isset( $debugTrace[2] ) ) { + $func = $debugTrace[2]['function'] ? $debugTrace[2]['function'] : 'n/a'; + } + echo "\n$file, $func, $line\n"; + } + } + + /** + * Class WC_Eval_Math_Stack. + */ + class WC_Eval_Math_Stack { + + /** + * Stack array. + * + * @var array + */ + public $stack = array(); + + /** + * Stack counter. + * + * @var integer + */ + public $count = 0; + + /** + * Push value into stack. + * + * @param mixed $val + */ + public function push( $val ) { + $this->stack[ $this->count ] = $val; + $this->count++; + } + + /** + * Pop value from stack. + * + * @return mixed + */ + public function pop() { + if ( $this->count > 0 ) { + $this->count--; + return $this->stack[ $this->count ]; + } + return null; + } + + /** + * Get last value from stack. + * + * @param int $n + * + * @return mixed + */ + public function last( $n=1 ) { + $key = $this->count - $n; + return array_key_exists( $key, $this->stack ) ? $this->stack[ $key ] : null; + } + } +} diff --git a/includes/libraries/wp-async-request.php b/includes/libraries/wp-async-request.php new file mode 100644 index 0000000..320c93d --- /dev/null +++ b/includes/libraries/wp-async-request.php @@ -0,0 +1,160 @@ +identifier = $this->prefix . '_' . $this->action; + + add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); + add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); + } + + /** + * Set data used during the request + * + * @param array $data Data. + * + * @return $this + */ + public function data( $data ) { + $this->data = $data; + + return $this; + } + + /** + * Dispatch the async request + * + * @return array|WP_Error + */ + public function dispatch() { + $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); + $args = $this->get_post_args(); + + return wp_remote_post( esc_url_raw( $url ), $args ); + } + + /** + * Get query args + * + * @return array + */ + protected function get_query_args() { + if ( property_exists( $this, 'query_args' ) ) { + return $this->query_args; + } + + return array( + 'action' => $this->identifier, + 'nonce' => wp_create_nonce( $this->identifier ), + ); + } + + /** + * Get query URL + * + * @return string + */ + protected function get_query_url() { + if ( property_exists( $this, 'query_url' ) ) { + return $this->query_url; + } + + return admin_url( 'admin-ajax.php' ); + } + + /** + * Get post args + * + * @return array + */ + protected function get_post_args() { + if ( property_exists( $this, 'post_args' ) ) { + return $this->post_args; + } + + return array( + 'timeout' => 0.01, + 'blocking' => false, + 'body' => $this->data, + 'cookies' => $_COOKIE, + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ); + } + + /** + * Maybe handle + * + * Check for correct nonce and pass to handler. + */ + public function maybe_handle() { + // Don't lock up other requests while processing + session_write_close(); + + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); + } + + /** + * Handle + * + * Override this method to perform any actions required + * during the async request. + */ + abstract protected function handle(); + +} diff --git a/includes/libraries/wp-background-process.php b/includes/libraries/wp-background-process.php new file mode 100644 index 0000000..7e3f101 --- /dev/null +++ b/includes/libraries/wp-background-process.php @@ -0,0 +1,503 @@ +cron_hook_identifier = $this->identifier . '_cron'; + $this->cron_interval_identifier = $this->identifier . '_cron_interval'; + + add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); + add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); + } + + /** + * Dispatch + * + * @access public + * @return void + */ + public function dispatch() { + // Schedule the cron healthcheck. + $this->schedule_event(); + + // Perform remote post. + return parent::dispatch(); + } + + /** + * Push to queue + * + * @param mixed $data Data. + * + * @return $this + */ + public function push_to_queue( $data ) { + $this->data[] = $data; + + return $this; + } + + /** + * Save queue + * + * @return $this + */ + public function save() { + $key = $this->generate_key(); + + if ( ! empty( $this->data ) ) { + update_site_option( $key, $this->data ); + } + + return $this; + } + + /** + * Update queue + * + * @param string $key Key. + * @param array $data Data. + * + * @return $this + */ + public function update( $key, $data ) { + if ( ! empty( $data ) ) { + update_site_option( $key, $data ); + } + + return $this; + } + + /** + * Delete queue + * + * @param string $key Key. + * + * @return $this + */ + public function delete( $key ) { + delete_site_option( $key ); + + return $this; + } + + /** + * Generate key + * + * Generates a unique key based on microtime. Queue items are + * given a unique key so that they can be merged upon save. + * + * @param int $length Length. + * + * @return string + */ + protected function generate_key( $length = 64 ) { + $unique = md5( microtime() . rand() ); + $prepend = $this->identifier . '_batch_'; + + return substr( $prepend . $unique, 0, $length ); + } + + /** + * Maybe process queue + * + * Checks whether data exists within the queue and that + * the process is not already running. + */ + public function maybe_handle() { + // Don't lock up other requests while processing + session_write_close(); + + if ( $this->is_process_running() ) { + // Background process already running. + wp_die(); + } + + if ( $this->is_queue_empty() ) { + // No data to process. + wp_die(); + } + + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); + } + + /** + * Is queue empty + * + * @return bool + */ + protected function is_queue_empty() { + global $wpdb; + + $table = $wpdb->options; + $column = 'option_name'; + + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + } + + $key = $this->identifier . '_batch_%'; + + $count = $wpdb->get_var( $wpdb->prepare( " + SELECT COUNT(*) + FROM {$table} + WHERE {$column} LIKE %s + ", $key ) ); + + return ! ( $count > 0 ); + } + + /** + * Is process running + * + * Check whether the current process is already running + * in a background process. + */ + protected function is_process_running() { + if ( get_site_transient( $this->identifier . '_process_lock' ) ) { + // Process already running. + return true; + } + + return false; + } + + /** + * Lock process + * + * Lock the process so that multiple instances can't run simultaneously. + * Override if applicable, but the duration should be greater than that + * defined in the time_exceeded() method. + */ + protected function lock_process() { + $this->start_time = time(); // Set start time of current process. + + $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute + $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); + + set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); + } + + /** + * Unlock process + * + * Unlock the process so that other instances can spawn. + * + * @return $this + */ + protected function unlock_process() { + delete_site_transient( $this->identifier . '_process_lock' ); + + return $this; + } + + /** + * Get batch + * + * @return stdClass Return the first batch from the queue + */ + protected function get_batch() { + global $wpdb; + + $table = $wpdb->options; + $column = 'option_name'; + $key_column = 'option_id'; + $value_column = 'option_value'; + + if ( is_multisite() ) { + $table = $wpdb->sitemeta; + $column = 'meta_key'; + $key_column = 'meta_id'; + $value_column = 'meta_value'; + } + + $key = $this->identifier . '_batch_%'; + + $query = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$table} + WHERE {$column} LIKE %s + ORDER BY {$key_column} ASC + LIMIT 1 + ", $key ) ); + + $batch = new stdClass(); + $batch->key = $query->$column; + $batch->data = maybe_unserialize( $query->$value_column ); + + return $batch; + } + + /** + * Handle + * + * Pass each queue item to the task handler, while remaining + * within server memory and time limit constraints. + */ + protected function handle() { + $this->lock_process(); + + do { + $batch = $this->get_batch(); + + foreach ( $batch->data as $key => $value ) { + $task = $this->task( $value ); + + if ( false !== $task ) { + $batch->data[ $key ] = $task; + } else { + unset( $batch->data[ $key ] ); + } + + if ( $this->time_exceeded() || $this->memory_exceeded() ) { + // Batch limits reached. + break; + } + } + + // Update or delete current batch. + if ( ! empty( $batch->data ) ) { + $this->update( $batch->key, $batch->data ); + } else { + $this->delete( $batch->key ); + } + } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); + + $this->unlock_process(); + + // Start next batch or complete process. + if ( ! $this->is_queue_empty() ) { + $this->dispatch(); + } else { + $this->complete(); + } + + wp_die(); + } + + /** + * Memory exceeded + * + * Ensures the batch process never exceeds 90% + * of the maximum WordPress memory. + * + * @return bool + */ + protected function memory_exceeded() { + $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory + $current_memory = memory_get_usage( true ); + $return = false; + + if ( $current_memory >= $memory_limit ) { + $return = true; + } + + return apply_filters( $this->identifier . '_memory_exceeded', $return ); + } + + /** + * Get memory limit + * + * @return int + */ + protected function get_memory_limit() { + if ( function_exists( 'ini_get' ) ) { + $memory_limit = ini_get( 'memory_limit' ); + } else { + // Sensible default. + $memory_limit = '128M'; + } + + if ( ! $memory_limit || -1 === $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( $this->identifier . '_default_time_limit', 20 ); // 20 seconds + $return = false; + + if ( time() >= $finish ) { + $return = true; + } + + return apply_filters( $this->identifier . '_time_exceeded', $return ); + } + + /** + * Complete. + * + * Override if applicable, but ensure that the below actions are + * performed, or, call parent::complete(). + */ + protected function complete() { + // Unschedule the cron healthcheck. + $this->clear_scheduled_event(); + } + + /** + * Schedule cron healthcheck + * + * @access public + * @param mixed $schedules Schedules. + * @return mixed + */ + public function schedule_cron_healthcheck( $schedules ) { + $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); + + if ( property_exists( $this, 'cron_interval' ) ) { + $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); + } + + // Adds every 5 minutes to the existing schedules. + $schedules[ $this->identifier . '_cron_interval' ] = array( + 'interval' => MINUTE_IN_SECONDS * $interval, + 'display' => sprintf( __( 'Every %d minutes', 'woocommerce' ), $interval ), + ); + + return $schedules; + } + + /** + * Handle cron healthcheck + * + * Restart the background process if not already running + * and data exists in the queue. + */ + public function handle_cron_healthcheck() { + if ( $this->is_process_running() ) { + // Background process already running. + exit; + } + + if ( $this->is_queue_empty() ) { + // No data to process. + $this->clear_scheduled_event(); + exit; + } + + $this->handle(); + + exit; + } + + /** + * Schedule event + */ + protected function schedule_event() { + if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { + wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); + } + } + + /** + * Clear scheduled event + */ + protected function clear_scheduled_event() { + $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); + + if ( $timestamp ) { + wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); + } + } + + /** + * Cancel Process + * + * Stop processing queue items, clear cronjob and delete batch. + * + */ + public function cancel_process() { + if ( ! $this->is_queue_empty() ) { + $batch = $this->get_batch(); + + $this->delete( $batch->key ); + + wp_clear_scheduled_hook( $this->cron_hook_identifier ); + } + + } + + /** + * Task + * + * Override this method to perform any actions required on each + * queue item. Return the modified item for further processing + * in the next pass through. Or, return false to remove the + * item from the queue. + * + * @param mixed $item Queue item to iterate over. + * + * @return mixed + */ + abstract protected function task( $item ); + +} diff --git a/includes/log-handlers/class-wc-log-handler-db.php b/includes/log-handlers/class-wc-log-handler-db.php new file mode 100644 index 0000000..ef96133 --- /dev/null +++ b/includes/log-handlers/class-wc-log-handler-db.php @@ -0,0 +1,189 @@ +get_log_source(); + } + + return $this->add( $timestamp, $level, $message, $source, $context ); + } + + /** + * Add a log entry to chosen file. + * + * @param int $timestamp Log timestamp. + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @param string $message Log message. + * @param string $source Log source. Useful for filtering and sorting. + * @param array $context Context will be serialized and stored in database. + * + * @return bool True if write was successful. + */ + protected static function add( $timestamp, $level, $message, $source, $context ) { + global $wpdb; + + $insert = array( + 'timestamp' => date( 'Y-m-d H:i:s', $timestamp ), + 'level' => WC_Log_Levels::get_level_severity( $level ), + 'message' => $message, + 'source' => $source, + ); + + $format = array( + '%s', + '%d', + '%s', + '%s', + '%s', // possible serialized context. + ); + + if ( ! empty( $context ) ) { + $insert['context'] = serialize( $context ); // @codingStandardsIgnoreLine. + } + + return false !== $wpdb->insert( "{$wpdb->prefix}woocommerce_log", $insert, $format ); + } + + /** + * Clear all logs from the DB. + * + * @return bool True if flush was successful. + */ + public static function flush() { + global $wpdb; + + return $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_log" ); + } + + /** + * Clear entries for a chosen handle/source. + * + * @param string $source Log source. + * @return bool + */ + public function clear( $source ) { + global $wpdb; + + return $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE source = %s", + $source + ) + ); + } + + /** + * Delete selected logs from DB. + * + * @param int|string|array $log_ids Log ID or array of Log IDs to be deleted. + * + * @return bool + */ + public static function delete( $log_ids ) { + global $wpdb; + + if ( ! is_array( $log_ids ) ) { + $log_ids = array( $log_ids ); + } + + $format = array_fill( 0, count( $log_ids ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + return $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE log_id IN {$query_in}", $log_ids ) ); // @codingStandardsIgnoreLine. + } + + /** + * Delete all logs older than a defined timestamp. + * + * @since 3.4.0 + * @param integer $timestamp Timestamp to delete logs before. + */ + public static function delete_logs_before_timestamp( $timestamp = 0 ) { + if ( ! $timestamp ) { + return; + } + + global $wpdb; + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_log WHERE timestamp < %s", + date( 'Y-m-d H:i:s', $timestamp ) + ) + ); + } + + /** + * Get appropriate source based on file name. + * + * Try to provide an appropriate source in case none is provided. + * + * @return string Text to use as log source. "" (empty string) if none is found. + */ + protected static function get_log_source() { + static $ignore_files = array( 'class-wc-log-handler-db', 'class-wc-logger' ); + + /** + * PHP < 5.3.6 correct behavior + * + * @see http://php.net/manual/en/function.debug-backtrace.php#refsect1-function.debug-backtrace-parameters + */ + if ( Constants::is_defined( 'DEBUG_BACKTRACE_IGNORE_ARGS' ) ) { + $debug_backtrace_arg = DEBUG_BACKTRACE_IGNORE_ARGS; // phpcs:ignore PHPCompatibility.Constants.NewConstants.debug_backtrace_ignore_argsFound + } else { + $debug_backtrace_arg = false; + } + + $trace = debug_backtrace( $debug_backtrace_arg ); // @codingStandardsIgnoreLine. + foreach ( $trace as $t ) { + if ( isset( $t['file'] ) ) { + $filename = pathinfo( $t['file'], PATHINFO_FILENAME ); + if ( ! in_array( $filename, $ignore_files, true ) ) { + return $filename; + } + } + } + + return ''; + } + +} diff --git a/includes/log-handlers/class-wc-log-handler-email.php b/includes/log-handlers/class-wc-log-handler-email.php new file mode 100644 index 0000000..d3d6a68 --- /dev/null +++ b/includes/log-handlers/class-wc-log-handler-email.php @@ -0,0 +1,226 @@ +add_email( $recipient ); + } + } else { + $this->add_email( $recipients ); + } + + $this->set_threshold( $threshold ); + add_action( 'shutdown', array( $this, 'send_log_email' ) ); + } + + /** + * Set handler severity threshold. + * + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + */ + public function set_threshold( $level ) { + $this->threshold = WC_Log_Levels::get_level_severity( $level ); + } + + /** + * Determine whether handler should handle log. + * + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @return bool True if the log should be handled. + */ + protected function should_handle( $level ) { + return $this->threshold <= WC_Log_Levels::get_level_severity( $level ); + } + + /** + * Handle a log entry. + * + * @param int $timestamp Log timestamp. + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @param string $message Log message. + * @param array $context Optional. Additional information for log handlers. + * + * @return bool False if value was not handled and true if value was handled. + */ + public function handle( $timestamp, $level, $message, $context ) { + + if ( $this->should_handle( $level ) ) { + $this->add_log( $timestamp, $level, $message, $context ); + return true; + } + + return false; + } + + /** + * Send log email. + * + * @return bool True if email is successfully sent otherwise false. + */ + public function send_log_email() { + $result = false; + + if ( ! empty( $this->logs ) ) { + $subject = $this->get_subject(); + $body = $this->get_body(); + $result = wp_mail( $this->recipients, $subject, $body ); + $this->clear_logs(); + } + + return $result; + } + + /** + * Build subject for log email. + * + * @return string subject + */ + protected function get_subject() { + $site_name = get_bloginfo( 'name' ); + $max_level = strtoupper( WC_Log_Levels::get_severity_level( $this->max_severity ) ); + $log_count = count( $this->logs ); + + return sprintf( + /* translators: 1: Site name 2: Maximum level 3: Log count */ + _n( + '[%1$s] %2$s: %3$s WooCommerce log message', + '[%1$s] %2$s: %3$s WooCommerce log messages', + $log_count, + 'woocommerce' + ), + $site_name, + $max_level, + $log_count + ); + } + + /** + * Build body for log email. + * + * @return string body + */ + protected function get_body() { + $site_name = get_bloginfo( 'name' ); + $entries = implode( PHP_EOL, $this->logs ); + $log_count = count( $this->logs ); + return _n( + 'You have received the following WooCommerce log message:', + 'You have received the following WooCommerce log messages:', + $log_count, + 'woocommerce' + ) . PHP_EOL + . PHP_EOL + . $entries + . PHP_EOL + . PHP_EOL + /* translators: %s: Site name */ + . sprintf( __( 'Visit %s admin area:', 'woocommerce' ), $site_name ) + . PHP_EOL + . admin_url(); + } + + /** + * Adds an email to the list of recipients. + * + * @param string $email Email address to add. + */ + public function add_email( $email ) { + array_push( $this->recipients, $email ); + } + + /** + * Add log message. + * + * @param int $timestamp Log timestamp. + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @param string $message Log message. + * @param array $context Additional information for log handlers. + */ + protected function add_log( $timestamp, $level, $message, $context ) { + $this->logs[] = $this->format_entry( $timestamp, $level, $message, $context ); + + $log_severity = WC_Log_Levels::get_level_severity( $level ); + if ( $this->max_severity < $log_severity ) { + $this->max_severity = $log_severity; + } + } + + /** + * Clear log messages. + */ + protected function clear_logs() { + $this->logs = array(); + } + +} diff --git a/includes/log-handlers/class-wc-log-handler-file.php b/includes/log-handlers/class-wc-log-handler-file.php new file mode 100644 index 0000000..49347b5 --- /dev/null +++ b/includes/log-handlers/class-wc-log-handler-file.php @@ -0,0 +1,446 @@ +log_size_limit = apply_filters( 'woocommerce_log_file_size_limit', $log_size_limit ); + + add_action( 'plugins_loaded', array( $this, 'write_cached_logs' ) ); + } + + /** + * Destructor. + * + * Cleans up open file handles. + */ + public function __destruct() { + foreach ( $this->handles as $handle ) { + if ( is_resource( $handle ) ) { + fclose( $handle ); // @codingStandardsIgnoreLine. + } + } + } + + /** + * Handle a log entry. + * + * @param int $timestamp Log timestamp. + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @param string $message Log message. + * @param array $context { + * Additional information for log handlers. + * + * @type string $source Optional. Determines log file to write to. Default 'log'. + * @type bool $_legacy Optional. Default false. True to use outdated log format + * originally used in deprecated WC_Logger::add calls. + * } + * + * @return bool False if value was not handled and true if value was handled. + */ + public function handle( $timestamp, $level, $message, $context ) { + + if ( isset( $context['source'] ) && $context['source'] ) { + $handle = $context['source']; + } else { + $handle = 'log'; + } + + $entry = self::format_entry( $timestamp, $level, $message, $context ); + + return $this->add( $entry, $handle ); + } + + /** + * Builds a log entry text from timestamp, level and message. + * + * @param int $timestamp Log timestamp. + * @param string $level emergency|alert|critical|error|warning|notice|info|debug. + * @param string $message Log message. + * @param array $context Additional information for log handlers. + * + * @return string Formatted log entry. + */ + protected static function format_entry( $timestamp, $level, $message, $context ) { + + if ( isset( $context['_legacy'] ) && true === $context['_legacy'] ) { + if ( isset( $context['source'] ) && $context['source'] ) { + $handle = $context['source']; + } else { + $handle = 'log'; + } + $message = apply_filters( 'woocommerce_logger_add_message', $message, $handle ); + $time = date_i18n( 'm-d-Y @ H:i:s' ); + $entry = "{$time} - {$message}"; + } else { + $entry = parent::format_entry( $timestamp, $level, $message, $context ); + } + + return $entry; + } + + /** + * Open log file for writing. + * + * @param string $handle Log handle. + * @param string $mode Optional. File mode. Default 'a'. + * @return bool Success. + */ + protected function open( $handle, $mode = 'a' ) { + if ( $this->is_open( $handle ) ) { + return true; + } + + $file = self::get_log_file_path( $handle ); + + if ( $file ) { + if ( ! file_exists( $file ) ) { + $temphandle = @fopen( $file, 'w+' ); // @codingStandardsIgnoreLine. + if ( is_resource( $temphandle ) ) { + @fclose( $temphandle ); // @codingStandardsIgnoreLine. + + if ( Constants::is_defined( 'FS_CHMOD_FILE' ) ) { + @chmod( $file, FS_CHMOD_FILE ); // @codingStandardsIgnoreLine. + } + } + } + + $resource = @fopen( $file, $mode ); // @codingStandardsIgnoreLine. + + if ( $resource ) { + $this->handles[ $handle ] = $resource; + return true; + } + } + + return false; + } + + /** + * Check if a handle is open. + * + * @param string $handle Log handle. + * @return bool True if $handle is open. + */ + protected function is_open( $handle ) { + return array_key_exists( $handle, $this->handles ) && is_resource( $this->handles[ $handle ] ); + } + + /** + * Close a handle. + * + * @param string $handle Log handle. + * @return bool success + */ + protected function close( $handle ) { + $result = false; + + if ( $this->is_open( $handle ) ) { + $result = fclose( $this->handles[ $handle ] ); // @codingStandardsIgnoreLine. + unset( $this->handles[ $handle ] ); + } + + return $result; + } + + /** + * Add a log entry to chosen file. + * + * @param string $entry Log entry text. + * @param string $handle Log entry handle. + * + * @return bool True if write was successful. + */ + protected function add( $entry, $handle ) { + $result = false; + + if ( $this->should_rotate( $handle ) ) { + $this->log_rotate( $handle ); + } + + if ( $this->open( $handle ) && is_resource( $this->handles[ $handle ] ) ) { + $result = fwrite( $this->handles[ $handle ], $entry . PHP_EOL ); // @codingStandardsIgnoreLine. + } else { + $this->cache_log( $entry, $handle ); + } + + return false !== $result; + } + + /** + * Clear entries from chosen file. + * + * @param string $handle Log handle. + * + * @return bool + */ + public function clear( $handle ) { + $result = false; + + // Close the file if it's already open. + $this->close( $handle ); + + /** + * $this->open( $handle, 'w' ) == Open the file for writing only. Place the file pointer at + * the beginning of the file, and truncate the file to zero length. + */ + if ( $this->open( $handle, 'w' ) && is_resource( $this->handles[ $handle ] ) ) { + $result = true; + } + + do_action( 'woocommerce_log_clear', $handle ); + + return $result; + } + + /** + * Remove/delete the chosen file. + * + * @param string $handle Log handle. + * + * @return bool + */ + public function remove( $handle ) { + $removed = false; + $logs = $this->get_log_files(); + $handle = sanitize_title( $handle ); + + if ( isset( $logs[ $handle ] ) && $logs[ $handle ] ) { + $file = realpath( trailingslashit( WC_LOG_DIR ) . $logs[ $handle ] ); + if ( 0 === stripos( $file, realpath( trailingslashit( WC_LOG_DIR ) ) ) && is_file( $file ) && is_writable( $file ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable + $this->close( $file ); // Close first to be certain no processes keep it alive after it is unlinked. + $removed = unlink( $file ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_unlink + } + do_action( 'woocommerce_log_remove', $handle, $removed ); + } + return $removed; + } + + /** + * Check if log file should be rotated. + * + * Compares the size of the log file to determine whether it is over the size limit. + * + * @param string $handle Log handle. + * @return bool True if if should be rotated. + */ + protected function should_rotate( $handle ) { + $file = self::get_log_file_path( $handle ); + if ( $file ) { + if ( $this->is_open( $handle ) ) { + $file_stat = fstat( $this->handles[ $handle ] ); + return $file_stat['size'] > $this->log_size_limit; + } elseif ( file_exists( $file ) ) { + return filesize( $file ) > $this->log_size_limit; + } else { + return false; + } + } else { + return false; + } + } + + /** + * Rotate log files. + * + * Logs are rotated by prepending '.x' to the '.log' suffix. + * The current log plus 10 historical logs are maintained. + * For example: + * base.9.log -> [ REMOVED ] + * base.8.log -> base.9.log + * ... + * base.0.log -> base.1.log + * base.log -> base.0.log + * + * @param string $handle Log handle. + */ + protected function log_rotate( $handle ) { + for ( $i = 8; $i >= 0; $i-- ) { + $this->increment_log_infix( $handle, $i ); + } + $this->increment_log_infix( $handle ); + } + + /** + * Increment a log file suffix. + * + * @param string $handle Log handle. + * @param null|int $number Optional. Default null. Log suffix number to be incremented. + * @return bool True if increment was successful, otherwise false. + */ + protected function increment_log_infix( $handle, $number = null ) { + if ( null === $number ) { + $suffix = ''; + $next_suffix = '.0'; + } else { + $suffix = '.' . $number; + $next_suffix = '.' . ( $number + 1 ); + } + + $rename_from = self::get_log_file_path( "{$handle}{$suffix}" ); + $rename_to = self::get_log_file_path( "{$handle}{$next_suffix}" ); + + if ( $this->is_open( $rename_from ) ) { + $this->close( $rename_from ); + } + + if ( is_writable( $rename_from ) ) { // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable + return rename( $rename_from, $rename_to ); // phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_rename + } else { + return false; + } + + } + + /** + * Get a log file path. + * + * @param string $handle Log name. + * @return bool|string The log file path or false if path cannot be determined. + */ + public static function get_log_file_path( $handle ) { + if ( function_exists( 'wp_hash' ) ) { + return trailingslashit( WC_LOG_DIR ) . self::get_log_file_name( $handle ); + } else { + wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.0' ); + return false; + } + } + + /** + * Get a log file name. + * + * File names consist of the handle, followed by the date, followed by a hash, .log. + * + * @since 3.3 + * @param string $handle Log name. + * @return bool|string The log file name or false if cannot be determined. + */ + public static function get_log_file_name( $handle ) { + if ( function_exists( 'wp_hash' ) ) { + $date_suffix = date( 'Y-m-d', time() ); + $hash_suffix = wp_hash( $handle ); + return sanitize_file_name( implode( '-', array( $handle, $date_suffix, $hash_suffix ) ) . '.log' ); + } else { + wc_doing_it_wrong( __METHOD__, __( 'This method should not be called before plugins_loaded.', 'woocommerce' ), '3.3' ); + return false; + } + } + + /** + * Cache log to write later. + * + * @param string $entry Log entry text. + * @param string $handle Log entry handle. + */ + protected function cache_log( $entry, $handle ) { + $this->cached_logs[] = array( + 'entry' => $entry, + 'handle' => $handle, + ); + } + + /** + * Write cached logs. + */ + public function write_cached_logs() { + foreach ( $this->cached_logs as $log ) { + $this->add( $log['entry'], $log['handle'] ); + } + } + + /** + * Delete all logs older than a defined timestamp. + * + * @since 3.4.0 + * @param integer $timestamp Timestamp to delete logs before. + */ + public static function delete_logs_before_timestamp( $timestamp = 0 ) { + if ( ! $timestamp ) { + return; + } + + $log_files = self::get_log_files(); + + foreach ( $log_files as $log_file ) { + $last_modified = filemtime( trailingslashit( WC_LOG_DIR ) . $log_file ); + + if ( $last_modified < $timestamp ) { + @unlink( trailingslashit( WC_LOG_DIR ) . $log_file ); // @codingStandardsIgnoreLine. + } + } + } + + /** + * Get all log files in the log directory. + * + * @since 3.4.0 + * @return array + */ + public static function get_log_files() { + $files = @scandir( WC_LOG_DIR ); // @codingStandardsIgnoreLine. + $result = array(); + + if ( ! empty( $files ) ) { + foreach ( $files as $key => $value ) { + if ( ! in_array( $value, array( '.', '..' ), true ) ) { + if ( ! is_dir( $value ) && strstr( $value, '.log' ) ) { + $result[ sanitize_title( $value ) ] = $value; + } + } + } + } + + return $result; + } +} diff --git a/includes/payment-tokens/class-wc-payment-token-cc.php b/includes/payment-tokens/class-wc-payment-token-cc.php new file mode 100644 index 0000000..f658389 --- /dev/null +++ b/includes/payment-tokens/class-wc-payment-token-cc.php @@ -0,0 +1,198 @@ + '', + 'expiry_year' => '', + 'expiry_month' => '', + 'card_type' => '', + ); + + /** + * Get type to display to user. + * + * @since 2.6.0 + * @param string $deprecated Deprecated since WooCommerce 3.0. + * @return string + */ + public function get_display_name( $deprecated = '' ) { + $display = sprintf( + /* translators: 1: credit card type 2: last 4 digits 3: expiry month 4: expiry year */ + __( '%1$s ending in %2$s (expires %3$s/%4$s)', 'woocommerce' ), + wc_get_credit_card_type_label( $this->get_card_type() ), + $this->get_last4(), + $this->get_expiry_month(), + substr( $this->get_expiry_year(), 2 ) + ); + return $display; + } + + /** + * Hook prefix + * + * @since 3.0.0 + */ + protected function get_hook_prefix() { + return 'woocommerce_payment_token_cc_get_'; + } + + /** + * Validate credit card payment tokens. + * + * These fields are required by all credit card payment tokens: + * expiry_month - string Expiration date (MM) for the card + * expiry_year - string Expiration date (YYYY) for the card + * last4 - string Last 4 digits of the card + * card_type - string Card type (visa, mastercard, etc) + * + * @since 2.6.0 + * @return boolean True if the passed data is valid + */ + public function validate() { + if ( false === parent::validate() ) { + return false; + } + + if ( ! $this->get_last4( 'edit' ) ) { + return false; + } + + if ( ! $this->get_expiry_year( 'edit' ) ) { + return false; + } + + if ( ! $this->get_expiry_month( 'edit' ) ) { + return false; + } + + if ( ! $this->get_card_type( 'edit' ) ) { + return false; + } + + if ( 4 !== strlen( $this->get_expiry_year( 'edit' ) ) ) { + return false; + } + + if ( 2 !== strlen( $this->get_expiry_month( 'edit' ) ) ) { + return false; + } + + return true; + } + + /** + * Returns the card type (mastercard, visa, ...). + * + * @since 2.6.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string Card type + */ + public function get_card_type( $context = 'view' ) { + return $this->get_prop( 'card_type', $context ); + } + + /** + * Set the card type (mastercard, visa, ...). + * + * @since 2.6.0 + * @param string $type Credit card type (mastercard, visa, ...). + */ + public function set_card_type( $type ) { + $this->set_prop( 'card_type', $type ); + } + + /** + * Returns the card expiration year (YYYY). + * + * @since 2.6.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string Expiration year + */ + public function get_expiry_year( $context = 'view' ) { + return $this->get_prop( 'expiry_year', $context ); + } + + /** + * Set the expiration year for the card (YYYY format). + * + * @since 2.6.0 + * @param string $year Credit card expiration year. + */ + public function set_expiry_year( $year ) { + $this->set_prop( 'expiry_year', $year ); + } + + /** + * Returns the card expiration month (MM). + * + * @since 2.6.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string Expiration month + */ + public function get_expiry_month( $context = 'view' ) { + return $this->get_prop( 'expiry_month', $context ); + } + + /** + * Set the expiration month for the card (formats into MM format). + * + * @since 2.6.0 + * @param string $month Credit card expiration month. + */ + public function set_expiry_month( $month ) { + $this->set_prop( 'expiry_month', str_pad( $month, 2, '0', STR_PAD_LEFT ) ); + } + + /** + * Returns the last four digits. + * + * @since 2.6.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string Last 4 digits + */ + public function get_last4( $context = 'view' ) { + return $this->get_prop( 'last4', $context ); + } + + /** + * Set the last four digits. + * + * @since 2.6.0 + * @param string $last4 Credit card last four digits. + */ + public function set_last4( $last4 ) { + $this->set_prop( 'last4', $last4 ); + } +} diff --git a/includes/payment-tokens/class-wc-payment-token-echeck.php b/includes/payment-tokens/class-wc-payment-token-echeck.php new file mode 100644 index 0000000..4e1e21e --- /dev/null +++ b/includes/payment-tokens/class-wc-payment-token-echeck.php @@ -0,0 +1,105 @@ + '', + ); + + /** + * Get type to display to user. + * + * @since 2.6.0 + * @param string $deprecated Deprecated since WooCommerce 3.0. + * @return string + */ + public function get_display_name( $deprecated = '' ) { + $display = sprintf( + /* translators: 1: last 4 digits */ + __( 'eCheck ending in %1$s', 'woocommerce' ), + $this->get_last4() + ); + return $display; + } + + /** + * Hook prefix + * + * @since 3.0.0 + */ + protected function get_hook_prefix() { + return 'woocommerce_payment_token_echeck_get_'; + } + + /** + * Validate eCheck payment tokens. + * + * These fields are required by all eCheck payment tokens: + * last4 - string Last 4 digits of the check + * + * @since 2.6.0 + * @return boolean True if the passed data is valid + */ + public function validate() { + if ( false === parent::validate() ) { + return false; + } + + if ( ! $this->get_last4( 'edit' ) ) { + return false; + } + return true; + } + + /** + * Returns the last four digits. + * + * @since 2.6.0 + * @param string $context What the value is for. Valid values are view and edit. + * @return string Last 4 digits + */ + public function get_last4( $context = 'view' ) { + return $this->get_prop( 'last4', $context ); + } + + /** + * Set the last four digits. + * + * @since 2.6.0 + * @param string $last4 eCheck last four digits. + */ + public function set_last4( $last4 ) { + $this->set_prop( 'last4', $last4 ); + } +} diff --git a/includes/queue/class-wc-action-queue.php b/includes/queue/class-wc-action-queue.php new file mode 100644 index 0000000..88bccae --- /dev/null +++ b/includes/queue/class-wc-action-queue.php @@ -0,0 +1,160 @@ +schedule_single( time(), $hook, $args, $group ); + } + + /** + * Schedule an action to run once at some time in the future + * + * @param int $timestamp When the job will run. + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * @return string The action ID. + */ + public function schedule_single( $timestamp, $hook, $args = array(), $group = '' ) { + return as_schedule_single_action( $timestamp, $hook, $args, $group ); + } + + /** + * Schedule a recurring action + * + * @param int $timestamp When the first instance of the job will run. + * @param int $interval_in_seconds How long to wait between runs. + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * @return string The action ID. + */ + public function schedule_recurring( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { + return as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args, $group ); + } + + /** + * Schedule an action that recurs on a cron-like schedule. + * + * @param int $timestamp The schedule will start on or after this time. + * @param string $cron_schedule A cron-link schedule string. + * @see http://en.wikipedia.org/wiki/Cron + * * * * * * * + * ┬ ┬ ┬ ┬ ┬ ┬ + * | | | | | | + * | | | | | + year [optional] + * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) + * | | | +---------- month (1 - 12) + * | | +--------------- day of month (1 - 31) + * | +-------------------- hour (0 - 23) + * +------------------------- min (0 - 59) + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * @return string The action ID + */ + public function schedule_cron( $timestamp, $cron_schedule, $hook, $args = array(), $group = '' ) { + return as_schedule_cron_action( $timestamp, $cron_schedule, $hook, $args, $group ); + } + + /** + * Dequeue the next scheduled instance of an action with a matching hook (and optionally matching args and group). + * + * Any recurring actions with a matching hook should also be cancelled, not just the next scheduled action. + * + * While technically only the next instance of a recurring or cron action is unscheduled by this method, that will also + * prevent all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled + * in a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled + * only after the former action is run. As the next instance is never run, because it's unscheduled by this function, + * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled + * by this method also. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Args that would have been passed to the job. + * @param string $group The group the job is assigned to (if any). + */ + public function cancel( $hook, $args = array(), $group = '' ) { + as_unschedule_action( $hook, $args, $group ); + } + + /** + * Dequeue all actions with a matching hook (and optionally matching args and group) so no matching actions are ever run. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Args that would have been passed to the job. + * @param string $group The group the job is assigned to (if any). + */ + public function cancel_all( $hook, $args = array(), $group = '' ) { + as_unschedule_all_actions( $hook, $args, $group ); + } + + /** + * Get the date and time for the next scheduled occurence of an action with a given hook + * (an optionally that matches certain args and group), if any. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Filter to a hook with matching args that will be passed to the job when it runs. + * @param string $group Filter to only actions assigned to a specific group. + * @return WC_DateTime|null The date and time for the next occurrence, or null if there is no pending, scheduled action for the given hook. + */ + public function get_next( $hook, $args = null, $group = '' ) { + + $next_timestamp = as_next_scheduled_action( $hook, $args, $group ); + + if ( is_numeric( $next_timestamp ) ) { + return new WC_DateTime( "@{$next_timestamp}", new DateTimeZone( 'UTC' ) ); + } + + return null; + } + + /** + * Find scheduled actions + * + * @param array $args Possible arguments, with their default values: + * 'hook' => '' - the name of the action that will be triggered + * 'args' => null - the args array that will be passed with the action + * 'date' => null - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'modified' => null - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'group' => '' - the group the action belongs to + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING + * 'claimed' => null - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID + * 'per_page' => 5 - Number of results to return + * 'offset' => 0 + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' + * 'order' => 'ASC'. + * + * @param string $return_format OBJECT, ARRAY_A, or ids. + * @return array + */ + public function search( $args = array(), $return_format = OBJECT ) { + return as_get_scheduled_actions( $args, $return_format ); + } +} diff --git a/includes/queue/class-wc-queue.php b/includes/queue/class-wc-queue.php new file mode 100644 index 0000000..80dd4b6 --- /dev/null +++ b/includes/queue/class-wc-queue.php @@ -0,0 +1,82 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'record_usage_data' ), + 'permission_callback' => array( $this, 'telemetry_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to post telemetry data + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function telemetry_permissions_check( $request ) { + if ( ! is_user_logged_in() ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you post telemetry data.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Record WCTracker Data + * + * @param WP_REST_Request $request Full details about the request. + */ + public function record_usage_data( $request ) { + $new = $this->get_usage_data( $request ); + if ( ! $new || ! $new['platform'] ) { + return; + } + + $data = get_option( 'woocommerce_mobile_app_usage' ); + if ( ! $data ) { + $data = array(); + } + + $platform = $new['platform']; + if ( ! $data[ $platform ] || version_compare( $new['version'], $data[ $platform ]['version'], '>=' ) ) { + $data[ $platform ] = $new; + } + + update_option( 'woocommerce_mobile_app_usage', $data ); + } + + /** + * Get usage data from current request + * + * @param WP_REST_Request $request Full details about the request. + * @return Array + */ + public function get_usage_data( $request ) { + $platform = strtolower( $request->get_param( 'platform' ) ); + switch ( $platform ) { + case 'ios': + case 'android': + break; + default: + return; + } + + $version = $request->get_param( 'version' ); + if ( ! $version ) { + return; + } + + return array( + 'platform' => sanitize_text_field( $platform ), + 'version' => sanitize_text_field( $version ), + 'last_used' => gmdate( 'c' ), + ); + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'platform' => array( + 'description' => __( 'Platform to track.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + 'version' => array( + 'description' => __( 'Platform version to track.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php new file mode 100644 index 0000000..14c4d3a --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php @@ -0,0 +1,580 @@ +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 return writable 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' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'individual_use' => array( + 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( "List of product IDs the coupon can be used on.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + '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/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php new file mode 100644 index 0000000..6b0511f --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php @@ -0,0 +1,252 @@ +/downloads endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Customers controller class. + * + * @package WooCommerce\RestApi + * @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/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php new file mode 100644 index 0000000..8a07bb3 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php @@ -0,0 +1,924 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'New user email address.', 'woocommerce' ), + ), + 'username' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), + 'description' => __( 'New user username.', 'woocommerce' ), + 'type' => 'string', + ), + 'password' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), + 'description' => __( 'New user password.', 'woocommerce' ), + 'type' => 'string', + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array( + 'default' => 0, + 'type' => 'integer', + 'description' => __( 'ID to reassign posts to.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create customers. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $prepared_args = array(); + $prepared_args['exclude'] = $request['exclude']; + $prepared_args['include'] = $request['include']; + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'ID', + 'include' => 'include', + 'name' => 'display_name', + 'registered_date' => 'registered', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['search'] = $request['search']; + + if ( '' !== $prepared_args['search'] ) { + $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; + } + + // Filter by email. + if ( ! empty( $request['email'] ) ) { + $prepared_args['search'] = $request['email']; + $prepared_args['search_columns'] = array( 'user_email' ); + } + + // Filter by role. + if ( 'all' !== $request['role'] ) { + $prepared_args['role'] = $request['role']; + } + + /** + * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. + * + * @see https://developer.wordpress.org/reference/classes/wp_user_query/ + * + * @param array $prepared_args Array of arguments for WP_User_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); + + $query = new WP_User_Query( $prepared_args ); + + $users = array(); + foreach ( $query->results as $user ) { + $data = $this->prepare_item_for_response( $user, $request ); + $users[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $users ); + + // Store pagination values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $prepared_args['fields'] = 'ID'; + + $total_users = $query->get_total(); + if ( $total_users < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'] ); + unset( $prepared_args['offset'] ); + $count_query = new WP_User_Query( $prepared_args ); + $total_users = $count_query->get_total(); + } + $response->header( 'X-WP-Total', (int) $total_users ); + $max_pages = ceil( $total_users / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + try { + if ( ! empty( $request['id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 ); + } + + // Sets the username. + $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; + + // Sets the password. + $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; + + // Create customer. + $customer = new WC_Customer; + $customer->set_username( $request['username'] ); + $customer->set_password( $request['password'] ); + $customer->set_email( $request['email'] ); + $this->update_customer_meta_fields( $customer, $request ); + $customer->save(); + + if ( ! $customer->get_id() ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); + } + + $user_data = get_userdata( $customer->get_id() ); + $this->update_additional_fields_for_object( $user_data, $request ); + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $user_data Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) ); + + return $response; + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $user_data = get_userdata( $id ); + + if ( empty( $id ) || empty( $user_data->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $customer = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $customer ); + + return $response; + } + + /** + * Update a single user. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $id = (int) $request['id']; + $customer = new WC_Customer( $id ); + + if ( ! $customer->get_id() ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 ); + } + + // Customer email. + if ( isset( $request['email'] ) ) { + $customer->set_email( sanitize_email( $request['email'] ) ); + } + + // Customer password. + if ( isset( $request['password'] ) ) { + $customer->set_password( $request['password'] ); + } + + $this->update_customer_meta_fields( $customer, $request ); + $customer->save(); + + $user_data = get_userdata( $customer->get_id() ); + $this->update_additional_fields_for_object( $user_data, $request ); + + if ( ! is_user_member_of_blog( $user_data->ID ) ) { + $user_data->add_role( 'customer' ); + } + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $customer Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $response ); + return $response; + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $user_data = get_userdata( $id ); + if ( ! $user_data ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $reassign ) ) { + if ( $reassign === $id || ! get_userdata( $reassign ) ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + + /** Include admin customer functions to get access to wp_delete_user() */ + require_once ABSPATH . 'wp-admin/includes/user.php'; + + $customer = new WC_Customer( $id ); + + if ( ! is_null( $reassign ) ) { + $result = $customer->delete_and_reassign( $reassign ); + } else { + $result = $customer->delete(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a customer is deleted via the REST API. + * + * @param WP_User $user_data User data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request ); + + return $response; + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $user_data User object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $user_data, $request ) { + $customer = new WC_Customer( $user_data->ID ); + $_data = $customer->get_data(); + $last_order = wc_get_customer_last_order( $customer->get_id() ); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; // v1 API used UTC. + } + + $data = array( + 'id' => $_data['id'], + 'date_created' => $_data['date_created'], + 'date_modified' => $_data['date_modified'], + 'email' => $_data['email'], + 'first_name' => $_data['first_name'], + 'last_name' => $_data['last_name'], + 'username' => $_data['username'], + 'last_order' => array( + 'id' => is_object( $last_order ) ? $last_order->get_id() : null, + 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->get_date_created() ) : null, // v1 API used UTC. + ), + 'orders_count' => $customer->get_order_count(), + 'total_spent' => $customer->get_total_spent(), + 'avatar_url' => $customer->get_avatar_url(), + 'billing' => $_data['billing'], + 'shipping' => $_data['shipping'], + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $user_data ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $user_data User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); + } + + /** + * Update customer meta fields. + * + * @param WC_Customer $customer + * @param WP_REST_Request $request + */ + protected function update_customer_meta_fields( $customer, $request ) { + $schema = $this->get_item_schema(); + + // Customer first name. + if ( isset( $request['first_name'] ) ) { + $customer->set_first_name( wc_clean( $request['first_name'] ) ); + } + + // Customer last name. + if ( isset( $request['last_name'] ) ) { + $customer->set_last_name( wc_clean( $request['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $request['billing'] ) ) { + foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) { + if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) { + $customer->{"set_billing_{$field}"}( $request['billing'][ $field ] ); + } + } + } + + // Customer shipping address. + if ( isset( $request['shipping'] ) ) { + foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) { + if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { + $customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ] ); + } + } + } + } + + /** + * Prepare links for the request. + * + * @param WP_User $customer Customer object. + * @return array Links for the given customer. + */ + protected function prepare_links( $customer ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'last_order' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Last order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date' => array( + 'description' => __( 'The date of the customer last order, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'orders_count' => array( + 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_spent' => array( + 'description' => __( 'Total amount spent.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get role names. + * + * @return array + */ + protected function get_role_names() { + global $wp_roles; + + return array_keys( $wp_roles->role_names ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'name', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'include', + 'name', + 'registered_date', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['email'] = array( + 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['role'] = array( + 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'customer', + 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), + 'validate_callback' => 'rest_validate_request_arg', + ); + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php new file mode 100644 index 0000000..b057cfd --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php @@ -0,0 +1,439 @@ +/notes endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Notes controller class. + * + * @package WooCommerce\RestApi + * @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 = wc_delete_order_note( $note->comment_ID ); + + 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/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php new file mode 100644 index 0000000..550aef9 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php @@ -0,0 +1,530 @@ +/refunds endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Orders_V1_Controller + */ +class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/refunds'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order_refund'; + + /** + * Order refunds actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' ); + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for order refunds. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => true, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Prepare a single order refund output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $post, $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + $refund = wc_get_order( $post ); + + if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); + + $data = array( + 'id' => $refund->get_id(), + 'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ), + 'amount' => wc_format_decimal( $refund->get_amount(), $dp ), + 'reason' => $refund->get_reason(), + 'line_items' => array(), + ); + + // Add line items. + foreach ( $refund->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $data['line_items'][] = $line_item; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $refund, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order_Refund $refund Comment object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given order refund. + */ + protected function prepare_links( $refund, $request ) { + $order_id = $refund->get_parent_id(); + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), + ), + ); + + return $links; + } + + /** + * Query args. + * + * @param array $args Request args. + * @param WP_REST_Request $request Request object. + * @return array + */ + public function query_args( $args, $request ) { + $args['post_status'] = array_keys( wc_get_order_statuses() ); + $args['post_parent__in'] = array( absint( $request['order_id'] ) ); + + return $args; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_data = get_post( (int) $request['order_id'] ); + + if ( empty( $order_data ) ) { + return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( array( + 'order_id' => $order_data->ID, + 'amount' => $request['amount'], + 'reason' => empty( $request['reason'] ) ? null : $request['reason'], + 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, + 'restock_items' => true, + ) ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + $post = get_post( $refund->get_id() ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'Refund amount.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reason' => array( + 'description' => __( 'Reason for refund.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php new file mode 100644 index 0000000..51fffe1 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php @@ -0,0 +1,1631 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for orders. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Prepare a single order output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + $order = wc_get_order( $post ); + $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); + + $data = array( + 'id' => $order->get_id(), + 'parent_id' => $order->get_parent_id(), + 'status' => $order->get_status(), + 'order_key' => $order->get_order_key(), + 'number' => $order->get_order_number(), + 'currency' => $order->get_currency(), + 'version' => $order->get_version(), + 'prices_include_tax' => $order->get_prices_include_tax(), + 'date_created' => wc_rest_prepare_date_response( $order->get_date_created() ), // v1 API used UTC. + 'date_modified' => wc_rest_prepare_date_response( $order->get_date_modified() ), // v1 API used UTC. + 'customer_id' => $order->get_customer_id(), + 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'discount_tax' => wc_format_decimal( $order->get_discount_tax(), $dp ), + 'shipping_total' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'billing' => array(), + 'shipping' => array(), + 'payment_method' => $order->get_payment_method(), + 'payment_method_title' => $order->get_payment_method_title(), + 'transaction_id' => $order->get_transaction_id(), + 'customer_ip_address' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'created_via' => $order->get_created_via(), + 'customer_note' => $order->get_customer_note(), + 'date_completed' => wc_rest_prepare_date_response( $order->get_date_completed(), false ), // v1 API used local time. + 'date_paid' => wc_rest_prepare_date_response( $order->get_date_paid(), false ), // v1 API used local time. + 'cart_hash' => $order->get_cart_hash(), + 'line_items' => array(), + 'tax_lines' => array(), + 'shipping_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + 'refunds' => array(), + ); + + // Add addresses. + $data['billing'] = $order->get_address( 'billing' ); + $data['shipping'] = $order->get_address( 'shipping' ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $item->get_product(); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $data['line_items'][] = $line_item; + } + + // Add taxes. + foreach ( $order->get_items( 'tax' ) as $key => $tax ) { + $tax_line = array( + 'id' => $key, + 'rate_code' => $tax['name'], + 'rate_id' => $tax['rate_id'], + 'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'], + 'compound' => (bool) $tax['compound'], + 'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ), + 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), + ); + + $data['tax_lines'][] = $tax_line; + } + + // Add shipping. + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $shipping_line = array( + 'id' => $shipping_item_id, + 'method_title' => $shipping_item['name'], + 'method_id' => $shipping_item['method_id'], + 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), + 'total_tax' => wc_format_decimal( '', $dp ), + 'taxes' => array(), + ); + + $shipping_taxes = $shipping_item->get_taxes(); + + if ( ! empty( $shipping_taxes['total'] ) ) { + $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes['total'] ), $dp ); + + foreach ( $shipping_taxes['total'] as $tax_rate_id => $tax ) { + $shipping_line['taxes'][] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + ); + } + } + + $data['shipping_lines'][] = $shipping_line; + } + + // Add fees. + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $fee_line = array( + 'id' => $fee_item_id, + 'name' => $fee_item['name'], + 'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '', + 'tax_status' => 'taxable', + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + 'taxes' => array(), + ); + + $fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] ); + if ( isset( $fee_line_taxes['total'] ) ) { + $fee_tax = array(); + + foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + if ( isset( $fee_line_taxes['subtotal'] ) ) { + foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + } + + $fee_line['taxes'] = array_values( $fee_tax ); + } + + $data['fee_lines'][] = $fee_line; + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item['name'], + 'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), + 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), + ); + + $data['coupon_lines'][] = $coupon_line; + } + + // Add refunds. + foreach ( $order->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'refund' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $dp ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $order, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order $order Order object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given order. + */ + protected function prepare_links( $order, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + if ( 0 !== (int) $order->get_user_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ), + ); + } + if ( 0 !== (int) $order->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ), + ); + } + return $links; + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + global $wpdb; + + // Set post_status. + if ( 'any' !== $request['status'] ) { + $args['post_status'] = 'wc-' . $request['status']; + } else { + $args['post_status'] = 'any'; + } + + if ( isset( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $request['product'] ) ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + return $args; + } + + /** + * Prepare a single order for create. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WC_Order $data Object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'billing' : + case 'shipping' : + $this->update_address( $order, $value, $key ); + break; + case 'line_items' : + case 'shipping_lines' : + case 'fee_lines' : + case 'coupon_lines' : + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + default : + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the data for the insert. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WC_Order $order The order object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); + } + + /** + * Create base WC Order object. + * @deprecated 3.0.0 + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * Only return writable props from schema. + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Create order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + try { + // Make sure customer exists. + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + + $order = $this->prepare_item_for_database( $request ); + $order->set_created_via( 'rest-api' ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->calculate_totals(); + $order->save(); + + // Handle set paid. + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function update_order( $request ) { + try { + $order = $this->prepare_item_for_database( $request ); + $order->save(); + + // Handle set paid. + if ( $order->needs_payment() && true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $order->calculate_totals( true ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @throws WC_REST_Exception When SKU or ID is not valid. + * @param array $posted Request data. + * @param string $action 'create' to add line item or 'update' to update it. + * @return int + */ + protected function get_product_id( $posted, $action = 'create' ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } elseif ( 'update' === $action ) { + $product_id = 0; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * @param WC_Order_Item $item + * @param string $prop + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * @param WC_Order_Item $item + * @param string[] $props + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } + } + + /** + * Create or update a line item. + * + * @param array $posted Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * + * @return WC_Order_Item_Product + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_line_items( $posted, $action = 'create' ) { + $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + $product = wc_get_product( $this->get_product_id( $posted, $action ) ); + + if ( $product && $product !== $item->get_product() ) { + $item->set_product( $product ); + + if ( 'create' === $action ) { + $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $item->set_total( $total ); + $item->set_subtotal( $total ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); + + return $item; + } + + /** + * Create or update an order shipping method. + * + * @param $posted $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * + * @return WC_Order_Item_Shipping + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_shipping_lines( $posted, $action ) { + $item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted ); + + return $item; + } + + /** + * Create or update an order fee. + * + * @param array $posted Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * + * @return WC_Order_Item_Fee + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_fee_lines( $posted, $action ) { + $item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); + + return $item; + } + + /** + * Create or update an order coupon. + * + * @param array $posted Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * + * @return WC_Order_Item_Coupon + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_coupon_lines( $posted, $action ) { + $item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); + + return $item; + } + + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order + * @param string $item_type + * @param array $posted item provided in the request body + * @throws WC_REST_Exception If item ID is not associated with order + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $posted['id'] ), + absint( $order->get_id() ) + ) ); + if ( is_null( $result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data + $item = $this->$method( $posted, $action ); + + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // Save or add to order + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_id = $this->create_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Update a single order. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $post_id = (int) $request['id']; + + if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $order_id = $this->update_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get order statuses without prefixes. + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $this->get_order_statuses(), + 'context' => array( 'view', 'edit' ), + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'currency' => array( + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'default' => get_woocommerce_currency(), + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'version' => array( + 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'User agent of the customer.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid' => array( + 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Shipping lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'fee_lines' => array( + 'description' => __( 'Fee lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'taxable', 'none' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Coupons line data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'refunds' => array( + 'description' => __( 'List of refunds.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Refund ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reason' => array( + 'description' => __( 'Refund reason.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Refund total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php new file mode 100644 index 0000000..244530b --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php @@ -0,0 +1,241 @@ +/terms endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Attribute Terms controller class. + * + * @package WooCommerce\RestApi + * @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_term_meta( $item->term_id, 'order_' . $this->taxonomy, true ); + + $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_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/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php new file mode 100644 index 0000000..c60ee46 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php @@ -0,0 +1,630 @@ +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 The request to get the attributes from. + * @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; + } + + $response = rest_ensure_response( $data ); + + // This API call always returns all product attributes due to retrieval from the object cache. + $response->header( 'X-WP-Total', count( $data ) ); + $response->header( 'X-WP-TotalPages', 1 ); + + return $response; + } + + /** + * 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; + + $id = wc_create_attribute( + array( + 'name' => $request['name'], + 'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ), + 'type' => ! empty( $request['type'] ) ? $request['type'] : 'select', + 'order_by' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order', + 'has_archives' => true === $request['has_archives'], + ) + ); + + // Checks for errors. + if ( is_wp_error( $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), 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, 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 ) ); + + 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']; + $edited = wc_update_attribute( + $id, + array( + 'name' => $request['name'], + 'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ), + 'type' => $request['type'], + 'order_by' => $request['order_by'], + 'has_archives' => $request['has_archives'], + ) + ); + + // Checks for errors. + if ( is_wp_error( $edited ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', $edited->get_error_message(), 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 ); + + 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 ) { + $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 = wc_delete_attribute( $attribute->attribute_id ); + + if ( false === $deleted ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * 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 ); + + return $response; + } + + /** + * Prepare a single product attribute output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request The request to process. + * @return WP_REST_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 ) { + $attribute_id = $request['id']; + + if ( empty( $attribute_id ) ) { + return ''; + } + + if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) { + return $this->taxonomies_by_id[ $attribute_id ]; + } + + $taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['id'] ); + if ( ! empty( $taxonomy ) ) { + $this->taxonomies_by_id[ $attribute_id ] = $taxonomy; + } + + return $taxonomy; + } + + /** + * 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. + * + * @deprecated 3.2.0 + * @param string $slug The slug to validate. + * @param bool $new_data If we are creating new data. + * @return bool|WP_Error + */ + protected function validate_attribute_slug( $slug, $new_data = true ) { + if ( strlen( $slug ) >= 28 ) { + /* translators: %s: slug being validated */ + 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 ) ) { + /* translators: %s: slug being validated */ + 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 ) ) ) { + /* translators: %s: slug being validated */ + 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. + * + * @deprecated 3.2.0 + * @since 3.0.0 + */ + protected function flush_rewrite_rules() { + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php new file mode 100644 index 0000000..4d19661 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php @@ -0,0 +1,271 @@ +term_id, 'display_type', true ); + + // Get category order. + $menu_order = get_term_meta( $item->term_id, 'order', true ); + + $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' => null, + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); + if ( $image_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 Term object. + * @param WP_REST_Request $request Request instance. + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + if ( isset( $request['display'] ) ) { + update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); + } + + if ( isset( $request['menu_order'] ) ) { + update_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_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_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' ), + ), + 'title' => 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/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php new file mode 100644 index 0000000..d8b0f77 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php @@ -0,0 +1,578 @@ +/reviews. + * + * @author WooThemes + * @category API + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Reviews Controller Class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Controller + */ +class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'review' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Review content.', 'woocommerce' ), + ), + 'name' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Name of the reviewer.', 'woocommerce' ), + ), + 'email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Email of the reviewer.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'product', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'product', 'read', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a new product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'create', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to update a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'edit', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to delete a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'delete', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get all reviews from a product. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $reviews = get_approved_comments( $product_id ); + $data = array(); + foreach ( $reviews as $review_data ) { + $review = $this->prepare_item_for_response( $review_data, $request ); + $review = $this->prepare_response_for_collection( $review ); + $data[] = $review; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $id ); + + if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + + /** + * Create a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + + /** + * Filter a product review (comment) before it is inserted via the REST API. + * + * Allows modification of the comment right before it is inserted via `wp_insert_comment`. + * + * @param array $prepared_review The prepared comment data for `wp_insert_comment`. + * @param WP_REST_Request $request Request used to insert the comment. + */ + $prepared_review = apply_filters( 'rest_pre_insert_product_review', $prepared_review, $request ); + + $product_review_id = wp_insert_comment( $prepared_review ); + if ( ! $product_review_id ) { + return new WP_Error( 'rest_product_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + update_comment_meta( $product_review_id, 'rating', ( ! empty( $request['rating'] ) ? $request['rating'] : '0' ) ); + + $product_review = get_comment( $product_review_id ); + $this->update_additional_fields_for_object( $product_review, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $product_review Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $product_review_id ) ) ); + + return $response; + } + + /** + * Update a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $product_review_id = (int) $request['id']; + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $product_review_id ); + + if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + + $updated = wp_update_comment( $prepared_review ); + if ( 0 === $updated ) { + return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( ! empty( $request['rating'] ) ) { + update_comment_meta( $product_review_id, 'rating', $request['rating'] ); + } + + $product_review = get_comment( $product_review_id ); + $this->update_additional_fields_for_object( $product_review, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $comment Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a product review. + * + * @param WP_REST_Request $request Full details about the request + * + * @return bool|WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $product_review_id = absint( is_array( $request['id'] ) ? $request['id']['id'] : $request['id'] ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + $product_review = get_comment( $product_review_id ); + if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + /** + * Filter whether a product review is trashable. + * + * Return false to disable trash support for the product review. + * + * @param boolean $supports_trash Whether the object supports trashing. + * @param WP_Post $product_review The object being considered for trashing support. + */ + $supports_trash = apply_filters( 'rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $product_review ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + + if ( $force ) { + $result = wp_delete_comment( $product_review_id, true ); + } else { + if ( ! $supports_trash ) { + return new WP_Error( 'rest_trash_not_supported', __( 'The product review does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + if ( 'trash' === $product_review->comment_approved ) { + return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); + } + + $result = wp_trash_comment( $product_review->comment_ID ); + } + + if ( ! $result ) { + return new WP_Error( 'rest_cannot_delete', __( 'The product review cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a product review is deleted via the REST API. + * + * @param object $product_review The deleted item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'rest_delete_product_review', $product_review, $response, $request ); + + return $response; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), + 'name' => $review->comment_author, + 'email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + /** + * Prepare a single product review to be inserted into the database. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error $prepared_review + */ + protected function prepare_item_for_database( $request ) { + $prepared_review = array( 'comment_approved' => 1, 'comment_type' => 'review' ); + + if ( isset( $request['id'] ) ) { + $prepared_review['comment_ID'] = (int) $request['id']; + } + + if ( isset( $request['review'] ) ) { + $prepared_review['comment_content'] = $request['review']; + } + + if ( isset( $request['product_id'] ) ) { + $prepared_review['comment_post_ID'] = (int) $request['product_id']; + } + + if ( isset( $request['name'] ) ) { + $prepared_review['comment_author'] = $request['name']; + } + + if ( isset( $request['email'] ) ) { + $prepared_review['comment_author_email'] = $request['email']; + } + + if ( isset( $request['date_created'] ) ) { + $prepared_review['comment_date'] = $request['date_created']; + } + + if ( isset( $request['date_created_gmt'] ) ) { + $prepared_review['comment_date_gmt'] = $request['date_created_gmt']; + } + + return apply_filters( 'rest_preprocess_product_review', $prepared_review, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given product review. + */ + protected function prepare_links( $review, $request ) { + $product_id = (int) $request['product_id']; + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php new file mode 100644 index 0000000..5de129c --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-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/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php new file mode 100644 index 0000000..121f101 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-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/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php new file mode 100644 index 0000000..64e9442 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php @@ -0,0 +1,2641 @@ +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 ( $product->get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_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' => wc_attribute_taxonomy_slug( $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'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @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( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_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 ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $product->delete( true ); + $result = ! ( $product->get_id() > 0 ); + } else { + // 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_merge( array_keys( get_post_statuses() ), array( 'future' ) ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( 'End date 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 ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + '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' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of 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' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_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 date 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 ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 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' => 'integer', + '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', 'future' ), 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/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php new file mode 100644 index 0000000..cc6bb03 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-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-DD' ), + '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-DD' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php new file mode 100644 index 0000000..37d2114 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-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/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php new file mode 100644 index 0000000..37b87b7 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php @@ -0,0 +1,184 @@ +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 reports list. + * + * @since 3.5.0 + * @return array + */ + protected function get_reports() { + return array( + array( + 'slug' => 'sales', + 'description' => __( 'List of sales reports.', 'woocommerce' ), + ), + array( + 'slug' => 'top_sellers', + 'description' => __( 'List of top sellers products.', 'woocommerce' ), + ), + ); + } + + /** + * Get all reports. + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $reports = $this->get_reports(); + + 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/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php new file mode 100644 index 0000000..71a9e2d --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php @@ -0,0 +1,327 @@ +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 Full details about the 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 class. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $tax_class = WC_Tax::create_tax_class( $request['name'] ); + + if ( is_wp_error( $tax_class ) ) { + return new WP_Error( 'woocommerce_rest_' . $tax_class->get_error_code(), $tax_class->get_error_message(), array( 'status' => 400 ) ); + } + + $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 = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) ); + $deleted = WC_Tax::delete_tax_class_by( 'slug', sanitize_title( $request['slug'] ) ); + + if ( ! $deleted ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( is_wp_error( $deleted ) ) { + return new WP_Error( 'woocommerce_rest_' . $deleted->get_error_code(), $deleted->get_error_message(), array( 'status' => 400 ) ); + } + + $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 Full details about the request. + * @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/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php new file mode 100644 index 0000000..19ba9b6 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php @@ -0,0 +1,761 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to read taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create taxes. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + global $wpdb; + + $prepared_args = array(); + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'tax_rate_id', + 'order' => 'tax_rate_order', + 'priority' => 'tax_rate_priority', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['class'] = $request['class']; + + /** + * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. + * + * @param array $prepared_args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); + + $orderby = sanitize_key( $prepared_args['orderby'] ) . ' ' . sanitize_key( $prepared_args['order'] ); + $query = " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + %s + ORDER BY {$orderby} + LIMIT %%d, %%d + "; + + $wpdb_prepare_args = array( + $prepared_args['offset'], + $prepared_args['number'], + ); + + // Filter by tax class. + if ( empty( $prepared_args['class'] ) ) { + $query = sprintf( $query, '' ); + } else { + $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; + array_unshift( $wpdb_prepare_args, $class ); + $query = sprintf( $query, 'WHERE tax_rate_class = %s' ); + } + + // Query taxes. + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( + $wpdb->prepare( + $query, + $wpdb_prepare_args + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + $taxes = array(); + foreach ( $results as $tax ) { + $data = $this->prepare_item_for_response( $tax, $request ); + $taxes[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $taxes ); + + // Store pagination values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + // Query only for ids. + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $query = str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ); + $wpdb->get_results( + $wpdb->prepare( + $query, + $wpdb_prepare_args + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + // Calculate totals. + $total_taxes = (int) $wpdb->num_rows; + $response->header( 'X-WP-Total', (int) $total_taxes ); + $max_pages = ceil( $total_taxes / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Take tax data from the request and return the updated or newly created rate. + * + * @param WP_REST_Request $request Full details about the request. + * @param stdClass|null $current Existing tax object. + * @return object + */ + protected function create_or_update_tax( $request, $current = null ) { + $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); + $data = array(); + $fields = array( + 'tax_rate_country', + 'tax_rate_state', + 'tax_rate', + 'tax_rate_name', + 'tax_rate_priority', + 'tax_rate_compound', + 'tax_rate_shipping', + 'tax_rate_order', + 'tax_rate_class', + ); + + foreach ( $fields as $field ) { + // Keys via API differ from the stored names returned by _get_tax_rate. + $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); + + // Remove data that was not posted. + if ( ! isset( $request[ $key ] ) ) { + continue; + } + + // Test new data against current data. + if ( $current && $current->$field === $request[ $key ] ) { + continue; + } + + // Add to data array. + switch ( $key ) { + case 'tax_rate_priority': + case 'tax_rate_compound': + case 'tax_rate_shipping': + case 'tax_rate_order': + $data[ $field ] = absint( $request[ $key ] ); + break; + case 'tax_rate_class': + $data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : ''; + break; + default: + $data[ $field ] = wc_clean( $request[ $key ] ); + break; + } + } + + if ( ! $id ) { + $id = WC_Tax::_insert_tax_rate( $data ); + } elseif ( $data ) { + WC_Tax::_update_tax_rate( $id, $data ); + } + + // Add locales. + if ( ! empty( $request['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); + } + if ( ! empty( $request['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); + } + + return WC_Tax::_get_tax_rate( $id, OBJECT ); + } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $tax = $this->create_or_update_tax( $request ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) ); + + return $response; + } + + /** + * Get a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->prepare_item_for_response( $tax_obj, $request ); + $response = rest_ensure_response( $tax ); + + return $response; + } + + /** + * Update a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->create_or_update_tax( $request, $tax_obj ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + + return $response; + } + + /** + * Delete a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a tax is deleted via the REST API. + * + * @param stdClass $tax The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); + + return $response; + } + + /** + * Prepare a single tax output for response. + * + * @param stdClass $tax Tax object. + * @param WP_REST_Request $request Request object. + * + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax, $request ) { + $id = (int) $tax->tax_rate_id; + $data = array( + 'id' => $id, + 'country' => $tax->tax_rate_country, + 'state' => $tax->tax_rate_state, + 'postcode' => '', + 'city' => '', + 'rate' => $tax->tax_rate, + 'name' => $tax->tax_rate_name, + 'priority' => (int) $tax->tax_rate_priority, + 'compound' => (bool) $tax->tax_rate_compound, + 'shipping' => (bool) $tax->tax_rate_shipping, + 'order' => (int) $tax->tax_rate_order, + 'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard', + ); + + $data = $this->add_tax_rate_locales( $data, $tax ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $tax ) ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $tax Tax object. + * @return array Links for the given tax. + */ + protected function prepare_links( $tax ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Add tax rate locales to the response array. + * + * @param array $data Response data. + * @param stdClass $tax Tax object. + * + * @return array + */ + protected function add_tax_rate_locales( $data, $tax ) { + global $wpdb; + + // Get locales from a tax rate. + $locales = $wpdb->get_results( + $wpdb->prepare( + " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", + $tax->tax_rate_id + ) + ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $data[ $locale->location_type ] = $locale->location_code; + } + } + + return $data; + } + + /** + * Get the Taxes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'country' => array( + 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'State code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postcode / ZIP.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'rate' => array( + 'description' => __( 'Tax rate.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tax rate name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'priority' => array( + 'description' => __( 'Tax priority.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + ), + 'compound' => array( + 'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'shipping' => array( + 'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'order' => array( + 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'standard', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param(); + $params['context']['default'] = 'view'; + + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'order', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'order', + 'priority', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['class'] = array( + 'description' => __( 'Sort by tax class.', 'woocommerce' ), + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_title', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php new file mode 100644 index 0000000..638e8b2 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php @@ -0,0 +1,314 @@ +/deliveries endpoint. + * + * @author WooThemes + * @category API + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Webhook Deliveries controller class. + * + * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. + * @package WooCommerce\RestApi + * @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 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( 'webhooks', '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 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( 'webhooks', '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 all webhook deliveries. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $webhook = wc_get_webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $logs = array(); + $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 = wc_get_webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $log = array(); + + 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; + $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/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php new file mode 100644 index 0000000..4a2463a --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php @@ -0,0 +1,763 @@ +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' ), + ), + ) ), + ), + '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 webhooks. + * + * @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( 'webhooks', '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 webhooks. + * + * @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( 'webhooks', '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 webhook. + * + * @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( 'webhooks', '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 webhook. + * + * @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( 'webhooks', '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 webhook. + * + * @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( 'webhooks', '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( 'webhooks', '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 the default REST API version. + * + * @since 3.0.0 + * @return string + */ + protected function get_default_api_version() { + return 'wp_api_v1'; + } + + /** + * Get all webhooks. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $args = array(); + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['status'] = 'all' === $request['status'] ? '' : $request['status']; + $args['include'] = implode( ',', $request['include'] ); + $args['exclude'] = implode( ',', $request['exclude'] ); + $args['limit'] = $request['per_page']; + $args['search'] = $request['search']; + $args['before'] = $request['before']; + $args['after'] = $request['after']; + + if ( empty( $request['offset'] ) ) { + $args['offset'] = 1 < $request['page'] ? ( $request['page'] - 1 ) * $args['limit'] : 0; + } + + /** + * Filter arguments, before passing to WC_Webhook_Data_Store->search_webhooks, when querying webhooks via the REST API. + * + * @param array $args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_webhook_query', $args, $request ); + unset( $prepared_args['page'] ); + $prepared_args['paginate'] = true; + + // Get the webhooks. + $webhooks = array(); + $data_store = WC_Data_Store::load( 'webhook' ); + $results = $data_store->search_webhooks( $prepared_args ); + $webhook_ids = $results->webhooks; + + foreach ( $webhook_ids as $webhook_id ) { + $data = $this->prepare_item_for_response( $webhook_id, $request ); + $webhooks[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $webhooks ); + $per_page = (int) $prepared_args['limit']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + $total_webhooks = $results->total; + $max_pages = $results->max_num_pages; + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + + $response->header( 'X-WP-Total', $total_webhooks ); + $response->header( 'X-WP-TotalPages', $max_pages ); + + 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; + } + + /** + * Get a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_item_for_response( $id, $request ); + $response = rest_ensure_response( $data ); + + return $response; + } + + /** + * 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; + } + + $webhook = new WC_Webhook(); + $webhook->set_name( $post->post_title ); + $webhook->set_user_id( $post->post_author ); + $webhook->set_status( 'publish' === $post->post_status ? 'active' : 'disabled' ); + $webhook->set_topic( $request['topic'] ); + $webhook->set_delivery_url( $request['delivery_url'] ); + $webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : wp_generate_password( 50, true, true ) ); + $webhook->set_api_version( $this->get_default_api_version() ); + $webhook->save(); + + $this->update_additional_fields_for_object( $webhook, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WC_Webhook $webhook Webhook data. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $webhook->get_id(), $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $webhook->get_id() ) ) ); + + // Send ping. + $webhook->deliver_ping(); + + 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']; + $webhook = wc_get_webhook( $id ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // 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'] ) ) { + if ( wc_is_webhook_valid_status( strtolower( $request['status'] ) ) ) { + $webhook->set_status( $request['status'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_status", __( 'Webhook status must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( isset( $post->post_title ) ) { + $webhook->set_name( $post->post_title ); + } + + $webhook->save(); + + $this->update_additional_fields_for_object( $webhook, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WC_Webhook $webhook Webhook data. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $webhook->get_id(), $request ); + + 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 ) ); + } + + $webhook = wc_get_webhook( $id ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $webhook, $request ); + $result = $webhook->delete( 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 WC_Webhook $webhook 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_webhook_object", $webhook, $response, $request ); + + 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 ) ) { + $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' ) ) ); // @codingStandardsIgnoreLine + + // Post author. + $data->post_author = get_current_user_id(); + + // Post password. + $data->post_password = 'webhook_' . wp_generate_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 int $id Webhook ID or object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $id, $request ) { + $webhook = wc_get_webhook( $id ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data = array( + 'id' => $webhook->get_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_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified() ), + ); + + $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( $webhook->get_id() ) ); + + /** + * 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 ); + } + + /** + * Prepare links for the request. + * + * @param int $id Webhook ID. + * @return array + */ + protected function prepare_links( $id ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + 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', + '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_keys( wc_get_webhook_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + '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 to 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['context']['default'] = 'view'; + + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'title', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $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/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php new file mode 100644 index 0000000..f8598fc --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php @@ -0,0 +1,542 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + ), + ) + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return new WC_Coupon( $id ); + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data instance. + * @return array + */ + protected function get_formatted_item_data( $object ) { + $data = $object->get_data(); + + $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); + $format_date = array( 'date_created', 'date_modified', 'date_expires' ); + $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format null values. + foreach ( $format_null as $key ) { + $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; + } + + return array( + 'id' => $object->get_id(), + 'code' => $data['code'], + 'amount' => $data['amount'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'discount_type' => $data['discount_type'], + 'description' => $data['description'], + 'date_expires' => $data['date_expires'], + 'date_expires_gmt' => $data['date_expires_gmt'], + 'usage_count' => $data['usage_count'], + 'individual_use' => $data['individual_use'], + 'product_ids' => $data['product_ids'], + 'excluded_product_ids' => $data['excluded_product_ids'], + 'usage_limit' => $data['usage_limit'], + 'usage_limit_per_user' => $data['usage_limit_per_user'], + 'limit_usage_to_x_items' => $data['limit_usage_to_x_items'], + 'free_shipping' => $data['free_shipping'], + 'product_categories' => $data['product_categories'], + 'excluded_product_categories' => $data['excluded_product_categories'], + 'exclude_sale_items' => $data['exclude_sale_items'], + 'minimum_amount' => $data['minimum_amount'], + 'maximum_amount' => $data['maximum_amount'], + 'email_restrictions' => $data['email_restrictions'], + 'used_by' => $data['used_by'], + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Prepare a single coupon output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = $this->get_formatted_item_data( $object ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + if ( ! empty( $request['code'] ) ) { + $id = wc_get_coupon_id_by_code( $request['code'] ); + $args['post__in'] = array( $id ); + } + + // Get only ids. + $args['fields'] = 'ids'; + + return $args; + } + + /** + * Only return writable props from schema. + * + * @param array $schema Schema. + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single coupon for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $coupon = new WC_Coupon( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Validate required POST fields. + if ( $creating && empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'code': + $coupon_code = wc_format_coupon_code( $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon->set_code( $coupon_code ); + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + case 'description': + $coupon->set_description( wp_filter_post_kses( $value ) ); + break; + default: + if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { + $coupon->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating ); + } + + /** + * Get the Coupon's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'amount' => array( + 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the coupon was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the coupon was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'fixed_cart', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_expires' => array( + 'description' => __( "The date the coupon expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_expires_gmt' => array( + 'description' => __( 'The date the coupon expires, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'individual_use' => array( + 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( 'List of product IDs the coupon can be used on.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_ids' => array( + 'description' => __( 'List of product IDs the coupon cannot be used on.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'limit_usage_to_x_items' => array( + 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'free_shipping' => array( + 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_categories' => array( + 'description' => __( 'List of category IDs the coupon applies to.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_categories' => array( + 'description' => __( 'List of category IDs the coupon does not apply to.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email_restrictions' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + 'used_by' => array( + 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['code'] = array( + 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php new file mode 100644 index 0000000..fd48b05 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php @@ -0,0 +1,165 @@ +/downloads endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Customers controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Customer_Downloads_V1_Controller + */ +class WC_REST_Customer_Downloads_V2_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.', '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/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php new file mode 100644 index 0000000..b7e023b --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php @@ -0,0 +1,364 @@ +get_data(); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + return array( + 'id' => $object->get_id(), + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'role' => $data['role'], + 'username' => $data['username'], + 'billing' => $data['billing'], + 'shipping' => $data['shipping'], + 'is_paying_customer' => $data['is_paying_customer'], + '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 Customer data. + * @param WP_REST_Request $request Request data. + */ + 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 customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'role' => array( + 'description' => __( 'Customer role.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'is_paying_customer' => array( + 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), + 'type' => 'bool', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + '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' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php new file mode 100644 index 0000000..b6a0538 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php @@ -0,0 +1,174 @@ +namespace, + '/' . $this->rest_base . '/network', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'network_orders' ), + 'permission_callback' => array( $this, 'network_orders_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + } + + /** + * Retrieves the item's schema for display / public consumption purposes. + * + * @return array Public item schema data. + */ + public function get_public_item_schema() { + $schema = parent::get_public_item_schema(); + + $schema['properties']['blog'] = array( + 'description' => __( 'Blog id of the record on the multisite.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ); + $schema['properties']['edit_url'] = array( + 'description' => __( 'URL to edit the order', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ); + $schema['properties']['customer'][] = array( + 'description' => __( 'Name of the customer for the order', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ); + $schema['properties']['status_name'][] = array( + 'description' => __( 'Order Status', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ); + $schema['properties']['formatted_total'][] = array( + 'description' => __( 'Order total formatted for locale', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ); + + return $schema; + } + + /** + * Does a permissions check for the proper requested blog + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool $permission + */ + public function network_orders_permissions_check( $request ) { + $blog_id = $request->get_param( 'blog_id' ); + $blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id(); + + switch_to_blog( $blog_id ); + + $permission = $this->get_items_permissions_check( $request ); + + restore_current_blog(); + + return $permission; + } + + /** + * Get a collection of orders from the requested blog id + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response + */ + public function network_orders( $request ) { + $blog_id = $request->get_param( 'blog_id' ); + $blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id(); + $active_plugins = get_blog_option( $blog_id, 'active_plugins', array() ); + $network_active_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); + + $plugins = array_merge( $active_plugins, $network_active_plugins ); + $wc_active = false; + foreach ( $plugins as $plugin ) { + if ( substr_compare( $plugin, '/woocommerce.php', strlen( $plugin ) - strlen( '/woocommerce.php' ), strlen( '/woocommerce.php' ) ) === 0 ) { + $wc_active = true; + } + } + + // If WooCommerce not active for site, return an empty response. + if ( ! $wc_active ) { + $response = rest_ensure_response( array() ); + return $response; + } + + switch_to_blog( $blog_id ); + add_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) ); + $items = $this->get_items( $request ); + remove_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) ); + + foreach ( $items->data as &$current_order ) { + $order = wc_get_order( $current_order['id'] ); + + $current_order['blog'] = get_blog_details( get_current_blog_id() ); + $current_order['edit_url'] = get_admin_url( $blog_id, 'post.php?post=' . absint( $order->get_id() ) . '&action=edit' ); + /* translators: 1: first name 2: last name */ + $current_order['customer'] = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $order->get_billing_first_name(), $order->get_billing_last_name() ) ); + $current_order['status_name'] = wc_get_order_status_name( $order->get_status() ); + $current_order['formatted_total'] = $order->get_formatted_order_total(); + } + + restore_current_blog(); + + return $items; + } + + /** + * Filters the post statuses to on hold and processing for the network order query. + * + * @param array $args Query args. + * + * @return array + */ + public function network_orders_filter_args( $args ) { + $args['post_status'] = array( + 'wc-on-hold', + 'wc-processing', + ); + + return $args; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php new file mode 100644 index 0000000..2861539 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php @@ -0,0 +1,182 @@ +/notes endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Order Notes controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Order_Notes_V1_Controller + */ +class WC_REST_Order_Notes_V2_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 Request data. + * + * @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( // WPCS: slow query ok. + array( + 'key' => 'is_customer_note', + 'value' => 1, + 'compare' => '=', + ), + ); + } elseif ( 'internal' === $request['type'] ) { + $args['meta_query'] = array( // WPCS: slow query ok. + 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/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php new file mode 100644 index 0000000..9a36e55 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php @@ -0,0 +1,591 @@ +/refunds endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Orders_V2_Controller + */ +class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_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', 'shipping_lines', 'tax_lines', 'fee_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 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'], + 'refunded_payment' => $data['refunded_payment'], + 'meta_data' => $data['meta_data'], + 'line_items' => $data['line_items'], + 'shipping_lines' => $data['shipping_lines'], + 'tax_lines' => $data['tax_lines'], + 'fee_lines' => $data['fee_lines'], + ); + } + + /** + * 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; + $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); + $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'], + '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 ); + } + + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + $refund->save_meta_data(); + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); + } + + /** + * 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 refund 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' ), + ), + 'refunded_payment' => array( + 'description' => __( 'If the payment was refunded via the API.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + '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' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + '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/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php new file mode 100644 index 0000000..0d963e0 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -0,0 +1,1827 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. Return false if object is not of required type. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data|bool + */ + protected function get_object( $id ) { + $order = wc_get_order( $id ); + // In case id is a refund's id (or it's not an order at all), don't expose it via /orders/ path. + if ( ! $order || 'shop_order_refund' === $order->get_type() ) { + return false; + } + + return $order; + } + + /** + * Expands an order item to get its data. + * + * @param WC_Order_item $item Order item data. + * @return array + */ + protected function get_order_item_data( $item ) { + $data = $item->get_data(); + $format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + if ( isset( $data[ $key ] ) ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + } + + // Add SKU and PRICE to products. + if ( is_callable( array( $item, 'get_product' ) ) ) { + $data['sku'] = $item->get_product() ? $item->get_product()->get_sku() : null; + $data['price'] = $item->get_quantity() ? $item->get_total() / $item->get_quantity() : 0; + } + + // Add parent_name if the product is a variation. + if ( is_callable( array( $item, 'get_product' ) ) ) { + $product = $item->get_product(); + + if ( is_callable( array( $product, 'get_parent_data' ) ) ) { + $data['parent_name'] = $product->get_title(); + } else { + $data['parent_name'] = null; + } + } + + // Format taxes. + if ( ! empty( $data['taxes']['total'] ) ) { + $taxes = array(); + + foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) { + $taxes[] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '', + ); + } + $data['taxes'] = $taxes; + } elseif ( isset( $data['taxes'] ) ) { + $data['taxes'] = array(); + } + + // Remove names for coupons, taxes and shipping. + if ( isset( $data['code'] ) || isset( $data['rate_code'] ) || isset( $data['method_title'] ) ) { + unset( $data['name'] ); + } + + // Remove props we don't want to expose. + unset( $data['order_id'] ); + unset( $data['type'] ); + + // Expand meta_data to include user-friendly values. + $formatted_meta_data = $item->get_formatted_meta_data( null, true ); + $data['meta_data'] = array_map( + array( $this, 'merge_meta_item_with_formatted_meta_display_attributes' ), + $data['meta_data'], + array_fill( 0, count( $data['meta_data'] ), $formatted_meta_data ) + ); + + return $data; + } + + /** + * Merge the `$formatted_meta_data` `display_key` and `display_value` attribute values into the corresponding + * {@link WC_Meta_Data}. Returns the merged array. + * + * @param WC_Meta_Data $meta_item An object from {@link WC_Order_Item::get_meta_data()}. + * @param array $formatted_meta_data An object result from {@link WC_Order_Item::get_formatted_meta_data}. + * The keys are the IDs of {@link WC_Meta_Data}. + * + * @return array + */ + private function merge_meta_item_with_formatted_meta_display_attributes( $meta_item, $formatted_meta_data ) { + $result = array( + 'id' => $meta_item->id, + 'key' => $meta_item->key, + 'value' => $meta_item->value, + 'display_key' => $meta_item->key, // Default to original key, in case a formatted key is not available. + 'display_value' => $meta_item->value, // Default to original value, in case a formatted value is not available. + ); + + if ( array_key_exists( $meta_item->id, $formatted_meta_data ) ) { + $formatted_meta_item = $formatted_meta_data[ $meta_item->id ]; + + $result['display_key'] = wc_clean( $formatted_meta_item->display_key ); + $result['display_value'] = wc_clean( $formatted_meta_item->display_value ); + } + + return $result; + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Order $order WC_Data instance. + * + * @return array + */ + protected function get_formatted_item_data( $order ) { + $extra_fields = array( 'meta_data', 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines', 'refunds' ); + $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); + $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); + // These fields are dependent on other fields. + $dependent_fields = array( + 'date_created_gmt' => 'date_created', + 'date_modified_gmt' => 'date_modified', + 'date_completed_gmt' => 'date_completed', + 'date_paid_gmt' => 'date_paid', + ); + + $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); + + // Only fetch fields that we need. + $fields = $this->get_fields_for_response( $this->request ); + foreach ( $dependent_fields as $field_key => $dependency ) { + if ( in_array( $field_key, $fields ) && ! in_array( $dependency, $fields ) ) { + $fields[] = $dependency; + } + } + + $extra_fields = array_intersect( $extra_fields, $fields ); + $format_decimal = array_intersect( $format_decimal, $fields ); + $format_date = array_intersect( $format_date, $fields ); + + $format_line_items = array_intersect( $format_line_items, $fields ); + + $data = $order->get_base_data(); + + // Add extra data as necessary. + foreach ( $extra_fields as $field ) { + switch ( $field ) { + case 'meta_data': + $data['meta_data'] = $order->get_meta_data(); + break; + case 'line_items': + $data['line_items'] = $order->get_items( 'line_item' ); + break; + case 'tax_lines': + $data['tax_lines'] = $order->get_items( 'tax' ); + break; + case 'shipping_lines': + $data['shipping_lines'] = $order->get_items( 'shipping' ); + break; + case 'fee_lines': + $data['fee_lines'] = $order->get_items( 'fee' ); + break; + case 'coupon_lines': + $data['coupon_lines'] = $order->get_items( 'coupon' ); + break; + case 'refunds': + $data['refunds'] = array(); + foreach ( $order->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'reason' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ), + ); + } + break; + } + } + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format the order status. + $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; + + // Format line items. + foreach ( $format_line_items as $key ) { + $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); + } + + $allowed_fields = array( + 'id', + 'parent_id', + 'number', + 'order_key', + 'created_via', + 'version', + 'status', + 'currency', + 'date_created', + 'date_created_gmt', + 'date_modified', + 'date_modified_gmt', + 'discount_total', + 'discount_tax', + 'shipping_total', + 'shipping_tax', + 'cart_tax', + 'total', + 'total_tax', + 'prices_include_tax', + 'customer_id', + 'customer_ip_address', + 'customer_user_agent', + 'customer_note', + 'billing', + 'shipping', + 'payment_method', + 'payment_method_title', + 'transaction_id', + 'date_paid', + 'date_paid_gmt', + 'date_completed', + 'date_completed_gmt', + 'cart_hash', + 'meta_data', + 'line_items', + 'tax_lines', + 'shipping_lines', + 'fee_lines', + 'coupon_lines', + 'refunds', + ); + + $data = array_intersect_key( $data, array_flip( $allowed_fields ) ); + + return $data; + } + + /** + * Prepare a single order output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $this->request = $request; + $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); + $request['context'] = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->get_formatted_item_data( $object ); + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $request['context'] ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $object->get_customer_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ), + ); + } + + if ( 0 !== (int) $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + global $wpdb; + + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + if ( in_array( $request['status'], $this->get_order_statuses(), true ) ) { + $args['post_status'] = 'wc-' . $request['status']; + } elseif ( 'any' === $request['status'] ) { + $args['post_status'] = 'any'; + } else { + $args['post_status'] = $request['status']; + } + + if ( isset( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item'", + $request['product'] + ) + ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for an order collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( 'woocommerce_rest_orders_prepare_object_query', $args, $request ); + + return $args; + } + + /** + * Only return writable props from schema. + * + * @param array $schema Schema. + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single order for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'status': + // Status change should be done later so transitions have new data. + break; + case 'billing': + case 'shipping': + $this->update_address( $order, $value, $key ); + break; + case 'line_items': + case 'shipping_lines': + case 'fee_lines': + case 'coupon_lines': + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default: + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $order Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @throws WC_REST_Exception But all errors are validated before returning any data. + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + // Make sure gateways are loaded so hooks from gateways fire on save/create. + WC()->payment_gateways(); + + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + } + + if ( $creating ) { + $object->set_created_via( 'rest-api' ); + $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $object->calculate_totals(); + } else { + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $object->calculate_totals( true ); + } + } + + // Set status. + if ( ! empty( $request['status'] ) ) { + $object->set_status( $request['status'] ); + } + + $object->save(); + + // Actions for after the order is saved. + if ( true === $request['set_paid'] ) { + if ( $creating || $object->needs_payment() ) { + $object->payment_complete( $request['transaction_id'] ); + } + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order Order data. + * @param array $posted Posted data. + * @param string $type Address type. + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @throws WC_REST_Exception When SKU or ID is not valid. + * @param array $posted Request data. + * @param string $action 'create' to add line item or 'update' to update it. + * @return int + */ + protected function get_product_id( $posted, $action = 'create' ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } elseif ( 'update' === $action ) { + $product_id = 0; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * + * @param WC_Order_Item $item Order item. + * @param string $prop Order property. + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * + * @param WC_Order_Item $item Order item data. + * @param string[] $props Properties. + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } + } + + /** + * Maybe set item meta if posted. + * + * @param WC_Order_Item $item Order item data. + * @param array $posted Request data. + */ + protected function maybe_set_item_meta_data( $item, $posted ) { + if ( ! empty( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) { + foreach ( $posted['meta_data'] as $meta ) { + if ( isset( $meta['key'] ) ) { + $value = isset( $meta['value'] ) ? $meta['value'] : null; + $item->update_meta_data( $meta['key'], $value, isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + } + } + + /** + * Create or update a line item. + * + * @param array $posted Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Product + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_line_items( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + $product = wc_get_product( $this->get_product_id( $posted, $action ) ); + + if ( $product && $product !== $item->get_product() ) { + $item->set_product( $product ); + + if ( 'create' === $action ) { + $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $item->set_total( $total ); + $item->set_subtotal( $total ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order shipping method. + * + * @param array $posted $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Shipping + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_shipping_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total', 'instance_id' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order fee. + * + * @param array $posted Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Fee + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_fee_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order coupon. + * + * @param array $posted Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Coupon + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_coupon_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order object. + * @param string $item_type The item type. + * @param array $posted item provided in the request body. + * @throws WC_REST_Exception If item ID is not associated with order. + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + $item = null; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $item = $order->get_item( absint( $posted['id'] ), false ); + + if ( ! $item ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data. + $item = $this->$method( $posted, $action, $item ); + + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // If creating the order, add the item to it. + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Get order statuses without prefixes. + * + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'version' => array( + 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $this->get_order_statuses(), + 'context' => array( 'view', 'edit' ), + ), + 'currency' => array( + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'default' => get_woocommerce_currency(), + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the order was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the order was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'User agent of the customer.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'billing' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_paid' => array( + 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid_gmt' => array( + 'description' => __( 'The date the order was paid, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_completed_gmt' => array( + 'description' => __( 'The date the order was completed, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'parent_name' => array( + 'description' => __( 'Parent product name if the product is a variation.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'display_key' => array( + 'description' => __( 'Meta key for UI display.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'display_value' => array( + 'description' => __( 'Meta value for UI display.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Shipping lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'instance_id' => array( + 'description' => __( 'Shipping instance ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'fee_lines' => array( + 'description' => __( 'Fee lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'taxable', 'none' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Coupons line data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'refunds' => array( + 'description' => __( 'List of refunds.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Refund ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reason' => array( + 'description' => __( 'Refund reason.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Refund total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php new file mode 100644 index 0000000..eefffa5 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php @@ -0,0 +1,466 @@ + + */ + 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 Request data. + * @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 Request data. + * @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'] ) ) { + $settings['enabled'] = wc_bool_to_string( $request['enabled'] ); + $gateway->enabled = $settings['enabled']; + } + + // Update title. + if ( isset( $request['title'] ) ) { + $settings['title'] = $request['title']; + $gateway->title = $settings['title']; + } + + // Update description. + if ( isset( $request['description'] ) ) { + $settings['description'] = $request['description']; + $gateway->description = $settings['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 Request data. + * @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 Gateway data. + * + * @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' ), true ) ) { + 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/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php new file mode 100644 index 0000000..09b72eb --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php @@ -0,0 +1,27 @@ +/terms endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Product Attribute Terms controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Product_Attribute_Terms_V1_Controller + */ +class WC_REST_Product_Attribute_Terms_V2_Controller extends WC_REST_Product_Attribute_Terms_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php new file mode 100644 index 0000000..35d8237 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php @@ -0,0 +1,27 @@ +term_id, 'display_type', true ); + + // Get category order. + $menu_order = get_term_meta( $item->term_id, 'order', true ); + + $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' => null, + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); + if ( $image_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' ), + ), + 'title' => 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/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php new file mode 100644 index 0000000..cc34809 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php @@ -0,0 +1,199 @@ +/reviews. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Product Reviews Controller Class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Product_Reviews_V1_Controller + */ +class WC_REST_Product_Reviews_V2_Controller extends WC_REST_Product_Reviews_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + parent::register_routes(); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check if a given request has access to batch manage product reviews. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'product', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), + 'name' => $review->comment_author, + 'email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + + /** + * Bulk create, update and delete items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + $items = array_filter( $request->get_params() ); + $params = $request->get_url_params(); + $product_id = $params['product_id']; + $body_params = array(); + + foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { + if ( ! empty( $items[ $batch_type ] ) ) { + $injected_items = array(); + foreach ( $items[ $batch_type ] as $item ) { + $injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item; + } + $body_params[ $batch_type ] = $injected_items; + } + } + + $request = new WP_REST_Request( $request->get_method() ); + $request->set_body_params( $body_params ); + + return parent::batch_items( $request ); + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php new file mode 100644 index 0000000..c4db401 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php @@ -0,0 +1,27 @@ +/variations endpoints. + * + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API variations controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Products_V2_Controller + */ +class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_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'; + + /** + * 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 ); + } + + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + // Check if variation belongs to the correct parent product. + if ( $object && 0 !== $object->get_parent_id() && absint( $request['product_id'] ) !== $object->get_parent_id() ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Parent product does not match current variation.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * 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(); + } + + // Update parent ID just once. + if ( 0 === $variation->get_parent_id() ) { + $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'] ) && ! empty( $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'] ) ) { + if ( 'parent' === $request['manage_stock'] ) { + $variation->set_manage_stock( false ); // This just indicates the variation does not manage stock, but the parent does. + } else { + $variation->set_manage_stock( wc_string_to_bool( $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'] ); + $raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $raw_attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $raw_attribute_name ) { + continue; + } + + $attribute_name = sanitize_title( $raw_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['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, $raw_attribute_name ); // @codingStandardsIgnoreLine + + 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() ) ) { + return new WP_Error( + /* translators: %s: post type */ + "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 ) { + return new WP_Error( + /* translators: %s: post type */ + '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() ) { + return new WP_Error( + /* translators: %s: post type */ + '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 ) { + return new WP_Error( + /* translators: %s: post type */ + '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(); + $query = $request->get_query_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 ); + $request->set_query_params( $query ); + + 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, as GMT.', '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 variation is visible on 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 ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at variation level.', 'woocommerce' ), + 'type' => 'mixed', + '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' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php new file mode 100644 index 0000000..4927dc3 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php @@ -0,0 +1,2385 @@ +post_type}_object", array( $this, 'clear_transients' ) ); + } + + /** + * Register the routes for products. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( + array( + 'default' => 'view', + ) + ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'type' => 'boolean', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. + * + * @param int $id Object ID. + * + * @since 3.0.0 + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_product( $id ); + } + + /** + * Prepare a single product output for response. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @since 3.0.0 + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $this->request = $request; + $data = $this->get_product_data( $object, $context, $request ); + + // Add variations to variable products. + if ( $object->is_type( 'variable' ) && $object->has_child() ) { + $data['variations'] = $object->get_children(); + } + + // Add grouped products data. + if ( $object->is_type( 'grouped' ) && $object->has_child() ) { + $data['grouped_products'] = $object->get_children(); + } + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @param WP_REST_Request $request Full details about the request. + * + * @since 3.0.0 + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; // WPCS: slow query ok. + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product in stock or out of stock. + if ( is_bool( $request['in_stock'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + return $args; + } + + /** + * Get the downloads for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_downloads() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param string $taxonomy Taxonomy slug. + * + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + return $terms; + } + + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + // Add featured image. + if ( $product->get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + $images[] = array( + 'id' => 0, + 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), // Default to now. + 'date_created_gmt' => wc_rest_prepare_date_response( time() ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( time() ), + 'src' => wc_placeholder_img_src(), + 'name' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get attribute taxonomy label. + * + * @param string $name Taxonomy name. + * + * @deprecated 3.0.0 + * @return string + */ + protected function get_attribute_taxonomy_label( $name ) { + $tax = get_taxonomy( $name ); + $labels = get_taxonomy_labels( $tax ); + + return $labels->singular_name; + } + + /** + * Get product attribute taxonomy name. + * + * @param string $slug Taxonomy name. + * @param WC_Product $product Product data. + * + * @since 3.0.0 + * @return string + */ + protected function get_attribute_taxonomy_name( $slug, $product ) { + // Format slug so it matches attributes of the product. + $slug = wc_attribute_taxonomy_slug( $slug ); + $attributes = $product->get_attributes(); + $attribute = false; + + // pa_ attributes. + if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) { + $attribute = $attributes[ wc_attribute_taxonomy_name( $slug ) ]; + } elseif ( isset( $attributes[ $slug ] ) ) { + $attribute = $attributes[ $slug ]; + } + + if ( ! $attribute ) { + return $slug; + } + + // Taxonomy attribute name. + if ( $attribute->is_taxonomy() ) { + $taxonomy = $attribute->get_taxonomy_object(); + return $taxonomy->attribute_label; + } + + // Custom product attribute name. + return $attribute->get_name(); + } + + /** + * Get default attributes. + * + * @param WC_Product $product Product instance. + * + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { + if ( 0 === strpos( $key, 'pa_' ) ) { + $default[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $key ), + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } else { + $default[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id Product ID. + * @param array $attribute Attribute data. + * + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( + $product_id, + $attribute['name'], + array( + 'fields' => 'names', + ) + ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + $_product = wc_get_product( $product->get_parent_id() ); + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + $name = str_replace( 'attribute_', '', $attribute_name ); + + if ( empty( $attribute ) && '0' !== $attribute ) { + continue; + } + + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $name ); + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $name ), + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $attribute, + ); + } + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'id' => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0, + 'name' => $this->get_attribute_taxonomy_name( $attribute['name'], $product ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Fetch price HTML. + * + * @param WC_Product $product Product object. + * @param string $context Context of request, can be `view` or `edit`. + * + * @return string + */ + protected function api_get_price_html( $product, $context ) { + return $product->get_price_html(); + } + + /** + * Fetch related IDs. + * + * @param WC_Product $product Product object. + * @param string $context Context of request, can be `view` or `edit`. + * + * @return array + */ + protected function api_get_related_ids( $product, $context ) { + return array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ); + } + + /** + * Fetch meta data. + * + * @param WC_Product $product Product object. + * @param string $context Context of request, can be `view` or `edit`. + * + * @return array + */ + protected function api_get_meta_data( $product, $context ) { + return $product->get_meta_data(); + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @param string $context Request context. Options: 'view' and 'edit'. + * + * @return array + */ + protected function get_product_data( $product, $context = 'view' ) { + /* + * @param WP_REST_Request $request Current request object. For backward compatibility, we pass this argument silently. + * + * TODO: Refactor to fix this behavior when DI gets included to make it obvious and clean. + */ + $request = func_num_args() >= 3 ? func_get_arg( 2 ) : new WP_REST_Request( '', '', array( 'context' => $context ) ); + $fields = $this->get_fields_for_response( $request ); + + $base_data = array(); + foreach ( $fields as $field ) { + switch ( $field ) { + case 'id': + $base_data['id'] = $product->get_id(); + break; + case 'name': + $base_data['name'] = $product->get_name( $context ); + break; + case 'slug': + $base_data['slug'] = $product->get_slug( $context ); + break; + case 'permalink': + $base_data['permalink'] = $product->get_permalink(); + break; + case 'date_created': + $base_data['date_created'] = wc_rest_prepare_date_response( $product->get_date_created( $context ), false ); + break; + case 'date_created_gmt': + $base_data['date_created_gmt'] = wc_rest_prepare_date_response( $product->get_date_created( $context ) ); + break; + case 'date_modified': + $base_data['date_modified'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ), false ); + break; + case 'date_modified_gmt': + $base_data['date_modified_gmt'] = wc_rest_prepare_date_response( $product->get_date_modified( $context ) ); + break; + case 'type': + $base_data['type'] = $product->get_type(); + break; + case 'status': + $base_data['status'] = $product->get_status( $context ); + break; + case 'featured': + $base_data['featured'] = $product->is_featured(); + break; + case 'catalog_visibility': + $base_data['catalog_visibility'] = $product->get_catalog_visibility( $context ); + break; + case 'description': + $base_data['description'] = 'view' === $context ? wpautop( do_shortcode( $product->get_description() ) ) : $product->get_description( $context ); + break; + case 'short_description': + $base_data['short_description'] = 'view' === $context ? apply_filters( 'woocommerce_short_description', $product->get_short_description() ) : $product->get_short_description( $context ); + break; + case 'sku': + $base_data['sku'] = $product->get_sku( $context ); + break; + case 'price': + $base_data['price'] = $product->get_price( $context ); + break; + case 'regular_price': + $base_data['regular_price'] = $product->get_regular_price( $context ); + break; + case 'sale_price': + $base_data['sale_price'] = $product->get_sale_price( $context ) ? $product->get_sale_price( $context ) : ''; + break; + case 'date_on_sale_from': + $base_data['date_on_sale_from'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ), false ); + break; + case 'date_on_sale_from_gmt': + $base_data['date_on_sale_from_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ) ); + break; + case 'date_on_sale_to': + $base_data['date_on_sale_to'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ), false ); + break; + case 'date_on_sale_to_gmt': + $base_data['date_on_sale_to_gmt'] = wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ) ); + break; + case 'on_sale': + $base_data['on_sale'] = $product->is_on_sale( $context ); + break; + case 'purchasable': + $base_data['purchasable'] = $product->is_purchasable(); + break; + case 'total_sales': + $base_data['total_sales'] = $product->get_total_sales( $context ); + break; + case 'virtual': + $base_data['virtual'] = $product->is_virtual(); + break; + case 'downloadable': + $base_data['downloadable'] = $product->is_downloadable(); + break; + case 'downloads': + $base_data['downloads'] = $this->get_downloads( $product ); + break; + case 'download_limit': + $base_data['download_limit'] = $product->get_download_limit( $context ); + break; + case 'download_expiry': + $base_data['download_expiry'] = $product->get_download_expiry( $context ); + break; + case 'external_url': + $base_data['external_url'] = $product->is_type( 'external' ) ? $product->get_product_url( $context ) : ''; + break; + case 'button_text': + $base_data['button_text'] = $product->is_type( 'external' ) ? $product->get_button_text( $context ) : ''; + break; + case 'tax_status': + $base_data['tax_status'] = $product->get_tax_status( $context ); + break; + case 'tax_class': + $base_data['tax_class'] = $product->get_tax_class( $context ); + break; + case 'manage_stock': + $base_data['manage_stock'] = $product->managing_stock(); + break; + case 'stock_quantity': + $base_data['stock_quantity'] = $product->get_stock_quantity( $context ); + break; + case 'in_stock': + $base_data['in_stock'] = $product->is_in_stock(); + break; + case 'backorders': + $base_data['backorders'] = $product->get_backorders( $context ); + break; + case 'backorders_allowed': + $base_data['backorders_allowed'] = $product->backorders_allowed(); + break; + case 'backordered': + $base_data['backordered'] = $product->is_on_backorder(); + break; + case 'low_stock_amount': + $base_data['low_stock_amount'] = '' === $product->get_low_stock_amount() ? null : $product->get_low_stock_amount(); + break; + case 'sold_individually': + $base_data['sold_individually'] = $product->is_sold_individually(); + break; + case 'weight': + $base_data['weight'] = $product->get_weight( $context ); + break; + case 'dimensions': + $base_data['dimensions'] = array( + 'length' => $product->get_length( $context ), + 'width' => $product->get_width( $context ), + 'height' => $product->get_height( $context ), + ); + break; + case 'shipping_required': + $base_data['shipping_required'] = $product->needs_shipping(); + break; + case 'shipping_taxable': + $base_data['shipping_taxable'] = $product->is_shipping_taxable(); + break; + case 'shipping_class': + $base_data['shipping_class'] = $product->get_shipping_class(); + break; + case 'shipping_class_id': + $base_data['shipping_class_id'] = $product->get_shipping_class_id( $context ); + break; + case 'reviews_allowed': + $base_data['reviews_allowed'] = $product->get_reviews_allowed( $context ); + break; + case 'average_rating': + $base_data['average_rating'] = 'view' === $context ? wc_format_decimal( $product->get_average_rating(), 2 ) : $product->get_average_rating( $context ); + break; + case 'rating_count': + $base_data['rating_count'] = $product->get_rating_count(); + break; + case 'upsell_ids': + $base_data['upsell_ids'] = array_map( 'absint', $product->get_upsell_ids( $context ) ); + break; + case 'cross_sell_ids': + $base_data['cross_sell_ids'] = array_map( 'absint', $product->get_cross_sell_ids( $context ) ); + break; + case 'parent_id': + $base_data['parent_id'] = $product->get_parent_id( $context ); + break; + case 'purchase_note': + $base_data['purchase_note'] = 'view' === $context ? wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ) : $product->get_purchase_note( $context ); + break; + case 'categories': + $base_data['categories'] = $this->get_taxonomy_terms( $product ); + break; + case 'tags': + $base_data['tags'] = $this->get_taxonomy_terms( $product, 'tag' ); + break; + case 'images': + $base_data['images'] = $this->get_images( $product ); + break; + case 'attributes': + $base_data['attributes'] = $this->get_attributes( $product ); + break; + case 'default_attributes': + $base_data['default_attributes'] = $this->get_default_attributes( $product ); + break; + case 'variations': + $base_data['variations'] = array(); + break; + case 'grouped_products': + $base_data['grouped_products'] = array(); + break; + case 'menu_order': + $base_data['menu_order'] = $product->get_menu_order( $context ); + break; + } + } + + $data = array_merge( + $base_data, + $this->fetch_fields_using_getters( $product, $context, $fields ) + ); + + return $data; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), // @codingStandardsIgnoreLine. + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), // @codingStandardsIgnoreLine. + ), + ); + + if ( $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), // @codingStandardsIgnoreLine. + ); + } + + return $links; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + if ( 'variation' === $product->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + } + + // Product parent ID. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + // Set children for a grouped product. + if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { + $product->set_children( $request['grouped_products'] ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Allow set meta_data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); + } + + /** + * Set product images. + * + * @param WC_Product $product Product instance. + * @param array $images Images data. + * + * @throws WC_REST_Exception REST API exceptions. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : array(); + + if ( ! empty( $images ) ) { + $gallery_positions = array(); + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: attachment id */ + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index ); + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + + // Set the image source if present, for future reference. + if ( ! empty( $image['src'] ) ) { + update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) ); + } + } + + // Sort images and get IDs in correct order. + asort( $gallery_positions ); + + // Get gallery in correct order. + $gallery = array_keys( $gallery_positions ); + + // Featured image is in position 0. + $image_id = array_shift( $gallery ); + + // Set images. + $product->set_image_id( $image_id ); + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Save product shipping data. + * + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * + * @return WC_Product + */ + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( $data['dimensions']['height'] ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( $data['dimensions']['width'] ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( $data['dimensions']['length'] ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * + * @since 3.0.0 + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Clear caches here so in sync with any new variations/children. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_id() ); + wp_cache_delete( 'product-' . $object->get_id(), 'products' ); + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( + "woocommerce_rest_{$this->post_type}_invalid_id", + __( 'Invalid ID.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + if ( 'variation' === $object->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + return new WP_Error( + "woocommerce_rest_user_cannot_delete_{$this->post_type}", + /* translators: %s: post type */ + sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $object->is_type( 'variable' ) ) { + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + return new WP_Error( + 'woocommerce_rest_trash_not_supported', + /* translators: %s: post type */ + sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 501, + ) + ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + return new WP_Error( + 'woocommerce_rest_already_trashed', + /* translators: %s: post type */ + sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 410, + ) + ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + return new WP_Error( + 'woocommerce_rest_cannot_delete', + /* translators: %s: post type */ + sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 500, + ) + ); + } + + // Delete parent product transients. + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + 'readonly' => true, + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'menu_order' ) ); + + $params['slug'] = array( + 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'future', 'trash' ), array_keys( get_post_statuses() ) ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['type'] = array( + 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_types() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['featured'] = array( + 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( + 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute'] = array( + 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute_term'] = array( + 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + + if ( wc_tax_enabled() ) { + $params['tax_class'] = array( + 'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + + $params['in_stock'] = array( + 'description' => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['on_sale'] = array( + 'description' => __( 'Limit result set to products on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['min_price'] = array( + 'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['max_price'] = array( + 'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php new file mode 100644 index 0000000..1ba6041 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php @@ -0,0 +1,27 @@ +[\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 Request data. + * @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 Request data. + * @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 ) { + $states = WC()->countries->get_states( $key ); + if ( $states ) { + 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 Request data. + * @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 Settings. + * @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. + * + * @todo remove in 4.0 + * @since 3.0.0 + * @param array $setting Settings. + * @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 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. + 'thumbnail_cropping', // Validates with validate_setting_text_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', 'thumbnail_cropping' ), + '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/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php new file mode 100644 index 0000000..cc05d9b --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-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 Request data. + * @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 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/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php new file mode 100644 index 0000000..4265e10 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php @@ -0,0 +1,231 @@ + + */ + 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 Request data. + * @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/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php new file mode 100644 index 0000000..ca0e54b --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php @@ -0,0 +1,190 @@ +/locations endpoint. + * + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Locations class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Shipping_Zones_Controller_Base + */ +class WC_REST_Shipping_Zone_Locations_V2_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 Request data. + * @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 Request data. + * @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 "locations not covered by your other zones" 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/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php new file mode 100644 index 0000000..e153ba4 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php @@ -0,0 +1,541 @@ +/methods endpoint. + * + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Methods class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Shipping_Zones_Controller_Base + */ +class WC_REST_Shipping_Zone_Methods_V2_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 Request data. + * @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 Request data. + * @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 Request data. + * @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 Instance ID. + * @param WC_Shipping_Method $method Shipping method data. + * @param WP_REST_Request $request Request data. + * + * @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 Shipping method data. + * + * @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/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php new file mode 100644 index 0000000..8320fad --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php @@ -0,0 +1,304 @@ +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 Request data. + * @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 Request data. + * @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 "locations not covered by your other zones" 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/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php new file mode 100644 index 0000000..4b8a1db --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php @@ -0,0 +1,645 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view system status tools. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to view a specific system status tool. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to execute a specific system status tool. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * A list of available tools for use in the system status section. + * 'button' becomes 'action' in the API. + * + * @return array + */ + public function get_tools() { + $tools = array( + 'clear_transients' => array( + 'name' => __( 'WooCommerce transients', 'woocommerce' ), + 'button' => __( 'Clear transients', 'woocommerce' ), + 'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ), + ), + 'clear_expired_transients' => array( + 'name' => __( 'Expired transients', 'woocommerce' ), + 'button' => __( 'Clear transients', 'woocommerce' ), + 'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ), + ), + 'delete_orphaned_variations' => array( + 'name' => __( 'Orphaned variations', 'woocommerce' ), + 'button' => __( 'Delete orphaned variations', 'woocommerce' ), + 'desc' => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ), + ), + 'clear_expired_download_permissions' => array( + 'name' => __( 'Used-up download permissions', 'woocommerce' ), + 'button' => __( 'Clean up download permissions', 'woocommerce' ), + 'desc' => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ), + ), + 'regenerate_product_lookup_tables' => array( + 'name' => __( 'Product lookup tables', 'woocommerce' ), + 'button' => __( 'Regenerate', 'woocommerce' ), + 'desc' => __( 'This tool will regenerate product lookup table data. This process may take a while.', 'woocommerce' ), + ), + 'recount_terms' => array( + 'name' => __( 'Term counts', 'woocommerce' ), + 'button' => __( 'Recount terms', 'woocommerce' ), + 'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ), + ), + 'reset_roles' => array( + 'name' => __( 'Capabilities', 'woocommerce' ), + 'button' => __( 'Reset capabilities', 'woocommerce' ), + 'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ), + ), + 'clear_sessions' => array( + 'name' => __( 'Clear customer sessions', 'woocommerce' ), + 'button' => __( 'Clear', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will delete all customer session data from the database, including current carts and saved carts in the database.', 'woocommerce' ) + ), + ), + 'clear_template_cache' => array( + 'name' => __( 'Clear template cache', 'woocommerce' ), + 'button' => __( 'Clear', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will empty the template cache.', 'woocommerce' ) + ), + ), + 'install_pages' => array( + 'name' => __( 'Create default WooCommerce pages', 'woocommerce' ), + 'button' => __( 'Create pages', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' ) + ), + ), + 'delete_taxes' => array( + 'name' => __( 'Delete WooCommerce tax rates', 'woocommerce' ), + 'button' => __( 'Delete tax rates', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This option will delete ALL of your tax rates, use with caution. This action cannot be reversed.', 'woocommerce' ) + ), + ), + 'regenerate_thumbnails' => array( + 'name' => __( 'Regenerate shop thumbnails', 'woocommerce' ), + 'button' => __( 'Regenerate', 'woocommerce' ), + 'desc' => __( 'This will regenerate all shop thumbnails to match your theme and/or image settings.', 'woocommerce' ), + ), + 'db_update_routine' => array( + 'name' => __( 'Update database', 'woocommerce' ), + 'button' => __( 'Update database', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will update your WooCommerce database to the latest version. Please ensure you make sufficient backups before proceeding.', 'woocommerce' ) + ), + ), + ); + if ( method_exists( 'WC_Install', 'verify_base_tables' ) ) { + $tools['verify_db_tables'] = array( + 'name' => __( 'Verify base database tables', 'woocommerce' ), + 'button' => __( 'Verify database', 'woocommerce' ), + 'desc' => sprintf( + __( 'Verify if all base database tables are present.', 'woocommerce' ) + ), + ); + } + + // Jetpack does the image resizing heavy lifting so you don't have to. + if ( ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) || ! apply_filters( 'woocommerce_background_image_regeneration', true ) ) { + unset( $tools['regenerate_thumbnails'] ); + } + + if ( ! function_exists( 'wc_clear_template_cache' ) ) { + unset( $tools['clear_template_cache'] ); + } + + return apply_filters( 'woocommerce_debug_tools', $tools ); + } + + /** + * Get a list of system status tools. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $tools = array(); + foreach ( $this->get_tools() as $id => $tool ) { + $tools[] = $this->prepare_response_for_collection( + $this->prepare_item_for_response( + array( + 'id' => $id, + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ), + $request + ) + ); + } + + $response = rest_ensure_response( $tools ); + return $response; + } + + /** + * Return a single tool. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $tools = $this->get_tools(); + if ( empty( $tools[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + $tool = $tools[ $request['id'] ]; + return rest_ensure_response( + $this->prepare_item_for_response( + array( + 'id' => $request['id'], + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ), + $request + ) + ); + } + + /** + * Update (execute) a tool. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $tools = $this->get_tools(); + if ( empty( $tools[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tool = $tools[ $request['id'] ]; + $tool = array( + 'id' => $request['id'], + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ); + + $execute_return = $this->execute_tool( $request['id'] ); + $tool = array_merge( $tool, $execute_return ); + + /** + * Fires after a WooCommerce REST system status tool has been executed. + * + * @param array $tool Details about the tool that has been executed. + * @param WP_REST_Request $request The current WP_REST_Request object. + */ + do_action( 'woocommerce_rest_insert_system_status_tool', $tool, $request ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tool, $request ); + return rest_ensure_response( $response ); + } + + /** + * Prepare a tool item for serialization. + * + * @param array $item Object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item['id'] ) ); + + return $response; + } + + /** + * Get the system status tools schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'system_status_tool', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the tool.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'name' => array( + 'description' => __( 'Tool name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'action' => array( + 'description' => __( 'What running the tool will do.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'description' => array( + 'description' => __( 'Tool description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'success' => array( + 'description' => __( 'Did the tool run successfully?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + ), + 'message' => array( + 'description' => __( 'Tool return message.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Prepare links for the request. + * + * @param string $id ID. + * @return array + */ + protected function prepare_links( $id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'item' => array( + 'href' => rest_url( trailingslashit( $base ) . $id ), + 'embeddable' => true, + ), + ); + + return $links; + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } + + /** + * Actually executes a tool. + * + * @param string $tool Tool. + * @return array + */ + public function execute_tool( $tool ) { + global $wpdb; + $ran = true; + switch ( $tool ) { + case 'clear_transients': + wc_delete_product_transients(); + wc_delete_shop_order_transients(); + delete_transient( 'wc_count_comments' ); + delete_transient( 'as_comment_count' ); + + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( $attribute_taxonomies ) { + foreach ( $attribute_taxonomies as $attribute ) { + delete_transient( 'wc_layered_nav_counts_pa_' . $attribute->attribute_name ); + } + } + + WC_Cache_Helper::get_transient_version( 'shipping', true ); + $message = __( 'Product transients cleared', 'woocommerce' ); + break; + + case 'clear_expired_transients': + /* translators: %d: amount of expired transients */ + $message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), wc_delete_expired_transients() ); + break; + + case 'delete_orphaned_variations': + // Delete orphans. + $result = absint( + $wpdb->query( + "DELETE products + FROM {$wpdb->posts} products + LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent + WHERE wp.ID IS NULL AND products.post_type = 'product_variation';" + ) + ); + /* translators: %d: amount of orphaned variations */ + $message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result ); + break; + + case 'clear_expired_download_permissions': + // Delete expired download permissions and ones with 0 downloads remaining. + $result = absint( + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )", + gmdate( 'Y-m-d', current_time( 'timestamp' ) ) + ) + ) + ); + /* translators: %d: amount of permissions */ + $message = sprintf( __( '%d permissions deleted', 'woocommerce' ), $result ); + break; + + case 'regenerate_product_lookup_tables': + if ( ! wc_update_product_lookup_tables_is_running() ) { + wc_update_product_lookup_tables(); + } + $message = __( 'Lookup tables are regenerating', 'woocommerce' ); + break; + case 'reset_roles': + // Remove then re-add caps and roles. + WC_Install::remove_roles(); + WC_Install::create_roles(); + $message = __( 'Roles successfully reset', 'woocommerce' ); + break; + + case 'recount_terms': + wc_recount_all_terms(); + $message = __( 'Terms successfully recounted', 'woocommerce' ); + break; + + case 'clear_sessions': + $wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" ); + $result = absint( $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key='_woocommerce_persistent_cart_" . get_current_blog_id() . "';" ) ); // WPCS: unprepared SQL ok. + wp_cache_flush(); + /* translators: %d: amount of sessions */ + $message = sprintf( __( 'Deleted all active sessions, and %d saved carts.', 'woocommerce' ), absint( $result ) ); + break; + + case 'install_pages': + WC_Install::create_pages(); + $message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' ); + break; + + case 'delete_taxes': + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" ); + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" ); + + if ( method_exists( 'WC_Cache_Helper', 'invalidate_cache_group' ) ) { + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + } else { + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + } + $message = __( 'Tax rates successfully deleted', 'woocommerce' ); + break; + + case 'regenerate_thumbnails': + WC_Regenerate_Images::queue_image_regeneration(); + $message = __( 'Thumbnail regeneration has been scheduled to run in the background.', 'woocommerce' ); + break; + + case 'db_update_routine': + $blog_id = get_current_blog_id(); + // Used to fire an action added in WP_Background_Process::_construct() that calls WP_Background_Process::handle_cron_healthcheck(). + // This method will make sure the database updates are executed even if cron is disabled. Nothing will happen if the updates are already running. + do_action( 'wp_' . $blog_id . '_wc_updater_cron' ); + $message = __( 'Database upgrade routine has been scheduled to run in the background.', 'woocommerce' ); + break; + + case 'clear_template_cache': + if ( function_exists( 'wc_clear_template_cache' ) ) { + wc_clear_template_cache(); + $message = __( 'Template cache cleared.', 'woocommerce' ); + } else { + $message = __( 'The active version of WooCommerce does not support template cache clearing.', 'woocommerce' ); + $ran = false; + } + break; + + case 'verify_db_tables': + if ( ! method_exists( 'WC_Install', 'verify_base_tables' ) ) { + $message = __( 'You need WooCommerce 4.2 or newer to run this tool.', 'woocommerce' ); + $ran = false; + break; + } + // Try to manually create table again. + $missing_tables = WC_Install::verify_base_tables( true, true ); + if ( 0 === count( $missing_tables ) ) { + $message = __( 'Database verified successfully.', 'woocommerce' ); + } else { + $message = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' ); + $message .= implode( ', ', $missing_tables ); + $ran = false; + } + break; + + default: + $tools = $this->get_tools(); + if ( isset( $tools[ $tool ]['callback'] ) ) { + $callback = $tools[ $tool ]['callback']; + try { + $return = call_user_func( $callback ); + } catch ( Exception $exception ) { + $return = $exception; + } + if ( is_a( $return, Exception::class ) ) { + $callback_string = $this->get_printable_callback_name( $callback, $tool ); + $ran = false; + /* translators: %1$s: callback string, %2$s: error message */ + $message = sprintf( __( 'There was an error calling %1$s: %2$s', 'woocommerce' ), $callback_string, $return->getMessage() ); + + $logger = wc_get_logger(); + $logger->error( + sprintf( + 'Error running debug tool %s: %s', + $tool, + $return->getMessage() + ), + array( + 'source' => 'run-debug-tool', + 'tool' => $tool, + 'callback' => $callback, + 'error' => $return, + ) + ); + } elseif ( is_string( $return ) ) { + $message = $return; + } elseif ( false === $return ) { + $callback_string = $this->get_printable_callback_name( $callback, $tool ); + $ran = false; + /* translators: %s: callback string */ + $message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string ); + } else { + $message = __( 'Tool ran.', 'woocommerce' ); + } + } else { + $ran = false; + $message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' ); + } + break; + } + + return array( + 'success' => $ran, + 'message' => $message, + ); + } + + /** + * Get a printable name for a callback. + * + * @param mixed $callback The callback to get a name for. + * @param string $default The default name, to be returned when the callback is an inline function. + * @return string A printable name for the callback. + */ + private function get_printable_callback_name( $callback, $default ) { + if ( is_array( $callback ) ) { + return get_class( $callback[0] ) . '::' . $callback[1]; + } + if ( is_string( $callback ) ) { + return $callback; + } + + return $default; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php new file mode 100644 index 0000000..a420be3 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php @@ -0,0 +1,1288 @@ +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 ) { + $fields = $this->get_fields_for_response( $request ); + $mappings = $this->get_item_mappings_per_fields( $fields ); + $response = $this->prepare_item_for_response( $mappings, $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, + ), + '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, + ), + 'mysql_version_string' => array( + 'description' => __( 'MySQL version string.', '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', + ), + ), + 'inactive_plugins' => array( + 'description' => __( 'Inactive plugins.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'dropins_mu_plugins' => array( + 'description' => __( 'Dropins & MU 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', + ), + ), + 'post_type_counts' => array( + 'description' => __( 'Total post count.', '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. + * + * @deprecated 3.9.0 + * @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(), + 'inactive_plugins' => $this->get_inactive_plugins(), + 'dropins_mu_plugins' => $this->get_dropins_mu_plugins(), + 'theme' => $this->get_theme_info(), + 'settings' => $this->get_settings(), + 'security' => $this->get_security_info(), + 'pages' => $this->get_pages(), + 'post_type_counts' => $this->get_post_type_counts(), + ); + } + + /** + * Return an array of sections and the data associated with each. + * + * @since 3.9.0 + * @param array $fields List of fields to be included on the response. + * @return array + */ + public function get_item_mappings_per_fields( $fields ) { + return array( + 'environment' => $this->get_environment_info_per_fields( $fields ), + 'database' => $this->get_database_info(), + 'active_plugins' => $this->get_active_plugins(), + 'inactive_plugins' => $this->get_inactive_plugins(), + 'dropins_mu_plugins' => $this->get_dropins_mu_plugins(), + 'theme' => $this->get_theme_info(), + 'settings' => $this->get_settings(), + 'security' => $this->get_security_info(), + 'pages' => $this->get_pages(), + 'post_type_counts' => $this->get_post_type_counts(), + ); + } + + /** + * Get array of environment information. Includes thing like software + * versions, and various server settings. + * + * @deprecated 3.9.0 + * @return array + */ + public function get_environment_info() { + return $this->get_environment_info_per_fields( array( 'environment' ) ); + } + + /** + * Check if field item exists. + * + * @since 3.9.0 + * @param string $section Fields section. + * @param array $items List of items to check for. + * @param array $fields List of fields to be included on the response. + * @return bool + */ + private function check_if_field_item_exists( $section, $items, $fields ) { + if ( ! in_array( $section, $fields, true ) ) { + return false; + } + + $exclude = array(); + foreach ( $fields as $field ) { + $values = explode( '.', $field ); + + if ( $section !== $values[0] || empty( $values[1] ) ) { + continue; + } + + $exclude[] = $values[1]; + } + + return 0 <= count( array_intersect( $items, $exclude ) ); + } + + /** + * Get array of environment information. Includes thing like software + * versions, and various server settings. + * + * @param array $fields List of fields to be included on the response. + * @return array + */ + public function get_environment_info_per_fields( $fields ) { + global $wpdb; + + $enable_remote_post = $this->check_if_field_item_exists( 'environment', array( 'remote_post_successful', 'remote_post_response' ), $fields ); + $enable_remote_get = $this->check_if_field_item_exists( 'environment', array( 'remote_get_successful', 'remote_get_response' ), $fields ); + + // 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']; + } elseif ( extension_loaded( 'curl' ) ) { + $curl_version = __( 'cURL installed but unable to retrieve version.', 'woocommerce' ); + } + + // 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' ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + } + + // Test POST requests. + $post_response_successful = null; + $post_response_code = null; + if ( $enable_remote_post ) { + $post_response_code = get_transient( 'woocommerce_test_remote_post' ); + + if ( false === $post_response_code || is_wp_error( $post_response_code ) ) { + $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', + ), + ) + ); + if ( ! is_wp_error( $response ) ) { + $post_response_code = $response['response']['code']; + } + set_transient( 'woocommerce_test_remote_post', $post_response_code, HOUR_IN_SECONDS ); + } + + $post_response_successful = ! is_wp_error( $post_response_code ) && $post_response_code >= 200 && $post_response_code < 300; + } + + // Test GET requests. + $get_response_successful = null; + $get_response_code = null; + if ( $enable_remote_get ) { + $get_response_code = get_transient( 'woocommerce_test_remote_get' ); + + if ( false === $get_response_code || is_wp_error( $get_response_code ) ) { + $response = wp_safe_remote_get( 'https://woocommerce.com/wc-api/product-key-api?request=ping&network=' . ( is_multisite() ? '1' : '0' ) ); + if ( ! is_wp_error( $response ) ) { + $get_response_code = $response['response']['code']; + } + set_transient( 'woocommerce_test_remote_get', $get_response_code, HOUR_IN_SECONDS ); + } + + $get_response_successful = ! is_wp_error( $get_response_code ) && $get_response_code >= 200 && $get_response_code < 300; + } + + $database_version = wc_get_server_database_version(); + + // 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' => (bool) @fopen( WC_LOG_DIR . 'test-log.log', 'a' ), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen + '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(), + 'external_object_cache' => wp_using_ext_object_cache(), + 'server_info' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? wc_clean( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '', + 'php_version' => phpversion(), + 'php_post_max_size' => wc_let_to_num( ini_get( 'post_max_size' ) ), + 'php_max_execution_time' => (int) ini_get( 'max_execution_time' ), + 'php_max_input_vars' => (int) ini_get( 'max_input_vars' ), + 'curl_version' => $curl_version, + 'suhosin_installed' => extension_loaded( 'suhosin' ), + 'max_upload_size' => wp_max_upload_size(), + 'mysql_version' => $database_version['number'], + 'mysql_version_string' => $database_version['string'], + '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_code ) ? $post_response_code->get_error_message() : $post_response_code, + 'remote_get_successful' => $get_response_successful, + 'remote_get_response' => is_wp_error( $get_response_code ) ? $get_response_code->get_error_message() : $get_response_code, + ); + } + + /** + * Add prefix to table. + * + * @param string $table Table name. + * @return stromg + */ + protected function add_db_table_prefix( $table ) { + global $wpdb; + return $wpdb->prefix . $table; + } + + /** + * Get array of database information. Version, prefix, and table existence. + * + * @return array + */ + public function get_database_info() { + global $wpdb; + + $tables = array(); + $database_size = array(); + + // It is not possible to get the database name from some classes that replace wpdb (e.g., HyperDB) + // and that is why this if condition is needed. + if ( defined( 'DB_NAME' ) ) { + $database_table_information = $wpdb->get_results( + $wpdb->prepare( + "SELECT + table_name AS 'name', + engine AS 'engine', + round( ( data_length / 1024 / 1024 ), 2 ) 'data', + round( ( index_length / 1024 / 1024 ), 2 ) 'index' + FROM information_schema.TABLES + WHERE table_schema = %s + ORDER BY name ASC;", + DB_NAME + ) + ); + + // WC Core tables to check existence of. + $core_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', + 'woocommerce_log', + ) + ); + + /** + * Adding the prefix to the tables array, for backwards compatibility. + * + * If we changed the tables above to include the prefix, then any filters against that table could break. + */ + $core_tables = array_map( array( $this, 'add_db_table_prefix' ), $core_tables ); + + /** + * Organize WooCommerce and non-WooCommerce tables separately for display purposes later. + * + * To ensure we include all WC tables, even if they do not exist, pre-populate the WC array with all the tables. + */ + $tables = array( + 'woocommerce' => array_fill_keys( $core_tables, false ), + 'other' => array(), + ); + + $database_size = array( + 'data' => 0, + 'index' => 0, + ); + + $site_tables_prefix = $wpdb->get_blog_prefix( get_current_blog_id() ); + $global_tables = $wpdb->tables( 'global', true ); + foreach ( $database_table_information as $table ) { + // Only include tables matching the prefix of the current site, this is to prevent displaying all tables on a MS install not relating to the current. + if ( is_multisite() && 0 !== strpos( $table->name, $site_tables_prefix ) && ! in_array( $table->name, $global_tables, true ) ) { + continue; + } + $table_type = in_array( $table->name, $core_tables, true ) ? 'woocommerce' : 'other'; + + $tables[ $table_type ][ $table->name ] = array( + 'data' => $table->data, + 'index' => $table->index, + 'engine' => $table->engine, + ); + + $database_size['data'] += $table->data; + $database_size['index'] += $table->index; + } + } + + // 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' => '', + 'database_tables' => $tables, + 'database_size' => $database_size, + ); + } + + /** + * Get array of counts of objects. Orders, products, etc. + * + * @return array + */ + public function get_post_type_counts() { + global $wpdb; + + $post_type_counts = $wpdb->get_results( "SELECT post_type AS 'type', count(1) AS 'count' FROM {$wpdb->posts} GROUP BY post_type;" ); + + return is_array( $post_type_counts ) ? $post_type_counts : array(); + } + + /** + * Get a list of plugins active on the site. + * + * @return array + */ + public function get_active_plugins() { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + if ( ! function_exists( 'get_plugin_data' ) ) { + return array(); + } + + $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(); + + foreach ( $active_plugins as $plugin ) { + $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $active_plugins_data[] = $this->format_plugin_data( $plugin, $data ); + } + + return $active_plugins_data; + } + + /** + * Get a list of inplugins active on the site. + * + * @return array + */ + public function get_inactive_plugins() { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + if ( ! function_exists( 'get_plugins' ) ) { + return array(); + } + + $plugins = get_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 ); + } + + $plugins_data = array(); + + foreach ( $plugins as $plugin => $data ) { + if ( in_array( $plugin, $active_plugins, true ) ) { + continue; + } + $plugins_data[] = $this->format_plugin_data( $plugin, $data ); + } + + return $plugins_data; + } + + /** + * Format plugin data, including data on updates, into a standard format. + * + * @since 3.6.0 + * @param string $plugin Plugin directory/file. + * @param array $data Plugin data from WP. + * @return array Formatted data. + */ + protected function format_plugin_data( $plugin, $data ) { + require_once ABSPATH . 'wp-admin/includes/update.php'; + + if ( ! function_exists( 'get_plugin_updates' ) ) { + return array(); + } + + // Use WP API to lookup latest updates for plugins. WC_Helper injects updates for premium plugins. + if ( empty( $this->available_updates ) ) { + $this->available_updates = get_plugin_updates(); + } + + $version_latest = $data['Version']; + + // Find latest version. + if ( isset( $this->available_updates[ $plugin ]->update->new_version ) ) { + $version_latest = $this->available_updates[ $plugin ]->update->new_version; + } + + return 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'], + ); + } + + /** + * Get a list of Dropins and MU plugins. + * + * @since 3.6.0 + * @return array + */ + public function get_dropins_mu_plugins() { + $dropins = get_dropins(); + $plugins = array( + 'dropins' => array(), + 'mu_plugins' => array(), + ); + foreach ( $dropins as $key => $dropin ) { + $plugins['dropins'][] = array( + 'plugin' => $key, + 'name' => $dropin['Name'], + ); + } + + $mu_plugins = get_mu_plugins(); + foreach ( $mu_plugins as $plugin => $mu_plugin ) { + $plugins['mu_plugins'][] = array( + 'plugin' => $plugin, + 'name' => $mu_plugin['Name'], + 'version' => $mu_plugin['Version'], + 'url' => $mu_plugin['PluginURI'], + 'author_name' => $mu_plugin['AuthorName'], + 'author_url' => esc_url_raw( $mu_plugin['AuthorURI'] ), + ); + } + return $plugins; + } + + /** + * 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/' ); + + // Include *-product_ templates for backwards compatibility. + $scan_files[] = 'content-product_cat.php'; + $scan_files[] = 'taxonomy-product_cat.php'; + $scan_files[] = 'taxonomy-product_tag.php'; + + foreach ( $scan_files as $file ) { + $located = apply_filters( 'wc_get_template', $file, $file, array(), WC()->template_path(), WC()->plugin_path() . '/templates/' ); + + if ( file_exists( $located ) ) { + $theme_file = $located; + } elseif ( 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_file = $file; + + // Update *-product_ template name before searching in core. + if ( false !== strpos( $core_file, '-product_cat' ) || false !== strpos( $core_file, '-product_tag' ) ) { + $core_file = str_replace( '_', '-', $core_file ); + } + + $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $core_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' ), + '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 ); + } + + // Check if WooCommerce.com account is connected. + $woo_com_connected = 'no'; + $helper_options = get_option( 'woocommerce_helper_data', array() ); + if ( array_key_exists( 'auth', $helper_options ) && ! empty( $helper_options['auth'] ) ) { + $woo_com_connected = 'yes'; + } + + // 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' ), true ), + 'taxonomies' => $term_response, + 'product_visibility_terms' => $product_visibility_terms, + 'woocommerce_com_connected' => $woo_com_connected, + ); + } + + /** + * Returns security tips. + * + * @return array + */ + public function get_security_info() { + $check_page = wc_get_page_permalink( 'shop' ); + 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 or block. + * + * @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' => '', + 'block' => '', + ), + _x( 'Cart', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_cart_page_id', + 'shortcode' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', + 'block' => 'woocommerce/cart', + ), + _x( 'Checkout', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_checkout_page_id', + 'shortcode' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', + 'block' => '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' ) . ']', + 'block' => '', + ), + _x( 'Terms and conditions', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_terms_page_id', + 'shortcode' => '', + 'block' => '', + ), + ); + + $pages_output = array(); + foreach ( $check_pages as $page_name => $values ) { + $page_id = get_option( $values['option'] ); + $page_set = false; + $page_exists = false; + $page_visible = false; + $shortcode_present = false; + $shortcode_required = false; + $block_present = false; + $block_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; + } + } + + // Block checks. + if ( $values['block'] && get_post( $page_id ) ) { + $block_required = true; + $block_present = WC_Blocks_Utils::has_block_in_page( $page_id, $values['block'] ); + } + + // 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'], + 'block' => $values['block'], + 'shortcode_required' => $shortcode_required, + 'shortcode_present' => $shortcode_present, + 'block_present' => $block_present, + 'block_required' => $block_required, + ); + } + + 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 System status data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + 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/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php new file mode 100644 index 0000000..04f7909 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php @@ -0,0 +1,109 @@ +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::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 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' ), + ) + ); + } + + /** + * Get one tax class. + * + * @param WP_REST_Request $request Request object. + * @return array + */ + public function get_item( $request ) { + if ( 'standard' === $request['slug'] ) { + $tax_class = array( + 'slug' => 'standard', + 'name' => __( 'Standard rate', 'woocommerce' ), + ); + } else { + $tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) ); + } + + $data = array(); + if ( $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 ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php new file mode 100644 index 0000000..54b53a2 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php @@ -0,0 +1,27 @@ +/deliveries endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Webhook Deliveries controller class. + * + * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. + * @package WooCommerce\RestApi + * @extends WC_REST_Webhook_Deliveries_V1_Controller + */ +class WC_REST_Webhook_Deliveries_V2_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 + */ + public function prepare_item_for_response( $log, $request ) { + $data = (array) $log; + + $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/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php new file mode 100644 index 0000000..c64eae1 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php @@ -0,0 +1,182 @@ +post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data = array( + 'id' => $webhook->get_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_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $webhook->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $webhook->get_date_modified() ), + ); + + $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( $webhook->get_id(), $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_keys( wc_get_webhook_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + '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 to 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/rest-api/Controllers/Version3/class-wc-rest-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php new file mode 100644 index 0000000..5105138 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php @@ -0,0 +1,599 @@ + + * + * NOTE THAT ONLY CODE RELEVANT FOR MOST ENDPOINTS SHOULD BE INCLUDED INTO THIS CLASS. + * If necessary extend this class and create new abstract classes like `WC_REST_CRUD_Controller` or `WC_REST_Terms_Controller`. + * + * @class WC_REST_Controller + * @package WooCommerce\RestApi + * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/ + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * Abstract Rest Controller Class + * + * @package WooCommerce\RestApi + * @extends WP_REST_Controller + * @version 2.6.0 + */ +abstract class WC_REST_Controller extends WP_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = ''; + + /** + * Used to cache computed return fields. + * + * @var null|array + */ + private $_fields = null; + + /** + * Used to verify if cached fields are for correct request object. + * + * @var null|WP_REST_Request + */ + private $_request = null; + + /** + * Add the schema from additional fields to an schema array. + * + * The type of object is inferred from the passed schema. + * + * @param array $schema Schema array. + * + * @return array + */ + protected function add_additional_fields_schema( $schema ) { + if ( empty( $schema['title'] ) ) { + return $schema; + } + + /** + * Can't use $this->get_object_type otherwise we cause an inf loop. + */ + $object_type = $schema['title']; + + $additional_fields = $this->get_additional_fields( $object_type ); + + foreach ( $additional_fields as $field_name => $field_options ) { + if ( ! $field_options['schema'] ) { + continue; + } + + $schema['properties'][ $field_name ] = $field_options['schema']; + } + + $schema['properties'] = apply_filters( 'woocommerce_rest_' . $object_type . '_schema', $schema['properties'] ); + + return $schema; + } + + /** + * Compatibility functions for WP 5.5, since custom types are not supported anymore. + * See @link https://core.trac.wordpress.org/changeset/48306 + * + * @param string $method Optional. HTTP method of the request. + * + * @return array Endpoint arguments. + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + + $endpoint_args = parent::get_endpoint_args_for_item_schema( $method ); + + if ( false === strpos( WP_REST_Server::EDITABLE, $method ) ) { + return $endpoint_args; + } + + $endpoint_args = $this->adjust_wp_5_5_datatype_compatibility( $endpoint_args ); + + return $endpoint_args; + } + + /** + * Change datatypes `date-time` to string, and `mixed` to composite of all built in types. This is required for maintaining forward compatibility with WP 5.5 since custom post types are not supported anymore. + * + * See @link https://core.trac.wordpress.org/changeset/48306 + * + * We still use the 'mixed' type, since if we convert to composite type everywhere, it won't work in 5.4 anymore because they require to define the full schema. + * + * @param array $endpoint_args Schema with datatypes to convert. + + * @return mixed Schema with converted datatype. + */ + protected function adjust_wp_5_5_datatype_compatibility( $endpoint_args ) { + if ( version_compare( get_bloginfo( 'version' ), '5.5', '<' ) ) { + return $endpoint_args; + } + + foreach ( $endpoint_args as $field_id => $params ) { + + if ( ! isset( $params['type'] ) ) { + continue; + } + + /** + * Custom types are not supported as of WP 5.5, this translates type => 'date-time' to type => 'string'. + */ + if ( 'date-time' === $params['type'] ) { + $params['type'] = array( 'null', 'string' ); + } + + /** + * WARNING: Order of fields here is important, types of fields are ordered from most specific to least specific as perceived by core's built-in type validation methods. + */ + if ( 'mixed' === $params['type'] ) { + $params['type'] = array( 'null', 'object', 'string', 'number', 'boolean', 'integer', 'array' ); + } + + if ( isset( $params['properties'] ) ) { + $params['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['properties'] ); + } + + if ( isset( $params['items'] ) && isset( $params['items']['properties'] ) ) { + $params['items']['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['items']['properties'] ); + } + + $endpoint_args[ $field_id ] = $params; + } + return $endpoint_args; + } + + /** + * Get normalized rest base. + * + * @return string + */ + protected function get_normalized_rest_base() { + return preg_replace( '/\(.*\)\//i', '', $this->rest_base ); + } + + /** + * Check batch limit. + * + * @param array $items Request items. + * @return bool|WP_Error + */ + protected function check_batch_limit( $items ) { + $limit = apply_filters( 'woocommerce_rest_batch_items_limit', 100, $this->get_normalized_rest_base() ); + $total = 0; + + if ( ! empty( $items['create'] ) ) { + $total += count( $items['create'] ); + } + + if ( ! empty( $items['update'] ) ) { + $total += count( $items['update'] ); + } + + if ( ! empty( $items['delete'] ) ) { + $total += count( $items['delete'] ); + } + + if ( $total > $limit ) { + /* translators: %s: items limit */ + return new WP_Error( 'woocommerce_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), array( 'status' => 413 ) ); + } + + return true; + } + + /** + * Bulk create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + /** + * REST Server + * + * @var WP_REST_Server $wp_rest_server + */ + global $wp_rest_server; + + // Get the request params. + $items = array_filter( $request->get_params() ); + $query = $request->get_query_params(); + $response = array(); + + // Check batch limit. + $limit = $this->check_batch_limit( $items ); + if ( is_wp_error( $limit ) ) { + return $limit; + } + + if ( ! empty( $items['create'] ) ) { + foreach ( $items['create'] as $item ) { + $_item = new WP_REST_Request( 'POST' ); + + // Default parameters. + $defaults = array(); + $schema = $this->get_public_item_schema(); + foreach ( $schema['properties'] as $arg => $options ) { + if ( isset( $options['default'] ) ) { + $defaults[ $arg ] = $options['default']; + } + } + $_item->set_default_params( $defaults ); + + // Set request parameters. + $_item->set_body_params( $item ); + + // Set query (GET) parameters. + $_item->set_query_params( $query ); + + $_response = $this->create_item( $_item ); + + if ( is_wp_error( $_response ) ) { + $response['create'][] = array( + 'id' => 0, + 'error' => array( + 'code' => $_response->get_error_code(), + 'message' => $_response->get_error_message(), + 'data' => $_response->get_error_data(), + ), + ); + } else { + $response['create'][] = $wp_rest_server->response_to_data( $_response, '' ); + } + } + } + + if ( ! empty( $items['update'] ) ) { + foreach ( $items['update'] as $item ) { + $_item = new WP_REST_Request( 'PUT' ); + $_item->set_body_params( $item ); + $_response = $this->update_item( $_item ); + + if ( is_wp_error( $_response ) ) { + $response['update'][] = array( + 'id' => $item['id'], + 'error' => array( + 'code' => $_response->get_error_code(), + 'message' => $_response->get_error_message(), + 'data' => $_response->get_error_data(), + ), + ); + } else { + $response['update'][] = $wp_rest_server->response_to_data( $_response, '' ); + } + } + } + + if ( ! empty( $items['delete'] ) ) { + foreach ( $items['delete'] as $id ) { + $id = (int) $id; + + if ( 0 === $id ) { + continue; + } + + $_item = new WP_REST_Request( 'DELETE' ); + $_item->set_query_params( + array( + 'id' => $id, + 'force' => true, + ) + ); + $_response = $this->delete_item( $_item ); + + if ( is_wp_error( $_response ) ) { + $response['delete'][] = array( + 'id' => $id, + 'error' => array( + 'code' => $_response->get_error_code(), + 'message' => $_response->get_error_message(), + 'data' => $_response->get_error_data(), + ), + ); + } else { + $response['delete'][] = $wp_rest_server->response_to_data( $_response, '' ); + } + } + } + + return $response; + } + + /** + * Validate a text value for a text based setting. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string + */ + public function validate_setting_text_field( $value, $setting ) { + $value = is_null( $value ) ? '' : $value; + return wp_kses_post( trim( stripslashes( $value ) ) ); + } + + /** + * Validate select based settings. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string|WP_Error + */ + public function validate_setting_select_field( $value, $setting ) { + if ( array_key_exists( $value, $setting['options'] ) ) { + return $value; + } else { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + /** + * Validate multiselect based settings. + * + * @since 3.0.0 + * @param array $values Values. + * @param array $setting Setting. + * @return array|WP_Error + */ + public function validate_setting_multiselect_field( $values, $setting ) { + if ( empty( $values ) ) { + return array(); + } + + if ( ! is_array( $values ) ) { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $final_values = array(); + foreach ( $values as $value ) { + if ( array_key_exists( $value, $setting['options'] ) ) { + $final_values[] = $value; + } + } + + return $final_values; + } + + /** + * Validate image_width based settings. + * + * @since 3.0.0 + * @param array $values Values. + * @param array $setting Setting. + * @return string|WP_Error + */ + public function validate_setting_image_width_field( $values, $setting ) { + if ( ! is_array( $values ) ) { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $current = $setting['value']; + if ( isset( $values['width'] ) ) { + $current['width'] = intval( $values['width'] ); + } + if ( isset( $values['height'] ) ) { + $current['height'] = intval( $values['height'] ); + } + if ( isset( $values['crop'] ) ) { + $current['crop'] = (bool) $values['crop']; + } + return $current; + } + + /** + * Validate radio based settings. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string|WP_Error + */ + public function validate_setting_radio_field( $value, $setting ) { + return $this->validate_setting_select_field( $value, $setting ); + } + + /** + * Validate checkbox based settings. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string|WP_Error + */ + public function validate_setting_checkbox_field( $value, $setting ) { + if ( in_array( $value, array( 'yes', 'no' ) ) ) { + return $value; + } elseif ( empty( $value ) ) { + $value = isset( $setting['default'] ) ? $setting['default'] : 'no'; + return $value; + } else { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + /** + * Validate textarea based settings. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string + */ + public function validate_setting_textarea_field( $value, $setting ) { + $value = is_null( $value ) ? '' : $value; + return wp_kses( + trim( stripslashes( $value ) ), + array_merge( + array( + 'iframe' => array( + 'src' => true, + 'style' => true, + 'id' => true, + 'class' => true, + ), + ), + wp_kses_allowed_html( 'post' ) + ) + ); + } + + /** + * Add meta query. + * + * @since 3.0.0 + * @param array $args Query args. + * @param array $meta_query Meta query. + * @return array + */ + protected function add_meta_query( $args, $meta_query ) { + if ( empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = $meta_query; + + return $args['meta_query']; + } + + /** + * Get the batch schema, conforming to JSON Schema. + * + * @return array + */ + public function get_public_batch_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'batch', + 'type' => 'object', + 'properties' => array( + 'create' => array( + 'description' => __( 'List of created resources.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + ), + ), + 'update' => array( + 'description' => __( 'List of updated resources.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + ), + ), + 'delete' => array( + 'description' => __( 'List of delete resources.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + ), + ), + ); + + return $schema; + } + + /** + * Gets an array of fields to be included on the response. + * + * Included fields are based on item schema and `_fields=` request argument. + * Updated from WordPress 5.3, included into this class to support old versions. + * + * @since 3.5.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Fields to be included in the response. + */ + public function get_fields_for_response( $request ) { + // From xdebug profiling, this method could take upto 25% of request time in index calls. + // Cache it and make sure _fields was cached on current request object! + // TODO: Submit this caching behavior in core. + if ( isset( $this->_fields ) && is_array( $this->_fields ) && $request === $this->_request ) { + return $this->_fields; + } + $this->_request = $request; + + $schema = $this->get_item_schema(); + $properties = isset( $schema['properties'] ) ? $schema['properties'] : array(); + + $additional_fields = $this->get_additional_fields(); + + foreach ( $additional_fields as $field_name => $field_options ) { + // For back-compat, include any field with an empty schema + // because it won't be present in $this->get_item_schema(). + if ( is_null( $field_options['schema'] ) ) { + $properties[ $field_name ] = $field_options; + } + } + + // Exclude fields that specify a different context than the request context. + $context = $request['context']; + if ( $context ) { + foreach ( $properties as $name => $options ) { + if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) { + unset( $properties[ $name ] ); + } + } + } + + $fields = array_keys( $properties ); + + if ( ! isset( $request['_fields'] ) ) { + $this->_fields = $fields; + return $fields; + } + $requested_fields = wp_parse_list( $request['_fields'] ); + if ( 0 === count( $requested_fields ) ) { + $this->_fields = $fields; + return $fields; + } + // Trim off outside whitespace from the comma delimited list. + $requested_fields = array_map( 'trim', $requested_fields ); + // Always persist 'id', because it can be needed for add_additional_fields_to_object(). + if ( in_array( 'id', $fields, true ) ) { + $requested_fields[] = 'id'; + } + // Return the list of all requested fields which appear in the schema. + $this->_fields = array_reduce( + $requested_fields, + function( $response_fields, $field ) use ( $fields ) { + if ( in_array( $field, $fields, true ) ) { + $response_fields[] = $field; + return $response_fields; + } + // Check for nested fields if $field is not a direct match. + $nested_fields = explode( '.', $field ); + // A nested field is included so long as its top-level property + // is present in the schema. + if ( in_array( $nested_fields[0], $fields, true ) ) { + $response_fields[] = $field; + } + return $response_fields; + }, + array() + ); + return $this->_fields; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php new file mode 100644 index 0000000..b975964 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php @@ -0,0 +1,27 @@ + 405 ) ); + } + + /** + * Check if a given request has access to read an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get object permalink. + * + * @param object $object Object. + * @return string + */ + protected function get_permalink( $object ) { + return ''; + } + + /** + * Prepares the object for the REST response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. + */ + protected function prepare_object_for_response( $object, $request ) { + // translators: %s: Class method name. + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepares one object for create or update operation. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + // translators: %s: Class method name. + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Get a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_object_for_response( $object, $request ); + $response = rest_ensure_response( $data ); + + if ( $this->public ) { + $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) ); + } + + return $response; + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $object->save(); + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $object = $this->save_object( $request, true ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + try { + $this->update_additional_fields_for_object( $object, $request ); + + /** + * Fires after a single object is created or updated via the REST API. + * + * @param WC_Data $object Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true ); + } catch ( WC_Data_Exception $e ) { + $object->delete(); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + $object->delete(); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) ); + + return $response; + } + + /** + * Update a single post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $object = $this->save_object( $request, false ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + try { + $this->update_additional_fields_for_object( $object, $request ); + + /** + * Fires after a single object is created or updated via the REST API. + * + * @param WC_Data $object Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + return rest_ensure_response( $response ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = array(); + $args['offset'] = $request['offset']; + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['paged'] = $request['page']; + $args['post__in'] = $request['include']; + $args['post__not_in'] = $request['exclude']; + $args['posts_per_page'] = $request['per_page']; + $args['name'] = $request['slug']; + $args['post_parent__in'] = $request['parent']; + $args['post_parent__not_in'] = $request['parent_exclude']; + $args['s'] = $request['search']; + $args['fields'] = $this->get_fields_for_response( $request ); + + if ( 'date' === $args['orderby'] ) { + $args['orderby'] = 'date ID'; + } + + $date_query = array(); + $use_gmt = $request['dates_are_gmt']; + + if ( isset( $request['before'] ) ) { + $date_query[] = array( + 'column' => $use_gmt ? 'post_date_gmt' : 'post_date', + 'before' => $request['before'], + ); + } + + if ( isset( $request['after'] ) ) { + $date_query[] = array( + 'column' => $use_gmt ? 'post_date_gmt' : 'post_date', + 'after' => $request['after'], + ); + } + + if ( isset( $request['modified_before'] ) ) { + $date_query[] = array( + 'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified', + 'before' => $request['modified_before'], + ); + } + + if ( isset( $request['modified_after'] ) ) { + $date_query[] = array( + 'column' => $use_gmt ? 'post_modified_gmt' : 'post_modified', + 'after' => $request['modified_after'], + ); + } + + if ( ! empty( $date_query ) ) { + $date_query['relation'] = 'AND'; + $args['date_query'] = $date_query; + } + + // Force the post_type argument, since it's not a user input variable. + $args['post_type'] = $this->post_type; + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for a post + * collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request ); + + return $this->prepare_items_query( $args, $request ); + } + + /** + * Get objects. + * + * @since 3.0.0 + * @param array $query_args Query args. + * @return array + */ + protected function get_objects( $query_args ) { + $query = new WP_Query(); + $result = $query->query( $query_args ); + + $total_posts = $query->found_posts; + if ( $total_posts < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $query_args['paged'] ); + $count_query = new WP_Query(); + $count_query->query( $query_args ); + $total_posts = $count_query->found_posts; + } + + return array( + 'objects' => array_filter( array_map( array( $this, 'get_object' ), $result ) ), + 'total' => (int) $total_posts, + 'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ), + ); + } + + /** + * Get a collection of posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $query_args = $this->prepare_objects_query( $request ); + if ( is_wp_error( current( $query_args ) ) ) { + return current( $query_args ); + } + $query_results = $this->get_objects( $query_args ); + + $objects = array(); + foreach ( $query_results['objects'] as $object ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { + continue; + } + + $data = $this->prepare_object_for_response( $object, $request ); + $objects[] = $this->prepare_response_for_collection( $data ); + } + + $page = (int) $query_args['paged']; + $max_pages = $query_results['pages']; + + $response = rest_ensure_response( $objects ); + $response->header( 'X-WP-Total', $query_results['total'] ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = $this->rest_base; + $attrib_prefix = '(?P<'; + if ( strpos( $base, $attrib_prefix ) !== false ) { + $attrib_names = array(); + preg_match( '/\(\?P<[^>]+>.*\)/', $base, $attrib_names, PREG_OFFSET_CAPTURE ); + foreach ( $attrib_names as $attrib_name_match ) { + $beginning_offset = strlen( $attrib_prefix ); + $attrib_name_end = strpos( $attrib_name_match[0], '>', $attrib_name_match[1] ); + $attrib_name = substr( $attrib_name_match[0], $beginning_offset, $attrib_name_end - $beginning_offset ); + if ( isset( $request[ $attrib_name ] ) ) { + $base = str_replace( "(?P<$attrib_name>[\d]+)", $request[ $attrib_name ], $base ); + } + } + } + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ) ); + + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Get fields for an object if getter is defined. + * + * @param object $object Object we are fetching response for. + * @param string $context Context of the request. Can be `view` or `edit`. + * @param array $fields List of fields to fetch. + * @return array Data fetched from getters. + */ + public function fetch_fields_using_getters( $object, $context, $fields ) { + $data = array(); + foreach ( $fields as $field ) { + if ( method_exists( $this, "api_get_$field" ) ) { + $data[ $field ] = $this->{"api_get_$field"}( $object, $context ); + } + } + return $data; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param(); + $params['context']['default'] = 'view'; + + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['search'] = array( + 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['modified_after'] = array( + 'description' => __( 'Limit response to resources modified after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['modified_before'] = array( + 'description' => __( 'Limit response to resources modified before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dates_are_gmt'] = array( + 'description' => __( 'Whether to consider GMT post dates when limiting response by published or modified date.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'include', + 'title', + 'slug', + 'modified', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + if ( $this->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + $params['parent_exclude'] = array( + 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + } + + /** + * Filter collection parameters for the posts controller. + * + * The dynamic part of the filter `$this->post_type` refers to the post + * type slug for the controller. + * + * This filter registers the collection parameter, but does not map the + * collection parameter to an internal WP_Query parameter. Use the + * `rest_{$this->post_type}_query` filter to set WP_Query parameters. + * + * @param array $query_params JSON Schema-formatted collection parameters. + * @param WP_Post_Type $post_type Post type object. + */ + return apply_filters( "rest_{$this->post_type}_collection_params", $params, $this->post_type ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php new file mode 100644 index 0000000..f6b8c98 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php @@ -0,0 +1,27 @@ +/downloads endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Customers controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Customer_Downloads_V2_Controller + */ +class WC_REST_Customer_Downloads_Controller extends WC_REST_Customer_Downloads_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php new file mode 100644 index 0000000..217aef0 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php @@ -0,0 +1,312 @@ +get_data(); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + // Date created is stored UTC, date modified is stored WP local time. + $datetime = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + return array( + 'id' => $object->get_id(), + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'role' => $data['role'], + 'username' => $data['username'], + 'billing' => $data['billing'], + 'shipping' => $data['shipping'], + 'is_paying_customer' => $data['is_paying_customer'], + 'avatar_url' => $object->get_avatar_url(), + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'role' => array( + 'description' => __( 'Customer role.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'is_paying_customer' => array( + 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), + 'type' => 'bool', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php new file mode 100644 index 0000000..f998271 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php @@ -0,0 +1,362 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'continent' => array( + 'description' => __( '2 character continent code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Return the list of countries and states for a given continent. + * + * @since 3.5.0 + * @param string $continent_code Continent code. + * @param WP_REST_Request $request Request data. + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_continent( $continent_code, $request ) { + $continents = WC()->countries->get_continents(); + $countries = WC()->countries->get_countries(); + $states = WC()->countries->get_states(); + $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; + $data = array(); + + if ( ! array_key_exists( $continent_code, $continents ) ) { + return false; + } + + $continent_list = $continents[ $continent_code ]; + + $continent = array( + 'code' => $continent_code, + 'name' => $continent_list['name'], + ); + + $local_countries = array(); + foreach ( $continent_list['countries'] as $country_code ) { + if ( isset( $countries[ $country_code ] ) ) { + $country = array( + 'code' => $country_code, + 'name' => $countries[ $country_code ], + ); + + // If we have detailed locale information include that in the response. + if ( array_key_exists( $country_code, $locale_info ) ) { + // Defensive programming against unexpected changes in locale-info.php. + $country_data = wp_parse_args( + $locale_info[ $country_code ], + array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'decimal_sep' => '.', + 'dimension_unit' => 'in', + 'num_decimals' => 2, + 'thousand_sep' => ',', + 'weight_unit' => 'lbs', + ) + ); + + $country = array_merge( $country, $country_data ); + } + + $local_states = array(); + if ( isset( $states[ $country_code ] ) ) { + foreach ( $states[ $country_code ] as $state_code => $state_name ) { + $local_states[] = array( + 'code' => $state_code, + 'name' => $state_name, + ); + } + } + $country['states'] = $local_states; + + // Allow only desired keys (e.g. filter out tax rates). + $allowed = array( + 'code', + 'currency_code', + 'currency_pos', + 'decimal_sep', + 'dimension_unit', + 'name', + 'num_decimals', + 'states', + 'thousand_sep', + 'weight_unit', + ); + $country = array_intersect_key( $country, array_flip( $allowed ) ); + + $local_countries[] = $country; + } + } + + $continent['countries'] = $local_countries; + return $continent; + } + + /** + * Return the list of states for all continents. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $continents = WC()->countries->get_continents(); + $data = array(); + + foreach ( array_keys( $continents ) as $continent_code ) { + $continent = $this->get_continent( $continent_code, $request ); + $response = $this->prepare_item_for_response( $continent, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return the list of locations for a given continent. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_continent( strtoupper( $request['location'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Prepare the data object for response. + * + * @since 3.5.0 + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter the location list returned from the API. + * + * Allows modification of the loction data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param array $item The original list of continent(s), countries, and states. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given continent. + */ + protected function prepare_links( $item ) { + $continent_code = strtolower( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + return $links; + } + + /** + * Get the location schema, conforming to JSON Schema. + * + * @since 3.5.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_continents', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( '2 character continent code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of continent.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'countries' => array( + 'type' => 'array', + 'description' => __( 'List of countries on this continent.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_code' => array( + 'type' => 'string', + 'description' => __( 'Default ISO4127 alpha-3 currency code for the country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_pos' => array( + 'type' => 'string', + 'description' => __( 'Currency symbol position for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'decimal_sep' => array( + 'type' => 'string', + 'description' => __( 'Decimal separator for displayed prices for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'dimension_unit' => array( + 'type' => 'string', + 'description' => __( 'The unit lengths are defined in for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'num_decimals' => array( + 'type' => 'integer', + 'description' => __( 'Number of decimal points shown in displayed prices for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'states' => array( + 'type' => 'array', + 'description' => __( 'List of states in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'State code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of state.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + 'thousand_sep' => array( + 'type' => 'string', + 'description' => __( 'Thousands separator for displayed prices in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'weight_unit' => array( + 'type' => 'string', + 'description' => __( 'The unit weights are defined in for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php new file mode 100644 index 0000000..9315030 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php @@ -0,0 +1,184 @@ +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' ), + ) + ); + } + + /** + * Check whether a given request has permission to read site data. + * + * @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 whether a given request has permission to read site settings. + * + * @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; + } + + /** + * Return the list of data resources. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $data = array(); + $resources = array( + array( + 'slug' => 'continents', + 'description' => __( 'List of supported continents, countries, and states.', 'woocommerce' ), + ), + array( + 'slug' => 'countries', + 'description' => __( 'List of supported states in a given country.', 'woocommerce' ), + ), + array( + 'slug' => 'currencies', + 'description' => __( 'List of supported currencies.', 'woocommerce' ), + ), + ); + + foreach ( $resources as $resource ) { + $item = $this->prepare_item_for_response( (object) $resource, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a data resource object for serialization. + * + * @param stdClass $resource Resource data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $resource, $request ) { + $data = array( + 'slug' => $resource->slug, + 'description' => $resource->description, + ); + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $resource ) ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given country. + */ + protected function prepare_links( $item ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $item->slug ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the data index schema, conforming to JSON Schema. + * + * @since 3.5.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_index', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'Data resource ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Data resource description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php new file mode 100644 index 0000000..7144e5f --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php @@ -0,0 +1,244 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'location' => array( + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get a list of countries and states. + * + * @param string $country_code Country code. + * @param WP_REST_Request $request Request data. + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_country( $country_code, $request ) { + $countries = WC()->countries->get_countries(); + $states = WC()->countries->get_states(); + $data = array(); + + if ( ! array_key_exists( $country_code, $countries ) ) { + return false; + } + + $country = array( + 'code' => $country_code, + 'name' => $countries[ $country_code ], + ); + + $local_states = array(); + if ( isset( $states[ $country_code ] ) ) { + foreach ( $states[ $country_code ] as $state_code => $state_name ) { + $local_states[] = array( + 'code' => $state_code, + 'name' => $state_name, + ); + } + } + $country['states'] = $local_states; + return $country; + } + + /** + * Return the list of states for all countries. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $countries = WC()->countries->get_countries(); + $data = array(); + + foreach ( array_keys( $countries ) as $country_code ) { + $country = $this->get_country( $country_code, $request ); + $response = $this->prepare_item_for_response( $country, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return the list of states for a given country. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_country( strtoupper( $request['location'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Prepare the data object for response. + * + * @since 3.5.0 + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter the states list for a country returned from the API. + * + * Allows modification of the loction data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param array $data The original country's states list. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_country', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given country. + */ + protected function prepare_links( $item ) { + $country_code = strtolower( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $country_code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + + /** + * Get the location schema, conforming to JSON Schema. + * + * @since 3.5.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_countries', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'states' => array( + 'type' => 'array', + 'description' => __( 'List of states in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'State code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of state.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php new file mode 100644 index 0000000..09fca0f --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php @@ -0,0 +1,227 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/current', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_current_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]{3})', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'location' => array( + 'description' => __( 'ISO4217 currency code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get currency information. + * + * @param string $code Currency code. + * @param WP_REST_Request $request Request data. + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_currency( $code, $request ) { + $currencies = get_woocommerce_currencies(); + $data = array(); + + if ( ! array_key_exists( $code, $currencies ) ) { + return false; + } + + $currency = array( + 'code' => $code, + 'name' => $currencies[ $code ], + 'symbol' => get_woocommerce_currency_symbol( $code ), + ); + + return $currency; + } + + /** + * Return the list of currencies. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $currencies = get_woocommerce_currencies(); + foreach ( array_keys( $currencies ) as $code ) { + $currency = $this->get_currency( $code, $request ); + $response = $this->prepare_item_for_response( $currency, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return information for a specific currency. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_currency( strtoupper( $request['currency'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_currency', __( 'There are no currencies matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Return information for the current site currency. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_current_item( $request ) { + $currency = get_option( 'woocommerce_currency' ); + return $this->prepare_item_for_response( $this->get_currency( $currency, $request ), $request ); + } + + /** + * Prepare the data object for response. + * + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter currency returned from the API. + * + * @param WP_REST_Response $response The response object. + * @param array $item Currency data. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_currency', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given currency. + */ + protected function prepare_links( $item ) { + $code = strtoupper( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + + /** + * Get the currency schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_currencies', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO4217 currency code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of currency.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'symbol' => array( + 'type' => 'string', + 'description' => __( 'Currency symbol.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php new file mode 100644 index 0000000..e37e2da --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php @@ -0,0 +1,27 @@ +/notes endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Order Notes controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Order_Notes_V2_Controller + */ +class WC_REST_Order_Notes_Controller extends WC_REST_Order_Notes_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * 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, + 'author' => __( 'woocommerce', 'woocommerce' ) === $note->comment_author ? 'system' : $note->comment_author, + '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 ); + } + + /** + * 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'], $request['added_by_user'] ); + + 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 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, + ), + 'author' => array( + 'description' => __( 'Order note author.', 'woocommerce' ), + 'type' => 'string', + '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' ), + ), + 'added_by_user' => array( + 'description' => __( 'If true, this note will be attributed to the current user. If false, the note will be attributed to the system.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php new file mode 100644 index 0000000..bd02194 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php @@ -0,0 +1,122 @@ +/refunds endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +use Automattic\WooCommerce\Internal\RestApiUtil; + +/** + * REST API Order Refunds controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Order_Refunds_V2_Controller + */ +class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Prepares one object for create or update operation. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + RestApiUtil::adjust_create_refund_request_parameters( $request ); + + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + 'amount' => $request['amount'], + 'reason' => $request['reason'], + 'line_items' => $request['line_items'], + 'refund_payment' => $request['api_refund'], + 'restock_items' => $request['api_restock'], + ) + ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + $refund->save_meta_data(); + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); + } + + /** + * Get the refund schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['line_items']['items']['properties']['refund_total'] = array( + 'description' => __( 'Amount that will be refunded for this line item (excluding taxes).', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'edit' ), + 'readonly' => true, + ); + + $schema['properties']['line_items']['items']['properties']['taxes']['items']['properties']['refund_total'] = array( + 'description' => __( 'Amount that will be refunded for this tax.', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'edit' ), + 'readonly' => true, + ); + + $schema['properties']['api_restock'] = array( + 'description' => __( 'When true, refunded items are restocked.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + 'default' => true, + ); + + return $schema; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php new file mode 100644 index 0000000..d61a52c --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php @@ -0,0 +1,300 @@ +get_coupons() ); + $current_order_coupon_codes = array_map( + function( $coupon ) { + return $coupon->get_code(); + }, + $current_order_coupons + ); + + foreach ( $request['coupon_lines'] as $item ) { + if ( ! empty( $item['id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_coupon_item_id_readonly', __( 'Coupon item ID is readonly.', 'woocommerce' ), 400 ); + } + + if ( empty( $item['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $coupon_code = wc_format_coupon_code( wc_clean( $item['code'] ) ); + $coupon = new WC_Coupon( $coupon_code ); + + // Skip check if the coupon is already applied to the order, as this could wrongly throw an error for single-use coupons. + if ( ! in_array( $coupon_code, $current_order_coupon_codes, true ) ) { + $check_result = $discounts->is_coupon_valid( $coupon ); + if ( is_wp_error( $check_result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_' . $check_result->get_error_code(), $check_result->get_error_message(), 400 ); + } + } + + $coupon_codes[] = $coupon_code; + } + + // Remove all coupons first to ensure calculation is correct. + foreach ( $order->get_items( 'coupon' ) as $existing_coupon ) { + $order->remove_coupon( $existing_coupon->get_code() ); + } + + // Apply the coupons. + foreach ( $coupon_codes as $new_coupon ) { + $results = $order->apply_coupon( $new_coupon ); + + if ( is_wp_error( $results ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 ); + } + } + + return true; + } + + /** + * Prepare a single order for create or update. + * + * @throws WC_REST_Exception When fails to set any item. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'coupon_lines': + case 'status': + // Change should be done later so transitions have new data. + break; + case 'billing': + case 'shipping': + $this->update_address( $order, $value, $key ); + break; + case 'line_items': + case 'shipping_lines': + case 'fee_lines': + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default: + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $order Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @throws WC_REST_Exception But all errors are validated before returning any data. + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + // Make sure gateways are loaded so hooks from gateways fire on save/create. + WC()->payment_gateways(); + + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + } + + if ( $creating ) { + $object->set_created_via( 'rest-api' ); + $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $object->calculate_totals(); + } else { + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $object->calculate_totals( true ); + } + } + + // Set coupons. + $this->calculate_coupons( $request, $object ); + + // Set status. + if ( ! empty( $request['status'] ) ) { + $object->set_status( $request['status'] ); + } + + $object->save(); + + // Actions for after the order is saved. + if ( true === $request['set_paid'] ) { + if ( $creating || $object->needs_payment() ) { + $object->payment_complete( $request['transaction_id'] ); + } + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + // This is needed to get around an array to string notice in WC_REST_Orders_V2_Controller::prepare_objects_query. + $statuses = $request['status']; + unset( $request['status'] ); + $args = parent::prepare_objects_query( $request ); + + $args['post_status'] = array(); + foreach ( $statuses as $status ) { + if ( in_array( $status, $this->get_order_statuses(), true ) ) { + $args['post_status'][] = 'wc-' . $status; + } elseif ( 'any' === $status ) { + // Set status to "any" and short-circuit out. + $args['post_status'] = 'any'; + break; + } else { + $args['post_status'][] = $status; + } + } + + // Put the statuses back for further processing (next/prev links, etc). + $request['status'] = $statuses; + + return $args; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['coupon_lines']['items']['properties']['discount']['readonly'] = true; + + return $schema; + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders which have specific statuses.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php new file mode 100644 index 0000000..8ffc68b --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php @@ -0,0 +1,226 @@ + $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(), + 'method_supports' => $gateway->supports, + '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 Gateway instance. + * + * @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 'enabled' and 'description' which get included elsewhere. + if ( in_array( $id, array( 'enabled', 'description' ), true ) ) { + 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; + } + + /** + * 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, + ), + 'method_supports' => array( + 'description' => __( 'Supported features for this payment gateway.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + '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 ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php new file mode 100644 index 0000000..59757f3 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php @@ -0,0 +1,724 @@ +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 to create an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + 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 an item. + * + * @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['id'] ); + + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, '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 update an item. + * + * @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['id'] ); + + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $post = get_post( (int) $request['id'] ); + + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->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 boolean|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, '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 a single item. + * + * @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']; + $post = get_post( $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_invalid_{$this->post_type}_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $data ); + + if ( $this->public ) { + $response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) ); + } + + return $response; + } + + /** + * 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 ) ); + } + + $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; + $post = get_post( $post_id ); + + $this->update_additional_fields_for_object( $post, $request ); + + // Add meta fields. + $meta_fields = $this->add_post_meta_fields( $post, $request ); + if ( is_wp_error( $meta_fields ) ) { + // Remove post. + $this->delete_post( $post ); + + return $meta_fields; + } + + /** + * 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; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post Post Object. + * @param WP_REST_Request $request WP_REST_Request Object. + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return true; + } + + /** + * Delete post. + * + * @param WP_Post $post Post object. + */ + protected function delete_post( $post ) { + wp_delete_post( $post->ID, true ); + } + + /** + * Update a single post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $post = get_post( $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", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $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 ); + + // Update meta fields. + $meta_fields = $this->update_post_meta_fields( $post, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * 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 ); + } + + /** + * Get a collection of posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $args = array(); + $args['offset'] = $request['offset']; + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['paged'] = $request['page']; + $args['post__in'] = $request['include']; + $args['post__not_in'] = $request['exclude']; + $args['posts_per_page'] = $request['per_page']; + $args['name'] = $request['slug']; + $args['post_parent__in'] = $request['parent']; + $args['post_parent__not_in'] = $request['parent_exclude']; + $args['s'] = $request['search']; + + $args['date_query'] = array(); + // Set before into date query. Date query must be specified as an array of an array. + if ( isset( $request['before'] ) ) { + $args['date_query'][0]['before'] = $request['before']; + } + + // Set after into date query. Date query must be specified as an array of an array. + if ( isset( $request['after'] ) ) { + $args['date_query'][0]['after'] = $request['after']; + } + + if ( 'wc/v1' === $this->namespace ) { + 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. + $args['post_type'] = $this->post_type; + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for a post + * collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( "woocommerce_rest_{$this->post_type}_query", $args, $request ); + $query_args = $this->prepare_items_query( $args, $request ); + + $posts_query = new WP_Query(); + $query_result = $posts_query->query( $query_args ); + + $posts = array(); + foreach ( $query_result as $post ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) { + continue; + } + + $data = $this->prepare_item_for_response( $post, $request ); + $posts[] = $this->prepare_response_for_collection( $data ); + } + + $page = (int) $query_args['paged']; + $total_posts = $posts_query->found_posts; + + if ( $total_posts < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $query_args['paged'] ); + $count_query = new WP_Query(); + $count_query->query( $query_args ); + $total_posts = $count_query->found_posts; + } + + $max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] ); + + $response = rest_ensure_response( $posts ); + $response->header( 'X-WP-Total', (int) $total_posts ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $request_params = $request->get_query_params(); + if ( ! empty( $request_params['filter'] ) ) { + // Normalize the pagination params. + unset( $request_params['filter']['posts_per_page'] ); + unset( $request_params['filter']['paged'] ); + } + $base = add_query_arg( $request_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; + } + + /** + * 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 ); + + if ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', '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 ) { + $result = wp_delete_post( $id, 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.) + $result = wp_trash_post( $id ); + } + + 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 ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $post, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Determine the allowed query_vars for a get_items() response and + * prepare for WP_Query. + * + * @param array $prepared_args Prepared arguments. + * @param WP_REST_Request $request Request object. + * @return array $query_args + */ + protected function prepare_items_query( $prepared_args = array(), $request = null ) { + + $valid_vars = array_flip( $this->get_allowed_query_vars() ); + $query_args = array(); + foreach ( $valid_vars as $var => $index ) { + if ( isset( $prepared_args[ $var ] ) ) { + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $var, refers to the query_var key. + * + * @param mixed $prepared_args[ $var ] The query_var value. + */ + $query_args[ $var ] = apply_filters( "woocommerce_rest_query_var-{$var}", $prepared_args[ $var ] ); + } + } + + $query_args['ignore_sticky_posts'] = true; + + if ( 'include' === $query_args['orderby'] ) { + $query_args['orderby'] = 'post__in'; + } elseif ( 'id' === $query_args['orderby'] ) { + $query_args['orderby'] = 'ID'; // ID must be capitalized. + } elseif ( 'slug' === $query_args['orderby'] ) { + $query_args['orderby'] = 'name'; + } + + return $query_args; + } + + /** + * Get all the WP Query vars that are allowed for the API request. + * + * @return array + */ + protected function get_allowed_query_vars() { + global $wp; + + /** + * Filter the publicly allowed query vars. + * + * Allows adjusting of the default query vars that are made public. + * + * @param array Array of allowed WP_Query query vars. + */ + $valid_vars = apply_filters( 'query_vars', $wp->public_query_vars ); + + $post_type_obj = get_post_type_object( $this->post_type ); + if ( current_user_can( $post_type_obj->cap->edit_posts ) ) { + /** + * Filter the allowed 'private' query vars for authorized users. + * + * If the user has the `edit_posts` capability, we also allow use of + * private query parameters, which are only undesirable on the + * frontend, but are safe for use in query strings. + * + * To disable anyway, use + * `add_filter( 'woocommerce_rest_private_query_vars', '__return_empty_array' );` + * + * @param array $private_query_vars Array of allowed query vars for authorized users. + * } + */ + $private = apply_filters( 'woocommerce_rest_private_query_vars', $wp->private_query_vars ); + $valid_vars = array_merge( $valid_vars, $private ); + } + // Define our own in addition to WP's normal vars. + $rest_valid = array( + 'date_query', + 'ignore_sticky_posts', + 'offset', + 'post__in', + 'post__not_in', + 'post_parent', + 'post_parent__in', + 'post_parent__not_in', + 'posts_per_page', + 'meta_query', + 'tax_query', + 'meta_key', + 'meta_value', + 'meta_compare', + 'meta_value_num', + ); + $valid_vars = array_merge( $valid_vars, $rest_valid ); + + /** + * Filter allowed query vars for the REST API. + * + * This filter allows you to add or remove query vars from the final allowed + * list for all requests, including unauthenticated ones. To alter the + * vars for editors only. + * + * @param array { + * Array of allowed WP_Query query vars. + * + * @param string $allowed_query_var The query var to allow. + * } + */ + $valid_vars = apply_filters( 'woocommerce_rest_query_vars', $valid_vars ); + + return $valid_vars; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'include', + 'title', + 'slug', + 'modified', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + $post_type_obj = get_post_type_object( $this->post_type ); + + if ( isset( $post_type_obj->hierarchical ) && $post_type_obj->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + $params['parent_exclude'] = array( + 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + } + + if ( 'wc/v1' === $this->namespace ) { + $params['filter'] = array( + 'type' => 'object', + 'description' => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.', 'woocommerce' ), + ); + } + + return $params; + } + + /** + * Update post meta fields. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + return true; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php new file mode 100644 index 0000000..3d08490 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php @@ -0,0 +1,27 @@ +/terms endpoint. + * + * @package WooCommerce\RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Product Attribute Terms controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Product_Attribute_Terms_V2_Controller + */ +class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Product_Attribute_Terms_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php new file mode 100644 index 0000000..06c55f5 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php @@ -0,0 +1,27 @@ +term_id, 'display_type', true ); + + // Get category order. + $menu_order = get_term_meta( $item->term_id, 'order', true ); + + $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' => null, + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); + if ( $image_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 ), + 'name' => 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 ); + } + + /** + * Update term meta fields. + * + * @param WP_Term $term Term object. + * @param WP_REST_Request $request Request instance. + * @return bool|WP_Error + * + * @since 3.5.5 + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + if ( isset( $request['display'] ) ) { + update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); + } + + if ( isset( $request['menu_order'] ) ) { + update_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_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']['name'] ) ) { + wp_update_post( + array( + 'ID' => $image_id, + 'post_title' => wc_clean( $request['image']['name'] ), + ) + ); + } + } else { + delete_term_meta( $id, 'thumbnail_id' ); + } + } + + return true; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php new file mode 100644 index 0000000..40aea51 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php @@ -0,0 +1,1164 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'product_id' => array( + 'required' => true, + 'description' => __( 'Unique identifier for the product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'review' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Review content.', 'woocommerce' ), + ), + 'reviewer' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Name of the reviewer.', 'woocommerce' ), + ), + 'reviewer_email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Email of the reviewer.', 'woocommerce' ), + ), + ) + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $id = (int) $request['id']; + $review = get_comment( $id ); + + if ( $review && ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a new product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $id = (int) $request['id']; + $review = get_comment( $id ); + + if ( $review && ! wc_rest_check_product_reviews_permissions( 'edit', $review->comment_ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $id = (int) $request['id']; + $review = get_comment( $id ); + + if ( $review && ! wc_rest_check_product_reviews_permissions( 'delete', $review->comment_ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all reviews. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + public function get_items( $request ) { + // Retrieve the list of registered collection query parameters. + $registered = $this->get_collection_params(); + + /* + * This array defines mappings between public API query parameters whose + * values are accepted as-passed, and their internal WP_Query parameter + * name equivalents (some are the same). Only values which are also + * present in $registered will be set. + */ + $parameter_mappings = array( + 'reviewer' => 'author__in', + 'reviewer_email' => 'author_email', + 'reviewer_exclude' => 'author__not_in', + 'exclude' => 'comment__not_in', + 'include' => 'comment__in', + 'offset' => 'offset', + 'order' => 'order', + 'per_page' => 'number', + 'product' => 'post__in', + 'search' => 'search', + 'status' => 'status', + ); + + $prepared_args = array(); + + /* + * For each known parameter which is both registered and present in the request, + * set the parameter's value on the query $prepared_args. + */ + foreach ( $parameter_mappings as $api_param => $wp_param ) { + if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { + $prepared_args[ $wp_param ] = $request[ $api_param ]; + } + } + + // Ensure certain parameter values default to empty strings. + foreach ( array( 'author_email', 'search' ) as $param ) { + if ( ! isset( $prepared_args[ $param ] ) ) { + $prepared_args[ $param ] = ''; + } + } + + if ( isset( $registered['orderby'] ) ) { + $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); + } + + if ( isset( $prepared_args['status'] ) ) { + $prepared_args['status'] = 'approved' === $prepared_args['status'] ? 'approve' : $prepared_args['status']; + } + + $prepared_args['no_found_rows'] = false; + $prepared_args['date_query'] = array(); + + // Set before into date query. Date query must be specified as an array of an array. + if ( isset( $registered['before'], $request['before'] ) ) { + $prepared_args['date_query'][0]['before'] = $request['before']; + } + + // Set after into date query. Date query must be specified as an array of an array. + if ( isset( $registered['after'], $request['after'] ) ) { + $prepared_args['date_query'][0]['after'] = $request['after']; + } + + if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) { + $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); + } + + /** + * Filters arguments, before passing to WP_Comment_Query, when querying reviews via the REST API. + * + * @since 3.5.0 + * @link https://developer.wordpress.org/reference/classes/wp_comment_query/ + * @param array $prepared_args Array of arguments for WP_Comment_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request ); + + // Make sure that returns only reviews. + $prepared_args['type'] = 'review'; + + // Query reviews. + $query = new WP_Comment_Query(); + $query_result = $query->query( $prepared_args ); + $reviews = array(); + + foreach ( $query_result as $review ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { + continue; + } + + $data = $this->prepare_item_for_response( $review, $request ); + $reviews[] = $this->prepare_response_for_collection( $data ); + } + + $total_reviews = (int) $query->found_comments; + $max_pages = (int) $query->max_num_pages; + + if ( $total_reviews < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'], $prepared_args['offset'] ); + + $query = new WP_Comment_Query(); + $prepared_args['count'] = true; + + $total_reviews = $query->query( $prepared_args ); + $max_pages = ceil( $total_reviews / $request['per_page'] ); + } + + $response = rest_ensure_response( $reviews ); + $response->header( 'X-WP-Total', $total_reviews ); + $response->header( 'X-WP-TotalPages', $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + + if ( $request['page'] > 1 ) { + $prev_page = $request['page'] - 1; + + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + + if ( $max_pages > $request['page'] ) { + $next_page = $request['page'] + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_review ) ) { + return $prepared_review; + } + + $prepared_review['comment_type'] = 'review'; + + /* + * Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission(). + */ + if ( empty( $prepared_review['comment_content'] ) ) { + return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Setting remaining values before wp_insert_comment so we can use wp_allow_comment(). + if ( ! isset( $prepared_review['comment_date_gmt'] ) ) { + $prepared_review['comment_date_gmt'] = current_time( 'mysql', true ); + } + + if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok. + $prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok. + } else { + $prepared_review['comment_author_IP'] = '127.0.0.1'; + } + + if ( ! empty( $request['author_user_agent'] ) ) { + $prepared_review['comment_agent'] = $request['author_user_agent']; + } elseif ( $request->get_header( 'user_agent' ) ) { + $prepared_review['comment_agent'] = $request->get_header( 'user_agent' ); + } else { + $prepared_review['comment_agent'] = ''; + } + + $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_review ); + if ( is_wp_error( $check_comment_lengths ) ) { + $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); + return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $prepared_review['comment_parent'] = 0; + $prepared_review['comment_author_url'] = ''; + $prepared_review['comment_approved'] = wp_allow_comment( $prepared_review, true ); + + if ( is_wp_error( $prepared_review['comment_approved'] ) ) { + $error_code = $prepared_review['comment_approved']->get_error_code(); + $error_message = $prepared_review['comment_approved']->get_error_message(); + + if ( 'comment_duplicate' === $error_code ) { + return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 409 ) ); + } + + if ( 'comment_flood' === $error_code ) { + return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 400 ) ); + } + + return $prepared_review['comment_approved']; + } + + /** + * Filters a review before it is inserted via the REST API. + * + * Allows modification of the review right before it is inserted via wp_insert_comment(). + * Returning a WP_Error value from the filter will shortcircuit insertion and allow + * skipping further processing. + * + * @since 3.5.0 + * @param array|WP_Error $prepared_review The prepared review data for wp_insert_comment(). + * @param WP_REST_Request $request Request used to insert the review. + */ + $prepared_review = apply_filters( 'woocommerce_rest_pre_insert_product_review', $prepared_review, $request ); + if ( is_wp_error( $prepared_review ) ) { + return $prepared_review; + } + + $review_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_review ) ) ); + + if ( ! $review_id ) { + return new WP_Error( 'woocommerce_rest_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( isset( $request['status'] ) ) { + $this->handle_status_param( $request['status'], $review_id ); + } + + update_comment_meta( $review_id, 'rating', ! empty( $request['rating'] ) ? $request['rating'] : '0' ); + + $review = get_comment( $review_id ); + + /** + * Fires after a comment is created or updated via the REST API. + * + * @param WP_Comment $review Inserted or updated comment object. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating a comment, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_review', $review, $request, true ); + + $fields_update = $this->update_additional_fields_for_object( $review, $request ); + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view'; + $request->set_param( 'context', $context ); + + $response = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $response ); + + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) ); + + return $response; + } + + /** + * Get a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $data = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $data ); + + return $response; + } + + /** + * Updates a review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. + */ + public function update_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $id = (int) $review->comment_ID; + + if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) { + return new WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_args = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_args ) ) { + return $prepared_args; + } + + if ( ! empty( $prepared_args['comment_post_ID'] ) ) { + if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + } + + if ( empty( $prepared_args ) && isset( $request['status'] ) ) { + // Only the comment status is being changed. + $change = $this->handle_status_param( $request['status'], $id ); + + if ( ! $change ) { + return new WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + } elseif ( ! empty( $prepared_args ) ) { + if ( is_wp_error( $prepared_args ) ) { + return $prepared_args; + } + + if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) { + return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $prepared_args['comment_ID'] = $id; + + $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args ); + if ( is_wp_error( $check_comment_lengths ) ) { + $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); + return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $updated = wp_update_comment( wp_slash( (array) $prepared_args ) ); + + if ( false === $updated ) { + return new WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( isset( $request['status'] ) ) { + $this->handle_status_param( $request['status'], $id ); + } + } + + if ( ! empty( $request['rating'] ) ) { + update_comment_meta( $id, 'rating', $request['rating'] ); + } + + $review = get_comment( $id ); + + /** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */ + do_action( 'woocommerce_rest_insert_product_review', $review, $request, false ); + + $fields_update = $this->update_additional_fields_for_object( $review, $request ); + + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $request->set_param( 'context', 'edit' ); + + $response = $this->prepare_item_for_response( $review, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Deletes a review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. + */ + public function delete_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + /** + * Filters whether a review can be trashed. + * + * Return false to disable trash support for the post. + * + * @since 3.5.0 + * @param bool $supports_trash Whether the post type support trashing. + * @param WP_Comment $review The review object being considered for trashing support. + */ + $supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review ); + + $request->set_param( 'context', 'edit' ); + + if ( $force ) { + $previous = $this->prepare_item_for_response( $review, $request ); + $result = wp_delete_comment( $review->comment_ID, true ); + $response = new WP_REST_Response(); + $response->set_data( + array( + 'deleted' => true, + 'previous' => $previous->get_data(), + ) + ); + } else { + // If this type doesn't support trashing, error out. + if ( ! $supports_trash ) { + /* translators: %s: force=true */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( "The object does not support trashing. Set '%s' to delete.", 'woocommerce' ), 'force=true' ), array( 'status' => 501 ) ); + } + + if ( 'trash' === $review->comment_approved ) { + return new WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); + } + + $result = wp_trash_comment( $review->comment_ID ); + $review = get_comment( $review->comment_ID ); + $response = $this->prepare_item_for_response( $review, $request ); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The object cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a review is deleted via the REST API. + * + * @param WP_Comment $review The deleted review data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_review', $review, $response, $request ); + + return $response; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( in_array( 'id', $fields, true ) ) { + $data['id'] = (int) $review->comment_ID; + } + if ( in_array( 'date_created', $fields, true ) ) { + $data['date_created'] = wc_rest_prepare_date_response( $review->comment_date ); + } + if ( in_array( 'date_created_gmt', $fields, true ) ) { + $data['date_created_gmt'] = wc_rest_prepare_date_response( $review->comment_date_gmt ); + } + if ( in_array( 'product_id', $fields, true ) ) { + $data['product_id'] = (int) $review->comment_post_ID; + } + if ( in_array( 'status', $fields, true ) ) { + $data['status'] = $this->prepare_status_response( (string) $review->comment_approved ); + } + if ( in_array( 'reviewer', $fields, true ) ) { + $data['reviewer'] = $review->comment_author; + } + if ( in_array( 'reviewer_email', $fields, true ) ) { + $data['reviewer_email'] = $review->comment_author_email; + } + if ( in_array( 'review', $fields, true ) ) { + $data['review'] = 'view' === $context ? wpautop( $review->comment_content ) : $review->comment_content; + } + if ( in_array( 'rating', $fields, true ) ) { + $data['rating'] = (int) get_comment_meta( $review->comment_ID, 'rating', true ); + } + if ( in_array( 'verified', $fields, true ) ) { + $data['verified'] = wc_review_is_from_verified_owner( $review->comment_ID ); + } + if ( in_array( 'reviewer_avatar_urls', $fields, true ) ) { + $data['reviewer_avatar_urls'] = rest_get_avatar_urls( $review->comment_author_email ); + } + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + /** + * Prepare a single product review to be inserted into the database. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error $prepared_review + */ + protected function prepare_item_for_database( $request ) { + if ( isset( $request['id'] ) ) { + $prepared_review['comment_ID'] = (int) $request['id']; + } + + if ( isset( $request['review'] ) ) { + $prepared_review['comment_content'] = $request['review']; + } + + if ( isset( $request['product_id'] ) ) { + $prepared_review['comment_post_ID'] = (int) $request['product_id']; + } + + if ( isset( $request['reviewer'] ) ) { + $prepared_review['comment_author'] = $request['reviewer']; + } + + if ( isset( $request['reviewer_email'] ) ) { + $prepared_review['comment_author_email'] = $request['reviewer_email']; + } + + if ( ! empty( $request['date_created'] ) ) { + $date_data = rest_get_date_with_gmt( $request['date_created'] ); + + if ( ! empty( $date_data ) ) { + list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; + } + } elseif ( ! empty( $request['date_created_gmt'] ) ) { + $date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true ); + + if ( ! empty( $date_data ) ) { + list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; + } + } + + /** + * Filters a review after it is prepared for the database. + * + * Allows modification of the review right after it is prepared for the database. + * + * @since 3.5.0 + * @param array $prepared_review The prepared review data for `wp_insert_comment`. + * @param WP_REST_Request $request The current request. + */ + return apply_filters( 'woocommerce_rest_preprocess_product_review', $prepared_review, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $review Product review object. + * @return array Links for the given product review. + */ + protected function prepare_links( $review ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $review->comment_post_ID ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ), + ); + } + + if ( 0 !== (int) $review->user_id ) { + $links['reviewer'] = array( + 'href' => rest_url( 'wp/v2/users/' . $review->user_id ), + 'embeddable' => true, + ); + } + + return $links; + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Status of the review.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'approved', + 'enum' => array( 'approved', 'hold', 'spam', 'unspam', 'trash', 'untrash' ), + 'context' => array( 'view', 'edit' ), + ), + 'reviewer' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reviewer_email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + if ( get_option( 'show_avatars' ) ) { + $avatar_properties = array(); + $avatar_sizes = rest_get_avatar_sizes(); + + foreach ( $avatar_sizes as $size ) { + $avatar_properties[ $size ] = array( + /* translators: %d: avatar image size in pixels */ + 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'woocommerce' ), $size ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'embed', 'view', 'edit' ), + ); + } + $schema['properties']['reviewer_avatar_urls'] = array( + 'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $avatar_properties, + ); + } + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + ); + $params['before'] = array( + 'description' => __( 'Limit response to reviews published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( + 'asc', + 'desc', + ), + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date_gmt', + 'enum' => array( + 'date', + 'date_gmt', + 'id', + 'include', + 'product', + ), + ); + $params['reviewer'] = array( + 'description' => __( 'Limit result set to reviews assigned to specific user IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['reviewer_exclude'] = array( + 'description' => __( 'Ensure result set excludes reviews assigned to specific user IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['reviewer_email'] = array( + 'default' => null, + 'description' => __( 'Limit result set to that from a specific author email.', 'woocommerce' ), + 'format' => 'email', + 'type' => 'string', + ); + $params['product'] = array( + 'default' => array(), + 'description' => __( 'Limit result set to reviews assigned to specific product IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['status'] = array( + 'default' => 'approved', + 'description' => __( 'Limit result set to reviews assigned a specific status.', 'woocommerce' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'enum' => array( + 'all', + 'hold', + 'approved', + 'spam', + 'trash', + ), + ); + + /** + * Filter collection parameters for the reviews controller. + * + * This filter registers the collection parameter, but does not map the + * collection parameter to an internal WP_Comment_Query parameter. Use the + * `wc_rest_review_query` filter to set WP_Comment_Query parameters. + * + * @since 3.5.0 + * @param array $params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'woocommerce_rest_product_review_collection_params', $params ); + } + + /** + * Get the reivew, if the ID is valid. + * + * @since 3.5.0 + * @param int $id Supplied ID. + * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise. + */ + protected function get_review( $id ) { + $id = (int) $id; + $error = new WP_Error( 'woocommerce_rest_review_invalid_id', __( 'Invalid review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + + if ( 0 >= $id ) { + return $error; + } + + $review = get_comment( $id ); + if ( empty( $review ) ) { + return $error; + } + + if ( ! empty( $review->comment_post_ID ) ) { + $post = get_post( (int) $review->comment_post_ID ); + + if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + } + + return $review; + } + + /** + * Prepends internal property prefix to query parameters to match our response fields. + * + * @since 3.5.0 + * @param string $query_param Query parameter. + * @return string + */ + protected function normalize_query_param( $query_param ) { + $prefix = 'comment_'; + + switch ( $query_param ) { + case 'id': + $normalized = $prefix . 'ID'; + break; + case 'product': + $normalized = $prefix . 'post_ID'; + break; + case 'include': + $normalized = 'comment__in'; + break; + default: + $normalized = $prefix . $query_param; + break; + } + + return $normalized; + } + + /** + * Checks comment_approved to set comment status for single comment output. + * + * @since 3.5.0 + * @param string|int $comment_approved comment status. + * @return string Comment status. + */ + protected function prepare_status_response( $comment_approved ) { + switch ( $comment_approved ) { + case 'hold': + case '0': + $status = 'hold'; + break; + case 'approve': + case '1': + $status = 'approved'; + break; + case 'spam': + case 'trash': + default: + $status = $comment_approved; + break; + } + + return $status; + } + + /** + * Sets the comment_status of a given review object when creating or updating a review. + * + * @since 3.5.0 + * @param string|int $new_status New review status. + * @param int $id Review ID. + * @return bool Whether the status was changed. + */ + protected function handle_status_param( $new_status, $id ) { + $old_status = wp_get_comment_status( $id ); + + if ( $new_status === $old_status ) { + return false; + } + + switch ( $new_status ) { + case 'approved': + case 'approve': + case '1': + $changed = wp_set_comment_status( $id, 'approve' ); + break; + case 'hold': + case '0': + $changed = wp_set_comment_status( $id, 'hold' ); + break; + case 'spam': + $changed = wp_spam_comment( $id ); + break; + case 'unspam': + $changed = wp_unspam_comment( $id ); + break; + case 'trash': + $changed = wp_trash_comment( $id ); + break; + case 'untrash': + $changed = wp_untrash_comment( $id ); + break; + default: + $changed = false; + break; + } + + return $changed; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php new file mode 100644 index 0000000..5d3a31b --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php @@ -0,0 +1,27 @@ +/variations endpoints. + * + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API variations controller class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Product_Variations_V2_Controller + */ +class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Prepare a single variation output for response. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = array( + 'id' => $object->get_id(), + 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ), + 'description' => wc_format_content( $object->get_description() ), + 'permalink' => $object->get_permalink(), + 'sku' => $object->get_sku(), + 'price' => $object->get_price(), + 'regular_price' => $object->get_regular_price(), + 'sale_price' => $object->get_sale_price(), + 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ), + 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ), + 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ), + 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ), + 'on_sale' => $object->is_on_sale(), + 'status' => $object->get_status(), + 'purchasable' => $object->is_purchasable(), + 'virtual' => $object->is_virtual(), + 'downloadable' => $object->is_downloadable(), + 'downloads' => $this->get_downloads( $object ), + 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, + 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, + 'tax_status' => $object->get_tax_status(), + 'tax_class' => $object->get_tax_class(), + 'manage_stock' => $object->managing_stock(), + 'stock_quantity' => $object->get_stock_quantity(), + 'stock_status' => $object->get_stock_status(), + 'backorders' => $object->get_backorders(), + 'backorders_allowed' => $object->backorders_allowed(), + 'backordered' => $object->is_on_backorder(), + 'low_stock_amount' => '' === $object->get_low_stock_amount() ? null : $object->get_low_stock_amount(), + 'weight' => $object->get_weight(), + 'dimensions' => array( + 'length' => $object->get_length(), + 'width' => $object->get_width(), + 'height' => $object->get_height(), + ), + 'shipping_class' => $object->get_shipping_class(), + 'shipping_class_id' => $object->get_shipping_class_id(), + 'image' => $this->get_image( $object ), + 'attributes' => $this->get_attributes( $object ), + 'menu_order' => $object->get_menu_order(), + 'meta_data' => $object->get_meta_data(), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare a single variation for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + if ( isset( $request['id'] ) ) { + $variation = wc_get_product( absint( $request['id'] ) ); + } else { + $variation = new WC_Product_Variation(); + } + + $variation->set_parent_id( absint( $request['product_id'] ) ); + + // Status. + if ( isset( $request['status'] ) ) { + $variation->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // SKU. + if ( isset( $request['sku'] ) ) { + $variation->set_sku( wc_clean( $request['sku'] ) ); + } + + // Thumbnail. + if ( isset( $request['image'] ) ) { + if ( is_array( $request['image'] ) ) { + $variation = $this->set_variation_image( $variation, $request['image'] ); + } else { + $variation->set_image_id( '' ); + } + } + + // Virtual variation. + if ( isset( $request['virtual'] ) ) { + $variation->set_virtual( $request['virtual'] ); + } + + // Downloadable variation. + if ( isset( $request['downloadable'] ) ) { + $variation->set_downloadable( $request['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $variation->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $variation->set_download_expiry( $request['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $request ); + + // Stock handling. + if ( isset( $request['manage_stock'] ) ) { + $variation->set_manage_stock( $request['manage_stock'] ); + } + + if ( isset( $request['stock_status'] ) ) { + $variation->set_stock_status( $request['stock_status'] ); + } + + if ( isset( $request['backorders'] ) ) { + $variation->set_backorders( $request['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $request['stock_quantity'] ) ) { + $variation->set_stock_quantity( $request['stock_quantity'] ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + // isset() returns false for value null, thus we need to check whether the value has been sent by the request. + if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) { + if ( null === $request['low_stock_amount'] ) { + $variation->set_low_stock_amount( '' ); + } else { + $variation->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) ); + } + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + $variation->set_low_stock_amount( '' ); + } + + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $variation->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $variation->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + + // Tax class. + if ( isset( $request['tax_class'] ) ) { + $variation->set_tax_class( $request['tax_class'] ); + } + + // Description. + if ( isset( $request['description'] ) ) { + $variation->set_description( wp_kses_post( $request['description'] ) ); + } + + // Update taxonomies. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + $parent = wc_get_product( $variation->get_parent_id() ); + + if ( ! $parent ) { + return new WP_Error( + // Translators: %d parent ID. + "woocommerce_rest_{$this->post_type}_invalid_parent", + __( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ), + array( 'status' => 404 ) + ); + } + + $parent_attributes = $parent->get_attributes(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + // Menu order. + if ( $request['menu_order'] ) { + $variation->set_menu_order( $request['menu_order'] ); + } + + // Meta data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $variation Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); + } + + /** + * Get the image for a product variation. + * + * @param WC_Product_Variation $variation Variation data. + * @return array + */ + protected function get_image( $variation ) { + if ( ! $variation->get_image_id() ) { + return; + } + + $attachment_id = $variation->get_image_id(); + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + return; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + return; + } + + if ( ! isset( $image ) ) { + return array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + ); + } + } + + /** + * Set variation image. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product_Variation $variation Variation instance. + * @param array $image Image data. + * @return WC_Product_Variation + */ + protected function set_variation_image( $variation, $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id ) { + if ( isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) { + throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 ); + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() ); + } else { + $variation->set_image_id( '' ); + return $variation; + } + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: attachment ID */ + throw new WC_REST_Exception( 'woocommerce_variation_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $variation->set_image_id( $attachment_id ); + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + + return $variation; + } + + /** + * Get the Variation's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Variation description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Variation URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current variation price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Variation regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Variation sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'on_sale' => array( + 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Variation status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_keys( get_post_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'purchasable' => array( + 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the variation is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at variation level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'stock_status' => array( + 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'instock', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'low_stock_amount' => array( + 'description' => __( 'Low Stock amount for the variation.', 'woocommerce' ), + 'type' => array( 'integer', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Variation dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'image' => array( + 'description' => __( 'Variation image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product based on stock_status. + if ( ! empty( $request['stock_status'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => $request['stock_status'], + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + $args['post_parent'] = $request['product_id']; + + return $args; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + unset( + $params['in_stock'], + $params['type'], + $params['featured'], + $params['category'], + $params['tag'], + $params['shipping_class'], + $params['attribute'], + $params['attribute_term'] + ); + + $params['stock_status'] = array( + 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php new file mode 100644 index 0000000..00966ce --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -0,0 +1,1390 @@ +get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + ); + } + + return $images; + } + + /** + * Make extra product orderby features supported by WooCommerce available to the WC API. + * This includes 'price', 'popularity', and 'rating'. + * + * @param WP_REST_Request $request Request data. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + // Build tax_query if taxonomies are set. + if ( ! empty( $tax_query ) ) { + if ( ! empty( $args['tax_query'] ) ) { + $args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok. + } else { + $args['tax_query'] = $tax_query; // WPCS: slow query ok. + } + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product by stock_status. + if ( ! empty( $request['stock_status'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => $request['stock_status'], + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + $orderby = $request->get_param( 'orderby' ); + $order = $request->get_param( 'order' ); + + $ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order ); + $args['orderby'] = $ordering_args['orderby']; + $args['order'] = $ordering_args['order']; + if ( $ordering_args['meta_key'] ) { + $args['meta_key'] = $ordering_args['meta_key']; // WPCS: slow query ok. + } + + return $args; + } + + /** + * Set product images. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : array(); + + if ( ! empty( $images ) ) { + $gallery = array(); + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: image ID */ + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $featured_image = $product->get_image_id(); + + if ( 0 === $index ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + } + + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + if ( 'variation' === $product->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + } + + // Product parent ID. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status; stock_status has priority over in_stock. + if ( isset( $request['stock_status'] ) ) { + $stock_status = $request['stock_status']; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + + // Low stock amount. + // isset() returns false for value null, thus we need to check whether the value has been sent by the request. + if ( array_key_exists( 'low_stock_amount', $request->get_params() ) ) { + if ( null === $request['low_stock_amount'] ) { + $product->set_low_stock_amount( '' ); + } else { + $product->set_low_stock_amount( wc_stock_amount( $request['low_stock_amount'] ) ); + } + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + $product->set_low_stock_amount( '' ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $new_tags = array(); + + foreach ( $request['tags'] as $tag ) { + if ( ! isset( $tag['name'] ) ) { + $new_tags[] = $tag; + continue; + } + + if ( ! term_exists( $tag['name'], 'product_tag' ) ) { + // Create the tag if it doesn't exist. + $term = wp_insert_term( $tag['name'], 'product_tag' ); + + if ( ! is_wp_error( $term ) ) { + $new_tags[] = array( + 'id' => $term['term_id'], + ); + + continue; + } + } else { + // Tag exists, assume user wants to set the product with this tag. + $new_tags[] = array( + 'id' => get_term_by( 'name', $tag['name'], 'product_tag' )->term_id, + ); + } + } + + $product = $this->save_taxonomy_terms( $product, $new_tags, 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + // Set children for a grouped product. + if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { + $product->set_children( $request['grouped_products'] ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Allow set meta_data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + if ( ! empty( $request['date_created'] ) ) { + $date = rest_parse_date( $request['date_created'] ); + + if ( $date ) { + $product->set_date_created( $date ); + } + } + + if ( ! empty( $request['date_created_gmt'] ) ) { + $date = rest_parse_date( $request['date_created_gmt'], true ); + + if ( $date ) { + $product->set_date_created( $date ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'stock_status' => array( + 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'instock', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'low_stock_amount' => array( + 'description' => __( 'Low Stock amount for the product.', 'woocommerce' ), + 'type' => array( 'integer', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + 'readonly' => true, + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Add new options for 'orderby' to the collection params. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) ); + + unset( $params['in_stock'] ); + $params['stock_status'] = array( + 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @param string $context Request context. Options: 'view' and 'edit'. + * + * @return array + */ + protected function get_product_data( $product, $context = 'view' ) { + $data = parent::get_product_data( ...func_get_args() ); + // Add stock_status if needed. + if ( isset( $this->request ) ) { + $fields = $this->get_fields_for_response( $this->request ); + if ( in_array( 'stock_status', $fields ) ) { + $data['stock_status'] = $product->get_stock_status( $context ); + } + } + return $data; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php new file mode 100644 index 0000000..10525ef --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php @@ -0,0 +1,143 @@ + $name ) { + $results = $wpdb->get_results( + $wpdb->prepare( " + SELECT count(meta_id) AS total + FROM $wpdb->postmeta + WHERE meta_key = 'discount_type' + AND meta_value = %s + ", $slug ) + ); + + $total = isset( $results[0] ) ? (int) $results[0]->total : 0; + + $data[] = array( + 'slug' => $slug, + 'name' => $name, + 'total' => $total, + ); + } + + set_transient( 'rest_api_coupons_type_count', $data, YEAR_IN_SECONDS ); + + return $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, + 'name' => $report->name, + 'total' => $report->total, + ); + + $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 ); + + /** + * 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_coupons_count', $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_coupon_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Coupon type name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of coupons.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php new file mode 100644 index 0000000..5e5e679 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php @@ -0,0 +1,154 @@ + $total ) { + if ( in_array( $role, array( 'administrator', 'shop_manager' ), true ) ) { + continue; + } + + $total_customers += (int) $total; + } + + $customers_query = new WP_User_Query( + array( + 'role__not_in' => array( 'administrator', 'shop_manager' ), + 'number' => 0, + 'fields' => 'ID', + 'count_total' => true, + 'meta_query' => array( // WPCS: slow query ok. + array( + 'key' => 'paying_customer', + 'value' => 1, + 'compare' => '=', + ), + ), + ) + ); + + $total_paying = (int) $customers_query->get_total(); + + $data = array( + array( + 'slug' => 'paying', + 'name' => __( 'Paying customer', 'woocommerce' ), + 'total' => $total_paying, + ), + array( + 'slug' => 'non_paying', + 'name' => __( 'Non-paying customer', 'woocommerce' ), + 'total' => $total_customers - $total_paying, + ), + ); + + return $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, + 'name' => $report->name, + 'total' => $report->total, + ); + + $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 ); + + /** + * 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_customers_count', $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_customer_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Customer type name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of customers.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php new file mode 100644 index 0000000..77c74f6 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php @@ -0,0 +1,127 @@ + $name ) { + if ( ! isset( $totals->$slug ) ) { + continue; + } + + $data[] = array( + 'slug' => str_replace( 'wc-', '', $slug ), + 'name' => $name, + 'total' => (int) $totals->$slug, + ); + } + + return $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, + 'name' => $report->name, + 'total' => $report->total, + ); + + $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 ); + + /** + * 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_orders_count', $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_order_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Order status name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of orders.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php new file mode 100644 index 0000000..95fb0e8 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php @@ -0,0 +1,133 @@ + 'product_type', + 'hide_empty' => false, + ) + ); + $data = array(); + + foreach ( $terms as $product_type ) { + if ( ! isset( $types[ $product_type->name ] ) ) { + continue; + } + + $data[] = array( + 'slug' => $product_type->name, + 'name' => $types[ $product_type->name ], + 'total' => (int) $product_type->count, + ); + } + + return $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, + 'name' => $report->name, + 'total' => $report->total, + ); + + $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 ); + + /** + * 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_products_count', $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_product_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product type name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php new file mode 100644 index 0000000..324a4aa --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php @@ -0,0 +1,132 @@ + true, + 'post_type' => 'product', + 'meta_key' => 'rating', // WPCS: slow query ok. + 'meta_value' => '', // WPCS: slow query ok. + ); + + for ( $i = 1; $i <= 5; $i++ ) { + $query_data['meta_value'] = $i; + + $data[] = array( + 'slug' => 'rated_' . $i . '_out_of_5', + /* translators: %s: average rating */ + 'name' => sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $i ), + 'total' => (int) get_comments( $query_data ), + ); + } + + return $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, + 'name' => $report->name, + 'total' => $report->total, + ); + + $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 ); + + /** + * 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_reviews_count', $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_review_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Review type name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of reviews.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php new file mode 100644 index 0000000..28fd0e9 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php @@ -0,0 +1,27 @@ + 'orders/totals', + 'description' => __( 'Orders totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'products/totals', + 'description' => __( 'Products totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'customers/totals', + 'description' => __( 'Customers totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'coupons/totals', + 'description' => __( 'Coupons totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'reviews/totals', + 'description' => __( 'Reviews totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'categories/totals', + 'description' => __( 'Categories totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'tags/totals', + 'description' => __( 'Tags totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'attributes/totals', + 'description' => __( 'Attributes totals.', 'woocommerce' ), + ); + + return $reports; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php new file mode 100644 index 0000000..0a2bb55 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php @@ -0,0 +1,250 @@ + 404 ) ); + } + + $settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + + 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(); + } elseif ( 'single_select_page' === $setting['type'] ) { + $pages = get_pages( + array( + 'sort_column' => 'menu_order', + 'sort_order' => 'ASC', + 'hierarchical' => 0, + ) + ); + $options = array(); + foreach ( $pages as $page ) { + $options[ $page->ID ] = ! empty( $page->post_title ) ? $page->post_title : '#' . $page->ID; + } + $setting['type'] = 'select'; + $setting['options'] = $options; + } + + $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 ) { + $states = WC()->countries->get_states( $key ); + + if ( $states ) { + foreach ( $states as $state_key => $state_value ) { + $output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value; + } + } else { + $output[ $key ] = $value; + } + } + return $output; + } + + /** + * Get the settings schema, conforming to JSON Schema. + * + * @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, + ), + 'group_id' => array( + 'description' => __( 'An identifier for the group this setting belongs to.', '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/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php new file mode 100644 index 0000000..ebc195b --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php @@ -0,0 +1,112 @@ +namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Makes sure the current user has access to WRITE the settings APIs. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + 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; + } + + /** + * Update a setting. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $options_controller = new WC_REST_Setting_Options_Controller(); + $response = $options_controller->update_item( $request ); + + return $response; + } + + /** + * 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', 'edit' ), + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'ID of parent grouping.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sub_groups' => array( + 'description' => __( 'IDs for settings sub groups.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php new file mode 100644 index 0000000..6baff7d --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php @@ -0,0 +1,27 @@ +/locations endpoint. + * + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Locations class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Shipping_Zone_Locations_V2_Controller + */ +class WC_REST_Shipping_Zone_Locations_Controller extends WC_REST_Shipping_Zone_Locations_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php new file mode 100644 index 0000000..f03234e --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php @@ -0,0 +1,43 @@ +/methods endpoint. + * + * @package WooCommerce\RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Methods class. + * + * @package WooCommerce\RestApi + * @extends WC_REST_Shipping_Zone_Methods_V2_Controller + */ +class WC_REST_Shipping_Zone_Methods_Controller extends WC_REST_Shipping_Zone_Methods_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Get the settings schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + // Get parent schema to append additional supported settings types for shipping zone method. + $schema = parent::get_item_schema(); + + // Append additional settings supported types (class, order). + $schema['properties']['settings']['properties']['type']['enum'][] = 'class'; + $schema['properties']['settings']['properties']['type']['enum'][] = 'order'; + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php new file mode 100644 index 0000000..ffd5dc4 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php @@ -0,0 +1,125 @@ + 404 ) ); + } + + return $zone; + } + + /** + * Check whether a given request has permission to read Shipping Zones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_shipping_enabled() ) { + return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + 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 to create Shipping Zones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_shipping_enabled() ) { + return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + 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 whether a given request has permission to edit Shipping Zones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_items_permissions_check( $request ) { + if ( ! wc_shipping_enabled() ) { + return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + 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 whether a given request has permission to delete Shipping Zones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_items_permissions_check( $request ) { + if ( ! wc_shipping_enabled() ) { + return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php new file mode 100644 index 0000000..dafef18 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php @@ -0,0 +1,27 @@ +get_results( + $wpdb->prepare( + " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", + $tax->tax_rate_id + ) + ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + if ( 'postcode' === $locale->location_type ) { + $data['postcodes'][] = $locale->location_code; + } elseif ( 'city' === $locale->location_type ) { + $data['cities'][] = $locale->location_code; + } + } + } + + return $data; + } + + /** + * Get the taxes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['postcodes'] = array( + 'description' => __( 'List of postcodes / ZIPs. Introduced in WooCommerce 5.3.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['cities'] = array( + 'description' => __( 'List of city names. Introduced in WooCommerce 5.3.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ); + + $schema['properties']['postcode']['description'] = + __( "Postcode/ZIP, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'postcodes' should be used instead.", 'woocommerce' ); + + $schema['properties']['city']['description'] = + __( "City name, it doesn't support multiple values. Deprecated as of WooCommerce 5.3, 'cities' should be used instead.", 'woocommerce' ); + + return $schema; + } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response The response, or an error. + */ + public function create_item( $request ) { + $this->adjust_cities_and_postcodes( $request ); + + return parent::create_item( $request ); + } + + /** + * Update a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response The response, or an error. + */ + public function update_item( $request ) { + $this->adjust_cities_and_postcodes( $request ); + + return parent::update_item( $request ); + } + + /** + * Convert array "cities" and "postcodes" parameters + * into semicolon-separated strings "city" and "postcode". + * + * @param WP_REST_Request $request The request to adjust. + */ + private function adjust_cities_and_postcodes( &$request ) { + if ( isset( $request['cities'] ) ) { + $request['city'] = join( ';', $request['cities'] ); + } + if ( isset( $request['postcodes'] ) ) { + $request['postcode'] = join( ';', $request['postcodes'] ); + } + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php new file mode 100644 index 0000000..40423de --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php @@ -0,0 +1,811 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'Name for the resource.', 'woocommerce' ), + 'required' => true, + ), + ) + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check if a given request has access to read the terms. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'read' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'create' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'read' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'edit' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'delete' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean|WP_Error + */ + public function batch_items_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'batch' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check permissions. + * + * @param WP_REST_Request $request Full details about the request. + * @param string $context Request context. + * @return bool|WP_Error + */ + protected function check_permissions( $request, $context = 'read' ) { + // Get taxonomy. + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy || ! taxonomy_exists( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Taxonomy does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Check permissions for a single term. + $id = intval( $request['id'] ); + if ( $id ) { + $term = get_term( $id, $taxonomy ); + + if ( is_wp_error( $term ) || ! $term || $term->taxonomy !== $taxonomy ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id ); + } + + return wc_rest_check_product_term_permissions( $taxonomy, $context ); + } + + /** + * Get terms associated with a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $prepared_args = array( + 'exclude' => $request['exclude'], + 'include' => $request['include'], + 'order' => $request['order'], + 'orderby' => $request['orderby'], + 'product' => $request['product'], + 'hide_empty' => $request['hide_empty'], + 'number' => $request['per_page'], + 'search' => $request['search'], + 'slug' => $request['slug'], + ); + + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + + if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) { + if ( 0 === $request['parent'] ) { + // Only query top-level terms. + $prepared_args['parent'] = 0; + } else { + if ( $request['parent'] ) { + $prepared_args['parent'] = $request['parent']; + } + } + } + + /** + * Filter the query arguments, before passing them to `get_terms()`. + * + * Enables adding extra arguments or setting defaults for a terms + * collection request. + * + * @see https://developer.wordpress.org/reference/functions/get_terms/ + * + * @param array $prepared_args Array of arguments to be + * passed to get_terms. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request ); + + if ( ! empty( $prepared_args['product'] ) ) { + $query_result = $this->get_terms_for_product( $prepared_args, $request ); + $total_terms = $this->total_terms; + } else { + $query_result = get_terms( $taxonomy, $prepared_args ); + + $count_args = $prepared_args; + unset( $count_args['number'] ); + unset( $count_args['offset'] ); + $total_terms = wp_count_terms( $taxonomy, $count_args ); + + // Ensure we don't return results when offset is out of bounds. + // See https://core.trac.wordpress.org/ticket/35935. + if ( $prepared_args['offset'] && $prepared_args['offset'] >= $total_terms ) { + $query_result = array(); + } + + // wp_count_terms can return a falsy value when the term has no children. + if ( ! $total_terms ) { + $total_terms = 0; + } + } + $response = array(); + foreach ( $query_result as $term ) { + $data = $this->prepare_item_for_response( $term, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $response ); + + // Store pagination values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $response->header( 'X-WP-Total', (int) $total_terms ); + $max_pages = ceil( $total_terms / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = str_replace( '(?P[\d]+)', $request['attribute_id'], $this->rest_base ); + $base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single term for a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $name = $request['name']; + $args = array(); + $schema = $this->get_item_schema(); + + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { + $args['description'] = $request['description']; + } + if ( isset( $request['slug'] ) ) { + $args['slug'] = $request['slug']; + } + if ( isset( $request['parent'] ) ) { + if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); + } + $args['parent'] = $request['parent']; + } + + $term = wp_insert_term( $name, $taxonomy, $args ); + if ( is_wp_error( $term ) ) { + $error_data = array( 'status' => 400 ); + + // If we're going to inform the client that the term exists, + // give them the identifier they can actually use. + $term_id = $term->get_error_data( 'term_exists' ); + if ( $term_id ) { + $error_data['resource_id'] = $term_id; + } + + return new WP_Error( $term->get_error_code(), $term->get_error_message(), $error_data ); + } + + $term = get_term( $term['term_id'], $taxonomy ); + + $this->update_additional_fields_for_object( $term, $request ); + + // Add term data. + $meta_fields = $this->update_term_meta_fields( $term, $request ); + if ( is_wp_error( $meta_fields ) ) { + wp_delete_term( $term->term_id, $taxonomy ); + + return $meta_fields; + } + + /** + * Fires after a single term is created or updated via the REST API. + * + * @param WP_Term $term Inserted Term object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating term, false when updating. + */ + do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + + $base = '/' . $this->namespace . '/' . $this->rest_base; + if ( ! empty( $request['attribute_id'] ) ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + + $response->header( 'Location', rest_url( $base . '/' . $term->term_id ) ); + + return $response; + } + + /** + * Get a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function get_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $term = get_term( (int) $request['id'], $taxonomy ); + + if ( is_wp_error( $term ) ) { + return $term; + } + + $response = $this->prepare_item_for_response( $term, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Update a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $term = get_term( (int) $request['id'], $taxonomy ); + $schema = $this->get_item_schema(); + $prepared_args = array(); + + if ( isset( $request['name'] ) ) { + $prepared_args['name'] = $request['name']; + } + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { + $prepared_args['description'] = $request['description']; + } + if ( isset( $request['slug'] ) ) { + $prepared_args['slug'] = $request['slug']; + } + if ( isset( $request['parent'] ) ) { + if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); + } + $prepared_args['parent'] = $request['parent']; + } + + // Only update the term if we haz something to update. + if ( ! empty( $prepared_args ) ) { + $update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args ); + if ( is_wp_error( $update ) ) { + return $update; + } + } + + $term = get_term( (int) $request['id'], $taxonomy ); + + $this->update_additional_fields_for_object( $term, $request ); + + // Update term data. + $meta_fields = $this->update_term_meta_fields( $term, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * Fires after a single term is created or updated via the REST API. + * + * @param WP_Term $term Inserted Term object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating term, false when updating. + */ + do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + return rest_ensure_response( $response ); + } + + /** + * Delete a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $term = get_term( (int) $request['id'], $taxonomy ); + // Get default category id. + $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); + + // Prevent deleting the default product category. + if ( $default_category_id === (int) $request['id'] ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Default product category cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + + $retval = wp_delete_term( $term->term_id, $term->taxonomy ); + if ( ! $retval ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + // Schedule action to assign default category. + wc_get_container()->get( AssignDefaultCategory::class )->schedule_action(); + + /** + * Fires after a single term is deleted via the REST API. + * + * @param WP_Term $term The deleted term. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param object $term Term object. + * @param WP_REST_Request $request Full details about the request. + * @return array Links for the given term. + */ + protected function prepare_links( $term, $request ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + + if ( ! empty( $request['attribute_id'] ) ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + + if ( $term->parent ) { + $parent_term = get_term( (int) $term->parent, $term->taxonomy ); + if ( $parent_term ) { + $links['up'] = array( + 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ), + ); + } + } + + return $links; + } + + /** + * Update term meta fields. + * + * @param WP_Term $term Term object. + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + return true; + } + + /** + * Get the terms attached to a product. + * + * This is an alternative to `get_terms()` that uses `get_the_terms()` + * instead, which hits the object cache. There are a few things not + * supported, notably `include`, `exclude`. In `self::get_items()` these + * are instead treated as a full query. + * + * @param array $prepared_args Arguments for `get_terms()`. + * @param WP_REST_Request $request Full details about the request. + * @return array List of term objects. (Total count in `$this->total_terms`). + */ + protected function get_terms_for_product( $prepared_args, $request ) { + $taxonomy = $this->get_taxonomy( $request ); + + $query_result = get_the_terms( $prepared_args['product'], $taxonomy ); + if ( empty( $query_result ) ) { + $this->total_terms = 0; + return array(); + } + + // get_items() verifies that we don't have `include` set, and default. + // ordering is by `name`. + if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) { + switch ( $prepared_args['orderby'] ) { + case 'id': + $this->sort_column = 'term_id'; + break; + case 'slug': + case 'term_group': + case 'description': + case 'count': + $this->sort_column = $prepared_args['orderby']; + break; + } + usort( $query_result, array( $this, 'compare_terms' ) ); + } + if ( strtolower( $prepared_args['order'] ) !== 'asc' ) { + $query_result = array_reverse( $query_result ); + } + + // Pagination. + $this->total_terms = count( $query_result ); + $query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] ); + + return $query_result; + } + + /** + * Comparison function for sorting terms by a column. + * + * Uses `$this->sort_column` to determine field to sort by. + * + * @param stdClass $left Term object. + * @param stdClass $right Term object. + * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left. + */ + protected function compare_terms( $left, $right ) { + $col = $this->sort_column; + $left_val = $left->$col; + $right_val = $right->$col; + + if ( is_int( $left_val ) && is_int( $right_val ) ) { + return $left_val - $right_val; + } + + return strcmp( $left_val, $right_val ); + } + + /** + * Get the query params for collections + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items. Applies to hierarchical taxonomies only.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'default' => 'asc', + 'enum' => array( + 'asc', + 'desc', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by resource attribute.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'default' => 'name', + 'enum' => array( + 'id', + 'include', + 'name', + 'slug', + 'term_group', + 'description', + 'count', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['hide_empty'] = array( + 'description' => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['parent'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific parent. Applies to hierarchical taxonomies only.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['slug'] = array( + 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } + + /** + * Get taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function get_taxonomy( $request ) { + $attribute_id = $request['attribute_id']; + + if ( empty( $attribute_id ) ) { + return $this->taxonomy; + } + + if ( isset( $this->taxonomies_by_id[ $attribute_id ] ) ) { + return $this->taxonomies_by_id[ $attribute_id ]; + } + + $taxonomy = WC()->call_function( 'wc_attribute_taxonomy_name_by_id', (int) $request['attribute_id'] ); + if ( ! empty( $taxonomy ) ) { + $this->taxonomy = $taxonomy; + $this->taxonomies_by_id[ $attribute_id ] = $taxonomy; + } + + return $taxonomy; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php new file mode 100644 index 0000000..16c3060 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php @@ -0,0 +1,37 @@ +init() + */ + public static function init() { + wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::instance()->init()', '4.5.0' ); + \Automattic\WooCommerce\RestApi\Server::instance()->init(); + } + + /** + * Return the version of the package. + * + * @deprecated since 4.5.0. This tracks WooCommerce version now. + * @return string + */ + public static function get_version() { + wc_deprecated_function( 'WC()->version', '4.5.0' ); + return WC()->version; + } + + /** + * Return the path to the package. + * + * @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::get_path() + * @return string + */ + public static function get_path() { + wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::get_path()', '4.5.0' ); + return \Automattic\WooCommerce\RestApi\Server::get_path(); + } +} diff --git a/includes/rest-api/Server.php b/includes/rest-api/Server.php new file mode 100644 index 0000000..b8d863f --- /dev/null +++ b/includes/rest-api/Server.php @@ -0,0 +1,202 @@ +get_rest_namespaces() as $namespace => $controllers ) { + foreach ( $controllers as $controller_name => $controller_class ) { + $this->controllers[ $namespace ][ $controller_name ] = new $controller_class(); + $this->controllers[ $namespace ][ $controller_name ]->register_routes(); + } + } + } + + /** + * Get API namespaces - new namespaces should be registered here. + * + * @return array List of Namespaces and Main controller classes. + */ + protected function get_rest_namespaces() { + return apply_filters( + 'woocommerce_rest_api_get_rest_namespaces', + array( + 'wc/v1' => $this->get_v1_controllers(), + 'wc/v2' => $this->get_v2_controllers(), + 'wc/v3' => $this->get_v3_controllers(), + 'wc-telemetry' => $this->get_telemetry_controllers(), + ) + ); + } + + /** + * List of controllers in the wc/v1 namespace. + * + * @return array + */ + protected function get_v1_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_V1_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_V1_Controller', + 'customers' => 'WC_REST_Customers_V1_Controller', + 'order-notes' => 'WC_REST_Order_Notes_V1_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_V1_Controller', + 'orders' => 'WC_REST_Orders_V1_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V1_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_V1_Controller', + 'product-categories' => 'WC_REST_Product_Categories_V1_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_V1_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V1_Controller', + 'product-tags' => 'WC_REST_Product_Tags_V1_Controller', + 'products' => 'WC_REST_Products_V1_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_V1_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V1_Controller', + 'reports' => 'WC_REST_Reports_V1_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_V1_Controller', + 'taxes' => 'WC_REST_Taxes_V1_Controller', + 'webhooks' => 'WC_REST_Webhooks_V1_Controller', + 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V1_Controller', + ); + } + + /** + * List of controllers in the wc/v2 namespace. + * + * @return array + */ + protected function get_v2_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_V2_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_V2_Controller', + 'customers' => 'WC_REST_Customers_V2_Controller', + 'network-orders' => 'WC_REST_Network_Orders_V2_Controller', + 'order-notes' => 'WC_REST_Order_Notes_V2_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_V2_Controller', + 'orders' => 'WC_REST_Orders_V2_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V2_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_V2_Controller', + 'product-categories' => 'WC_REST_Product_Categories_V2_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_V2_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V2_Controller', + 'product-tags' => 'WC_REST_Product_Tags_V2_Controller', + 'products' => 'WC_REST_Products_V2_Controller', + 'product-variations' => 'WC_REST_Product_Variations_V2_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_V2_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V2_Controller', + 'reports' => 'WC_REST_Reports_V2_Controller', + 'settings' => 'WC_REST_Settings_V2_Controller', + 'settings-options' => 'WC_REST_Setting_Options_V2_Controller', + 'shipping-zones' => 'WC_REST_Shipping_Zones_V2_Controller', + 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_V2_Controller', + 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_V2_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_V2_Controller', + 'taxes' => 'WC_REST_Taxes_V2_Controller', + 'webhooks' => 'WC_REST_Webhooks_V2_Controller', + 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V2_Controller', + 'system-status' => 'WC_REST_System_Status_V2_Controller', + 'system-status-tools' => 'WC_REST_System_Status_Tools_V2_Controller', + 'shipping-methods' => 'WC_REST_Shipping_Methods_V2_Controller', + 'payment-gateways' => 'WC_REST_Payment_Gateways_V2_Controller', + ); + } + + /** + * List of controllers in the wc/v3 namespace. + * + * @return array + */ + protected function get_v3_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_Controller', + 'customers' => 'WC_REST_Customers_Controller', + 'network-orders' => 'WC_REST_Network_Orders_Controller', + 'order-notes' => 'WC_REST_Order_Notes_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_Controller', + 'orders' => 'WC_REST_Orders_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_Controller', + 'product-categories' => 'WC_REST_Product_Categories_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_Controller', + 'product-tags' => 'WC_REST_Product_Tags_Controller', + 'products' => 'WC_REST_Products_Controller', + 'product-variations' => 'WC_REST_Product_Variations_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_Controller', + 'reports-orders-totals' => 'WC_REST_Report_Orders_Totals_Controller', + 'reports-products-totals' => 'WC_REST_Report_Products_Totals_Controller', + 'reports-customers-totals' => 'WC_REST_Report_Customers_Totals_Controller', + 'reports-coupons-totals' => 'WC_REST_Report_Coupons_Totals_Controller', + 'reports-reviews-totals' => 'WC_REST_Report_Reviews_Totals_Controller', + 'reports' => 'WC_REST_Reports_Controller', + 'settings' => 'WC_REST_Settings_Controller', + 'settings-options' => 'WC_REST_Setting_Options_Controller', + 'shipping-zones' => 'WC_REST_Shipping_Zones_Controller', + 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_Controller', + 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_Controller', + 'taxes' => 'WC_REST_Taxes_Controller', + 'webhooks' => 'WC_REST_Webhooks_Controller', + 'system-status' => 'WC_REST_System_Status_Controller', + 'system-status-tools' => 'WC_REST_System_Status_Tools_Controller', + 'shipping-methods' => 'WC_REST_Shipping_Methods_Controller', + 'payment-gateways' => 'WC_REST_Payment_Gateways_Controller', + 'data' => 'WC_REST_Data_Controller', + 'data-continents' => 'WC_REST_Data_Continents_Controller', + 'data-countries' => 'WC_REST_Data_Countries_Controller', + 'data-currencies' => 'WC_REST_Data_Currencies_Controller', + ); + } + + /** + * List of controllers in the telemetry namespace. + * + * @return array + */ + protected function get_telemetry_controllers() { + return array( + 'tracker' => 'WC_REST_Telemetry_Controller', + ); + } + + /** + * Return the path to the package. + * + * @return string + */ + public static function get_path() { + return dirname( __DIR__ ); + } +} diff --git a/includes/rest-api/Utilities/ImageAttachment.php b/includes/rest-api/Utilities/ImageAttachment.php new file mode 100644 index 0000000..debecff --- /dev/null +++ b/includes/rest-api/Utilities/ImageAttachment.php @@ -0,0 +1,93 @@ +id = (int) $id; + $this->object_id = (int) $object_id; + } + + /** + * Upload an attachment file. + * + * @throws \WC_REST_Exception REST API exceptions. + * @param string $src URL to file. + */ + public function upload_image_from_src( $src ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $src ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $this->object_id, $images ) ) { + throw new \WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + return; + } + } + + $this->id = wc_rest_set_uploaded_image_as_attachment( $upload, $this->object_id ); + + if ( ! wp_attachment_is_image( $this->id ) ) { + /* translators: %s: image ID */ + throw new \WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $this->id ), 400 ); + } + } + + /** + * Update attachment alt text. + * + * @param string $text Text to set. + */ + public function update_alt_text( $text ) { + if ( ! $this->id ) { + return; + } + update_post_meta( $this->id, '_wp_attachment_image_alt', wc_clean( $text ) ); + } + + /** + * Update attachment name. + * + * @param string $text Text to set. + */ + public function update_name( $text ) { + if ( ! $this->id ) { + return; + } + wp_update_post( + array( + 'ID' => $this->id, + 'post_title' => $text, + ) + ); + } +} diff --git a/includes/rest-api/Utilities/SingletonTrait.php b/includes/rest-api/Utilities/SingletonTrait.php new file mode 100644 index 0000000..b713a6c --- /dev/null +++ b/includes/rest-api/Utilities/SingletonTrait.php @@ -0,0 +1,52 @@ +id = 'flat_rate'; + $this->instance_id = absint( $instance_id ); + $this->method_title = __( 'Flat rate', 'woocommerce' ); + $this->method_description = __( 'Lets you charge a fixed rate for shipping.', 'woocommerce' ); + $this->supports = array( + 'shipping-zones', + 'instance-settings', + 'instance-settings-modal', + ); + $this->init(); + + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + } + + /** + * Init user set variables. + */ + public function init() { + $this->instance_form_fields = include __DIR__ . '/includes/settings-flat-rate.php'; + $this->title = $this->get_option( 'title' ); + $this->tax_status = $this->get_option( 'tax_status' ); + $this->cost = $this->get_option( 'cost' ); + $this->type = $this->get_option( 'type', 'class' ); + } + + /** + * Evaluate a cost from a sum/string. + * + * @param string $sum Sum of shipping. + * @param array $args Args, must contain `cost` and `qty` keys. Having `array()` as default is for back compat reasons. + * @return string + */ + protected function evaluate_cost( $sum, $args = array() ) { + // Add warning for subclasses. + if ( ! is_array( $args ) || ! array_key_exists( 'qty', $args ) || ! array_key_exists( 'cost', $args ) ) { + wc_doing_it_wrong( __FUNCTION__, '$args must contain `cost` and `qty` keys.', '4.0.1' ); + } + + include_once WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php'; + + // Allow 3rd parties to process shipping cost arguments. + $args = apply_filters( 'woocommerce_evaluate_shipping_cost_args', $args, $sum, $this ); + $locale = localeconv(); + $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'], ',' ); + $this->fee_cost = $args['cost']; + + // Expand shortcodes. + add_shortcode( 'fee', array( $this, 'fee' ) ); + + $sum = do_shortcode( + str_replace( + array( + '[qty]', + '[cost]', + ), + array( + $args['qty'], + $args['cost'], + ), + $sum + ) + ); + + remove_shortcode( 'fee', array( $this, 'fee' ) ); + + // Remove whitespace from string. + $sum = preg_replace( '/\s+/', '', $sum ); + + // Remove locale from string. + $sum = str_replace( $decimals, '.', $sum ); + + // Trim invalid start/end characters. + $sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" ); + + // Do the math. + return $sum ? WC_Eval_Math::evaluate( $sum ) : 0; + } + + /** + * Work out fee (shortcode). + * + * @param array $atts Attributes. + * @return string + */ + public function fee( $atts ) { + $atts = shortcode_atts( + array( + 'percent' => '', + 'min_fee' => '', + 'max_fee' => '', + ), + $atts, + 'fee' + ); + + $calculated_fee = 0; + + if ( $atts['percent'] ) { + $calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 ); + } + + if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) { + $calculated_fee = $atts['min_fee']; + } + + if ( $atts['max_fee'] && $calculated_fee > $atts['max_fee'] ) { + $calculated_fee = $atts['max_fee']; + } + + return $calculated_fee; + } + + /** + * Calculate the shipping costs. + * + * @param array $package Package of items from cart. + */ + public function calculate_shipping( $package = array() ) { + $rate = array( + 'id' => $this->get_rate_id(), + 'label' => $this->title, + 'cost' => 0, + 'package' => $package, + ); + + // Calculate the costs. + $has_costs = false; // True when a cost is set. False if all costs are blank strings. + $cost = $this->get_option( 'cost' ); + + if ( '' !== $cost ) { + $has_costs = true; + $rate['cost'] = $this->evaluate_cost( + $cost, + array( + 'qty' => $this->get_package_item_qty( $package ), + 'cost' => $package['contents_cost'], + ) + ); + } + + // Add shipping class costs. + $shipping_classes = WC()->shipping()->get_shipping_classes(); + + if ( ! empty( $shipping_classes ) ) { + $found_shipping_classes = $this->find_shipping_classes( $package ); + $highest_class_cost = 0; + + foreach ( $found_shipping_classes as $shipping_class => $products ) { + // Also handles BW compatibility when slugs were used instead of ids. + $shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' ); + $class_cost_string = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' ); + + if ( '' === $class_cost_string ) { + continue; + } + + $has_costs = true; + $class_cost = $this->evaluate_cost( + $class_cost_string, + array( + 'qty' => array_sum( wp_list_pluck( $products, 'quantity' ) ), + 'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ), + ) + ); + + if ( 'class' === $this->type ) { + $rate['cost'] += $class_cost; + } else { + $highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost; + } + } + + if ( 'order' === $this->type && $highest_class_cost ) { + $rate['cost'] += $highest_class_cost; + } + } + + if ( $has_costs ) { + $this->add_rate( $rate ); + } + + /** + * Developers can add additional flat rates based on this one via this action since @version 2.4. + * + * Previously there were (overly complex) options to add additional rates however this was not user. + * friendly and goes against what Flat Rate Shipping was originally intended for. + */ + do_action( 'woocommerce_' . $this->id . '_shipping_add_rate', $this, $rate ); + } + + /** + * Get items in package. + * + * @param array $package Package of items from cart. + * @return int + */ + public function get_package_item_qty( $package ) { + $total_quantity = 0; + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) { + $total_quantity += $values['quantity']; + } + } + return $total_quantity; + } + + /** + * Finds and returns shipping classes and the products with said class. + * + * @param mixed $package Package of items from cart. + * @return array + */ + public function find_shipping_classes( $package ) { + $found_shipping_classes = array(); + + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['data']->needs_shipping() ) { + $found_class = $values['data']->get_shipping_class(); + + if ( ! isset( $found_shipping_classes[ $found_class ] ) ) { + $found_shipping_classes[ $found_class ] = array(); + } + + $found_shipping_classes[ $found_class ][ $item_id ] = $values; + } + } + + return $found_shipping_classes; + } + + /** + * Sanitize the cost field. + * + * @since 3.4.0 + * @param string $value Unsanitized value. + * @throws Exception Last error triggered. + * @return string + */ + public function sanitize_cost( $value ) { + $value = is_null( $value ) ? '' : $value; + $value = wp_kses_post( trim( wp_unslash( $value ) ) ); + $value = str_replace( array( get_woocommerce_currency_symbol(), html_entity_decode( get_woocommerce_currency_symbol() ) ), '', $value ); + // Thrown an error on the front end if the evaluate_cost will fail. + $dummy_cost = $this->evaluate_cost( + $value, + array( + 'cost' => 1, + 'qty' => 1, + ) + ); + if ( false === $dummy_cost ) { + throw new Exception( WC_Eval_Math::$last_error ); + } + return $value; + } +} diff --git a/includes/shipping/flat-rate/includes/settings-flat-rate.php b/includes/shipping/flat-rate/includes/settings-flat-rate.php new file mode 100644 index 0000000..b4609d6 --- /dev/null +++ b/includes/shipping/flat-rate/includes/settings-flat-rate.php @@ -0,0 +1,89 @@ +10.00 * [qty].', 'woocommerce' ) . '

    ' . __( 'Use [qty] for the number of items,
    [cost] for the total cost of items, and [fee percent="10" min_fee="20" max_fee=""] for percentage based fees.', 'woocommerce' ); + +$settings = array( + 'title' => array( + 'title' => __( 'Method title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'Flat rate', 'woocommerce' ), + 'desc_tip' => true, + ), + 'tax_status' => array( + 'title' => __( 'Tax status', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'default' => 'taxable', + 'options' => array( + 'taxable' => __( 'Taxable', 'woocommerce' ), + 'none' => _x( 'None', 'Tax status', 'woocommerce' ), + ), + ), + 'cost' => array( + 'title' => __( 'Cost', 'woocommerce' ), + 'type' => 'text', + 'placeholder' => '', + 'description' => $cost_desc, + 'default' => '0', + 'desc_tip' => true, + 'sanitize_callback' => array( $this, 'sanitize_cost' ), + ), +); + +$shipping_classes = WC()->shipping()->get_shipping_classes(); + +if ( ! empty( $shipping_classes ) ) { + $settings['class_costs'] = array( + 'title' => __( 'Shipping class costs', 'woocommerce' ), + 'type' => 'title', + 'default' => '', + /* translators: %s: URL for link. */ + 'description' => sprintf( __( 'These costs can optionally be added based on the product shipping class.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=classes' ) ), + ); + foreach ( $shipping_classes as $shipping_class ) { + if ( ! isset( $shipping_class->term_id ) ) { + continue; + } + $settings[ 'class_cost_' . $shipping_class->term_id ] = array( + /* translators: %s: shipping class name */ + 'title' => sprintf( __( '"%s" shipping class cost', 'woocommerce' ), esc_html( $shipping_class->name ) ), + 'type' => 'text', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'description' => $cost_desc, + 'default' => $this->get_option( 'class_cost_' . $shipping_class->slug ), // Before 2.5.0, we used slug here which caused issues with long setting names. + 'desc_tip' => true, + 'sanitize_callback' => array( $this, 'sanitize_cost' ), + ); + } + + $settings['no_class_cost'] = array( + 'title' => __( 'No shipping class cost', 'woocommerce' ), + 'type' => 'text', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'description' => $cost_desc, + 'default' => '', + 'desc_tip' => true, + 'sanitize_callback' => array( $this, 'sanitize_cost' ), + ); + + $settings['type'] = array( + 'title' => __( 'Calculation type', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'default' => 'class', + 'options' => array( + 'class' => __( 'Per class: Charge shipping for each shipping class individually', 'woocommerce' ), + 'order' => __( 'Per order: Charge shipping for the most expensive shipping class', 'woocommerce' ), + ), + ); +} + +return $settings; diff --git a/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php b/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php new file mode 100644 index 0000000..00d813f --- /dev/null +++ b/includes/shipping/free-shipping/class-wc-shipping-free-shipping.php @@ -0,0 +1,245 @@ +id = 'free_shipping'; + $this->instance_id = absint( $instance_id ); + $this->method_title = __( 'Free shipping', 'woocommerce' ); + $this->method_description = __( 'Free shipping is a special method which can be triggered with coupons and minimum spends.', 'woocommerce' ); + $this->supports = array( + 'shipping-zones', + 'instance-settings', + 'instance-settings-modal', + ); + + $this->init(); + } + + /** + * Initialize free shipping. + */ + public function init() { + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->min_amount = $this->get_option( 'min_amount', 0 ); + $this->requires = $this->get_option( 'requires' ); + $this->ignore_discounts = $this->get_option( 'ignore_discounts' ); + + // Actions. + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + add_action( 'admin_footer', array( 'WC_Shipping_Free_Shipping', 'enqueue_admin_js' ), 10 ); // Priority needs to be higher than wc_print_js (25). + } + + /** + * Init form fields. + */ + public function init_form_fields() { + $this->instance_form_fields = array( + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => $this->method_title, + 'desc_tip' => true, + ), + 'requires' => array( + 'title' => __( 'Free shipping requires...', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'default' => '', + 'options' => array( + '' => __( 'N/A', 'woocommerce' ), + 'coupon' => __( 'A valid free shipping coupon', 'woocommerce' ), + 'min_amount' => __( 'A minimum order amount', 'woocommerce' ), + 'either' => __( 'A minimum order amount OR a coupon', 'woocommerce' ), + 'both' => __( 'A minimum order amount AND a coupon', 'woocommerce' ), + ), + ), + 'min_amount' => array( + 'title' => __( 'Minimum order amount', 'woocommerce' ), + 'type' => 'price', + 'placeholder' => wc_format_localized_price( 0 ), + 'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'woocommerce' ), + 'default' => '0', + 'desc_tip' => true, + ), + 'ignore_discounts' => array( + 'title' => __( 'Coupons discounts', 'woocommerce' ), + 'label' => __( 'Apply minimum order rule before coupon discount', 'woocommerce' ), + 'type' => 'checkbox', + 'description' => __( 'If checked, free shipping would be available based on pre-discount order amount.', 'woocommerce' ), + 'default' => 'no', + 'desc_tip' => true, + ), + ); + } + + /** + * Get setting form fields for instances of this shipping method within zones. + * + * @return array + */ + public function get_instance_form_fields() { + return parent::get_instance_form_fields(); + } + + /** + * See if free shipping is available based on the package and cart. + * + * @param array $package Shipping package. + * @return bool + */ + public function is_available( $package ) { + $has_coupon = false; + $has_met_min_amount = false; + + if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) { + $coupons = WC()->cart->get_coupons(); + + if ( $coupons ) { + foreach ( $coupons as $code => $coupon ) { + if ( $coupon->is_valid() && $coupon->get_free_shipping() ) { + $has_coupon = true; + break; + } + } + } + } + + if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) { + $total = WC()->cart->get_displayed_subtotal(); + + if ( WC()->cart->display_prices_including_tax() ) { + $total = $total - WC()->cart->get_discount_tax(); + } + + if ( 'no' === $this->ignore_discounts ) { + $total = $total - WC()->cart->get_discount_total(); + } + + $total = NumberUtil::round( $total, wc_get_price_decimals() ); + + if ( $total >= $this->min_amount ) { + $has_met_min_amount = true; + } + } + + switch ( $this->requires ) { + case 'min_amount': + $is_available = $has_met_min_amount; + break; + case 'coupon': + $is_available = $has_coupon; + break; + case 'both': + $is_available = $has_met_min_amount && $has_coupon; + break; + case 'either': + $is_available = $has_met_min_amount || $has_coupon; + break; + default: + $is_available = true; + break; + } + + return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this ); + } + + /** + * Called to calculate shipping rates for this method. Rates can be added using the add_rate() method. + * + * @uses WC_Shipping_Method::add_rate() + * + * @param array $package Shipping package. + */ + public function calculate_shipping( $package = array() ) { + $this->add_rate( + array( + 'label' => $this->title, + 'cost' => 0, + 'taxes' => false, + 'package' => $package, + ) + ); + } + + /** + * Enqueue JS to handle free shipping options. + * + * Static so that's enqueued only once. + */ + public static function enqueue_admin_js() { + wc_enqueue_js( + "jQuery( function( $ ) { + function wcFreeShippingShowHideMinAmountField( el ) { + var form = $( el ).closest( 'form' ); + var minAmountField = $( '#woocommerce_free_shipping_min_amount', form ).closest( 'tr' ); + var ignoreDiscountField = $( '#woocommerce_free_shipping_ignore_discounts', form ).closest( 'tr' ); + if ( 'coupon' === $( el ).val() || '' === $( el ).val() ) { + minAmountField.hide(); + ignoreDiscountField.hide(); + } else { + minAmountField.show(); + ignoreDiscountField.show(); + } + } + + $( document.body ).on( 'change', '#woocommerce_free_shipping_requires', function() { + wcFreeShippingShowHideMinAmountField( this ); + }); + + // Change while load. + $( '#woocommerce_free_shipping_requires' ).trigger( 'change' ); + $( document.body ).on( 'wc_backbone_modal_loaded', function( evt, target ) { + if ( 'wc-modal-shipping-method-settings' === target ) { + wcFreeShippingShowHideMinAmountField( $( '#wc-backbone-modal-dialog #woocommerce_free_shipping_requires', evt.currentTarget ) ); + } + } ); + });" + ); + } +} diff --git a/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php b/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php new file mode 100644 index 0000000..1f579c4 --- /dev/null +++ b/includes/shipping/legacy-flat-rate/class-wc-shipping-legacy-flat-rate.php @@ -0,0 +1,411 @@ +id = 'legacy_flat_rate'; + $this->method_title = __( 'Flat rate (legacy)', 'woocommerce' ); + /* translators: %s: Admin shipping settings URL */ + $this->method_description = '' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your Shipping zones.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . ''; + $this->init(); + + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + add_action( 'woocommerce_flat_rate_shipping_add_rate', array( $this, 'calculate_extra_shipping' ), 10, 2 ); + } + + /** + * Process and redirect if disabled. + */ + public function process_admin_options() { + parent::process_admin_options(); + + if ( 'no' === $this->settings['enabled'] ) { + wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); + exit; + } + } + + /** + * Return the name of the option in the WP DB. + * + * @since 2.6.0 + * @return string + */ + public function get_option_key() { + return $this->plugin_id . 'flat_rate_settings'; + } + + /** + * Init function. + */ + public function init() { + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->availability = $this->get_option( 'availability' ); + $this->countries = $this->get_option( 'countries' ); + $this->tax_status = $this->get_option( 'tax_status' ); + $this->cost = $this->get_option( 'cost' ); + $this->type = $this->get_option( 'type', 'class' ); + $this->options = $this->get_option( 'options', false ); // @deprecated 2.4.0 + } + + /** + * Initialise Settings Form Fields. + */ + public function init_form_fields() { + $this->form_fields = include __DIR__ . '/includes/settings-flat-rate.php'; + } + + /** + * Evaluate a cost from a sum/string. + * + * @param string $sum Sum to evaluate. + * @param array $args Arguments. + * @return string + */ + protected function evaluate_cost( $sum, $args = array() ) { + include_once WC()->plugin_path() . '/includes/libraries/class-wc-eval-math.php'; + + $locale = localeconv(); + $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); + + $this->fee_cost = $args['cost']; + + // Expand shortcodes. + add_shortcode( 'fee', array( $this, 'fee' ) ); + + $sum = do_shortcode( + str_replace( + array( + '[qty]', + '[cost]', + ), + array( + $args['qty'], + $args['cost'], + ), + $sum + ) + ); + + remove_shortcode( 'fee', array( $this, 'fee' ) ); + + // Remove whitespace from string. + $sum = preg_replace( '/\s+/', '', $sum ); + + // Remove locale from string. + $sum = str_replace( $decimals, '.', $sum ); + + // Trim invalid start/end characters. + $sum = rtrim( ltrim( $sum, "\t\n\r\0\x0B+*/" ), "\t\n\r\0\x0B+-*/" ); + + // Do the math. + return $sum ? WC_Eval_Math::evaluate( $sum ) : 0; + } + + /** + * Work out fee (shortcode). + * + * @param array $atts Shortcode attributes. + * @return string + */ + public function fee( $atts ) { + $atts = shortcode_atts( + array( + 'percent' => '', + 'min_fee' => '', + ), + $atts, + 'fee' + ); + + $calculated_fee = 0; + + if ( $atts['percent'] ) { + $calculated_fee = $this->fee_cost * ( floatval( $atts['percent'] ) / 100 ); + } + + if ( $atts['min_fee'] && $calculated_fee < $atts['min_fee'] ) { + $calculated_fee = $atts['min_fee']; + } + + return $calculated_fee; + } + + /** + * Calculate shipping. + * + * @param array $package (default: array()). + */ + public function calculate_shipping( $package = array() ) { + $rate = array( + 'id' => $this->id, + 'label' => $this->title, + 'cost' => 0, + 'package' => $package, + ); + + // Calculate the costs. + $has_costs = false; // True when a cost is set. False if all costs are blank strings. + $cost = $this->get_option( 'cost' ); + + if ( '' !== $cost ) { + $has_costs = true; + $rate['cost'] = $this->evaluate_cost( + $cost, + array( + 'qty' => $this->get_package_item_qty( $package ), + 'cost' => $package['contents_cost'], + ) + ); + } + + // Add shipping class costs. + $found_shipping_classes = $this->find_shipping_classes( $package ); + $highest_class_cost = 0; + + foreach ( $found_shipping_classes as $shipping_class => $products ) { + // Also handles BW compatibility when slugs were used instead of ids. + $shipping_class_term = get_term_by( 'slug', $shipping_class, 'product_shipping_class' ); + $class_cost_string = $shipping_class_term && $shipping_class_term->term_id ? $this->get_option( 'class_cost_' . $shipping_class_term->term_id, $this->get_option( 'class_cost_' . $shipping_class, '' ) ) : $this->get_option( 'no_class_cost', '' ); + + if ( '' === $class_cost_string ) { + continue; + } + + $has_costs = true; + $class_cost = $this->evaluate_cost( + $class_cost_string, + array( + 'qty' => array_sum( wp_list_pluck( $products, 'quantity' ) ), + 'cost' => array_sum( wp_list_pluck( $products, 'line_total' ) ), + ) + ); + + if ( 'class' === $this->type ) { + $rate['cost'] += $class_cost; + } else { + $highest_class_cost = $class_cost > $highest_class_cost ? $class_cost : $highest_class_cost; + } + } + + if ( 'order' === $this->type && $highest_class_cost ) { + $rate['cost'] += $highest_class_cost; + } + + $rate['package'] = $package; + + // Add the rate. + if ( $has_costs ) { + $this->add_rate( $rate ); + } + + /** + * Developers can add additional flat rates based on this one via this action since @version 2.4. + * + * Previously there were (overly complex) options to add additional rates however this was not user. + * friendly and goes against what Flat Rate Shipping was originally intended for. + * + * This example shows how you can add an extra rate based on this flat rate via custom function: + * + * add_action( 'woocommerce_flat_rate_shipping_add_rate', 'add_another_custom_flat_rate', 10, 2 ); + * + * function add_another_custom_flat_rate( $method, $rate ) { + * $new_rate = $rate; + * $new_rate['id'] .= ':' . 'custom_rate_name'; // Append a custom ID. + * $new_rate['label'] = 'Rushed Shipping'; // Rename to 'Rushed Shipping'. + * $new_rate['cost'] += 2; // Add $2 to the cost. + * + * // Add it to WC. + * $method->add_rate( $new_rate ); + * }. + */ + do_action( 'woocommerce_flat_rate_shipping_add_rate', $this, $rate ); + } + + /** + * Get items in package. + * + * @param array $package Package information. + * @return int + */ + public function get_package_item_qty( $package ) { + $total_quantity = 0; + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) { + $total_quantity += $values['quantity']; + } + } + return $total_quantity; + } + + /** + * Finds and returns shipping classes and the products with said class. + * + * @param mixed $package Package information. + * @return array + */ + public function find_shipping_classes( $package ) { + $found_shipping_classes = array(); + + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['data']->needs_shipping() ) { + $found_class = $values['data']->get_shipping_class(); + + if ( ! isset( $found_shipping_classes[ $found_class ] ) ) { + $found_shipping_classes[ $found_class ] = array(); + } + + $found_shipping_classes[ $found_class ][ $item_id ] = $values; + } + } + + return $found_shipping_classes; + } + + /** + * Adds extra calculated flat rates. + * + * @deprecated 2.4.0 + * + * Additional rates defined like this: + * Option Name | Additional Cost [+- Percents%] | Per Cost Type (order, class, or item). + * + * @param null $method Deprecated. + * @param array $rate Rate information. + */ + public function calculate_extra_shipping( $method, $rate ) { + if ( $this->options ) { + $options = array_filter( (array) explode( "\n", $this->options ) ); + + foreach ( $options as $option ) { + $this_option = array_map( 'trim', explode( WC_DELIMITER, $option ) ); + if ( count( $this_option ) !== 3 ) { + continue; + } + $extra_rate = $rate; + $extra_rate['id'] = $this->id . ':' . urldecode( sanitize_title( $this_option[0] ) ); + $extra_rate['label'] = $this_option[0]; + $extra_cost = $this->get_extra_cost( $this_option[1], $this_option[2], $rate['package'] ); + if ( is_array( $extra_rate['cost'] ) ) { + $extra_rate['cost']['order'] = $extra_rate['cost']['order'] + $extra_cost; + } else { + $extra_rate['cost'] += $extra_cost; + } + + $this->add_rate( $extra_rate ); + } + } + } + + /** + * Calculate the percentage adjustment for each shipping rate. + * + * @deprecated 2.4.0 + * @param float $cost Cost. + * @param float $percent_adjustment Percent adjusment. + * @param string $percent_operator Percent operator. + * @param float $base_price Base price. + * @return float + */ + public function calc_percentage_adjustment( $cost, $percent_adjustment, $percent_operator, $base_price ) { + if ( '+' === $percent_operator ) { + $cost += $percent_adjustment * $base_price; + } else { + $cost -= $percent_adjustment * $base_price; + } + return $cost; + } + + /** + * Get extra cost. + * + * @deprecated 2.4.0 + * @param string $cost_string Cost string. + * @param string $type Type. + * @param array $package Package information. + * @return float + */ + public function get_extra_cost( $cost_string, $type, $package ) { + $cost = $cost_string; + $cost_percent = false; + // @codingStandardsIgnoreStart + $pattern = + '/' . // Start regex. + '(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits. + '\s*' . // Match whitespace. + '(\+|-)' . // Capture the operand. + '\s*' . // Match whitespace. + '(\d+\.?\d*)' . // Capture digits, optionally capture a `.` and more digits. + '\%/'; // Match the percent sign & end regex. + // @codingStandardsIgnoreEnd + if ( preg_match( $pattern, $cost_string, $this_cost_matches ) ) { + $cost_operator = $this_cost_matches[2]; + $cost_percent = $this_cost_matches[3] / 100; + $cost = $this_cost_matches[1]; + } + switch ( $type ) { + case 'class': + $cost = $cost * count( $this->find_shipping_classes( $package ) ); + break; + case 'item': + $cost = $cost * $this->get_package_item_qty( $package ); + break; + } + if ( $cost_percent ) { + switch ( $type ) { + case 'class': + $shipping_classes = $this->find_shipping_classes( $package ); + foreach ( $shipping_classes as $shipping_class => $items ) { + foreach ( $items as $item_id => $values ) { + $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] ); + } + } + break; + case 'item': + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['data']->needs_shipping() ) { + $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $values['line_total'] ); + } + } + break; + case 'order': + $cost = $this->calc_percentage_adjustment( $cost, $cost_percent, $cost_operator, $package['contents_cost'] ); + break; + } + } + return $cost; + } +} diff --git a/includes/shipping/legacy-flat-rate/includes/settings-flat-rate.php b/includes/shipping/legacy-flat-rate/includes/settings-flat-rate.php new file mode 100644 index 0000000..de0fd0b --- /dev/null +++ b/includes/shipping/legacy-flat-rate/includes/settings-flat-rate.php @@ -0,0 +1,133 @@ +10.00 * [qty]
    .', 'woocommerce' ) . '
    ' . __( 'Supports the following placeholders: [qty] = number of items, [cost] = cost of items, [fee percent="10" min_fee="20"] = Percentage based fee.', 'woocommerce' ); + +/** + * Settings for flat rate shipping. + */ +$settings = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ), + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Method title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'Flat rate', 'woocommerce' ), + 'desc_tip' => true, + ), + 'availability' => array( + 'title' => __( 'Availability', 'woocommerce' ), + 'type' => 'select', + 'default' => 'all', + 'class' => 'availability wc-enhanced-select', + 'options' => array( + 'all' => __( 'All allowed countries', 'woocommerce' ), + 'specific' => __( 'Specific Countries', 'woocommerce' ), + ), + ), + 'countries' => array( + 'title' => __( 'Specific countries', 'woocommerce' ), + 'type' => 'multiselect', + 'class' => 'wc-enhanced-select', + 'css' => 'width: 400px;', + 'default' => '', + 'options' => WC()->countries->get_shipping_countries(), + 'custom_attributes' => array( + 'data-placeholder' => __( 'Select some countries', 'woocommerce' ), + ), + ), + 'tax_status' => array( + 'title' => __( 'Tax status', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'default' => 'taxable', + 'options' => array( + 'taxable' => __( 'Taxable', 'woocommerce' ), + 'none' => _x( 'None', 'Tax status', 'woocommerce' ), + ), + ), + 'cost' => array( + 'title' => __( 'Cost', 'woocommerce' ), + 'type' => 'text', + 'placeholder' => '', + 'description' => $cost_desc, + 'default' => '', + 'desc_tip' => true, + ), +); + +$shipping_classes = WC()->shipping()->get_shipping_classes(); + +if ( ! empty( $shipping_classes ) ) { + $settings['class_costs'] = array( + 'title' => __( 'Shipping class costs', 'woocommerce' ), + 'type' => 'title', + 'default' => '', + /* translators: %s: Admin shipping settings URL */ + 'description' => sprintf( __( 'These costs can optionally be added based on the product shipping class.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=classes' ) ), + ); + foreach ( $shipping_classes as $shipping_class ) { + if ( ! isset( $shipping_class->term_id ) ) { + continue; + } + $settings[ 'class_cost_' . $shipping_class->term_id ] = array( + /* translators: %s: shipping class name */ + 'title' => sprintf( __( '"%s" shipping class cost', 'woocommerce' ), esc_html( $shipping_class->name ) ), + 'type' => 'text', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'description' => $cost_desc, + 'default' => $this->get_option( 'class_cost_' . $shipping_class->slug ), // Before 2.5.0, we used slug here which caused issues with long setting names. + 'desc_tip' => true, + ); + } + $settings['no_class_cost'] = array( + 'title' => __( 'No shipping class cost', 'woocommerce' ), + 'type' => 'text', + 'placeholder' => __( 'N/A', 'woocommerce' ), + 'description' => $cost_desc, + 'default' => '', + 'desc_tip' => true, + ); + $settings['type'] = array( + 'title' => __( 'Calculation type', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'default' => 'class', + 'options' => array( + 'class' => __( 'Per class: Charge shipping for each shipping class individually', 'woocommerce' ), + 'order' => __( 'Per order: Charge shipping for the most expensive shipping class', 'woocommerce' ), + ), + ); +} + +if ( apply_filters( 'woocommerce_enable_deprecated_additional_flat_rates', $this->get_option( 'options', false ) ) ) { + $settings['additional_rates'] = array( + 'title' => __( 'Additional rates', 'woocommerce' ), + 'type' => 'title', + 'default' => '', + 'description' => __( 'These rates are extra shipping options with additional costs (based on the flat rate).', 'woocommerce' ), + ); + $settings['options'] = array( + 'title' => __( 'Additional rates', 'woocommerce' ), + 'type' => 'textarea', + 'description' => __( 'One per line: Option name | Additional cost [+- Percents] | Per cost type (order, class, or item) Example: Priority mail | 6.95 [+ 0.2%] | order.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => __( 'Option name | Additional cost [+- Percents%] | Per cost type (order, class, or item)', 'woocommerce' ), + ); +} + +return $settings; diff --git a/includes/shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php b/includes/shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php new file mode 100644 index 0000000..1993426 --- /dev/null +++ b/includes/shipping/legacy-free-shipping/class-wc-shipping-legacy-free-shipping.php @@ -0,0 +1,252 @@ +id = 'legacy_free_shipping'; + $this->method_title = __( 'Free shipping (legacy)', 'woocommerce' ); + /* translators: %s: Admin shipping settings URL */ + $this->method_description = '' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your Shipping zones.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . ''; + $this->init(); + } + + /** + * Process and redirect if disabled. + */ + public function process_admin_options() { + parent::process_admin_options(); + + if ( 'no' === $this->settings['enabled'] ) { + wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); + exit; + } + } + + /** + * Return the name of the option in the WP DB. + * + * @since 2.6.0 + * @return string + */ + public function get_option_key() { + return $this->plugin_id . 'free_shipping_settings'; + } + + /** + * Init function. + */ + public function init() { + + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->enabled = $this->get_option( 'enabled' ); + $this->title = $this->get_option( 'title' ); + $this->min_amount = $this->get_option( 'min_amount', 0 ); + $this->availability = $this->get_option( 'availability' ); + $this->countries = $this->get_option( 'countries' ); + $this->requires = $this->get_option( 'requires' ); + + // Actions. + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + } + + /** + * Initialise Gateway Settings Form Fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable/Disable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ), + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Method title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'Free Shipping', 'woocommerce' ), + 'desc_tip' => true, + ), + 'availability' => array( + 'title' => __( 'Method availability', 'woocommerce' ), + 'type' => 'select', + 'default' => 'all', + 'class' => 'availability wc-enhanced-select', + 'options' => array( + 'all' => __( 'All allowed countries', 'woocommerce' ), + 'specific' => __( 'Specific Countries', 'woocommerce' ), + ), + ), + 'countries' => array( + 'title' => __( 'Specific countries', 'woocommerce' ), + 'type' => 'multiselect', + 'class' => 'wc-enhanced-select', + 'css' => 'width: 400px;', + 'default' => '', + 'options' => WC()->countries->get_shipping_countries(), + 'custom_attributes' => array( + 'data-placeholder' => __( 'Select some countries', 'woocommerce' ), + ), + ), + 'requires' => array( + 'title' => __( 'Free shipping requires...', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'default' => '', + 'options' => array( + '' => __( 'N/A', 'woocommerce' ), + 'coupon' => __( 'A valid free shipping coupon', 'woocommerce' ), + 'min_amount' => __( 'A minimum order amount', 'woocommerce' ), + 'either' => __( 'A minimum order amount OR a coupon', 'woocommerce' ), + 'both' => __( 'A minimum order amount AND a coupon', 'woocommerce' ), + ), + ), + 'min_amount' => array( + 'title' => __( 'Minimum order amount', 'woocommerce' ), + 'type' => 'price', + 'placeholder' => wc_format_localized_price( 0 ), + 'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'woocommerce' ), + 'default' => '0', + 'desc_tip' => true, + ), + ); + } + + /** + * Check if package is available. + * + * @param array $package Package information. + * @return bool + */ + public function is_available( $package ) { + if ( 'no' === $this->enabled ) { + return false; + } + + if ( 'specific' === $this->availability ) { + $ship_to_countries = $this->countries; + } else { + $ship_to_countries = array_keys( WC()->countries->get_shipping_countries() ); + } + + if ( is_array( $ship_to_countries ) && ! in_array( $package['destination']['country'], $ship_to_countries, true ) ) { + return false; + } + + // Enabled logic. + $is_available = false; + $has_coupon = false; + $has_met_min_amount = false; + + if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) { + $coupons = WC()->cart->get_coupons(); + + if ( $coupons ) { + foreach ( $coupons as $code => $coupon ) { + if ( $coupon->is_valid() && $coupon->get_free_shipping() ) { + $has_coupon = true; + } + } + } + } + + if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) { + $total = WC()->cart->get_displayed_subtotal(); + + if ( WC()->cart->display_prices_including_tax() ) { + $total = NumberUtil::round( $total - ( WC()->cart->get_discount_total() + WC()->cart->get_discount_tax() ), wc_get_price_decimals() ); + } else { + $total = NumberUtil::round( $total - WC()->cart->get_discount_total(), wc_get_price_decimals() ); + } + + if ( $total >= $this->min_amount ) { + $has_met_min_amount = true; + } + } + + switch ( $this->requires ) { + case 'min_amount': + if ( $has_met_min_amount ) { + $is_available = true; + } + break; + case 'coupon': + if ( $has_coupon ) { + $is_available = true; + } + break; + case 'both': + if ( $has_met_min_amount && $has_coupon ) { + $is_available = true; + } + break; + case 'either': + if ( $has_met_min_amount || $has_coupon ) { + $is_available = true; + } + break; + default: + $is_available = true; + break; + } + + return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this ); + } + + /** + * Calculate shipping. + * + * @param array $package Package information. + */ + public function calculate_shipping( $package = array() ) { + $args = array( + 'id' => $this->id, + 'label' => $this->title, + 'cost' => 0, + 'taxes' => false, + 'package' => $package, + ); + $this->add_rate( $args ); + } +} diff --git a/includes/shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php b/includes/shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php new file mode 100644 index 0000000..6ddc038 --- /dev/null +++ b/includes/shipping/legacy-international-delivery/class-wc-shipping-legacy-international-delivery.php @@ -0,0 +1,85 @@ +id = 'legacy_international_delivery'; + $this->method_title = __( 'International flat rate (legacy)', 'woocommerce' ); + /* translators: %s: Admin shipping settings URL */ + $this->method_description = '' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your Shipping zones.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . ''; + $this->init(); + + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + } + + /** + * Return the name of the option in the WP DB. + * + * @since 2.6.0 + * @return string + */ + public function get_option_key() { + return $this->plugin_id . 'international_delivery_settings'; + } + + /** + * Initialise settings form fields. + */ + public function init_form_fields() { + parent::init_form_fields(); + $this->form_fields['availability'] = array( + 'title' => __( 'Availability', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'description' => '', + 'default' => 'including', + 'options' => array( + 'including' => __( 'Selected countries', 'woocommerce' ), + 'excluding' => __( 'Excluding selected countries', 'woocommerce' ), + ), + ); + } + + /** + * Check if package is available. + * + * @param array $package Package information. + * @return bool + */ + public function is_available( $package ) { + if ( 'no' === $this->enabled ) { + return false; + } + if ( 'including' === $this->availability ) { + if ( is_array( $this->countries ) && ! in_array( $package['destination']['country'], $this->countries, true ) ) { + return false; + } + } else { + if ( is_array( $this->countries ) && ( in_array( $package['destination']['country'], $this->countries, true ) || ! $package['destination']['country'] ) ) { + return false; + } + } + return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', true, $package, $this ); + } +} diff --git a/includes/shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php b/includes/shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php new file mode 100644 index 0000000..4484b79 --- /dev/null +++ b/includes/shipping/legacy-local-delivery/class-wc-shipping-legacy-local-delivery.php @@ -0,0 +1,181 @@ +id = 'legacy_local_delivery'; + $this->method_title = __( 'Local delivery (legacy)', 'woocommerce' ); + /* translators: %s: Admin shipping settings URL */ + $this->method_description = '' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your Shipping zones.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . ''; + $this->init(); + } + + /** + * Process and redirect if disabled. + */ + public function process_admin_options() { + parent::process_admin_options(); + + if ( 'no' === $this->settings['enabled'] ) { + wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); + exit; + } + } + + /** + * Return the name of the option in the WP DB. + * + * @since 2.6.0 + * @return string + */ + public function get_option_key() { + return $this->plugin_id . 'local_delivery_settings'; + } + + /** + * Init function. + */ + public function init() { + + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->type = $this->get_option( 'type' ); + $this->fee = $this->get_option( 'fee' ); + $this->type = $this->get_option( 'type' ); + $this->codes = $this->get_option( 'codes' ); + $this->availability = $this->get_option( 'availability' ); + $this->countries = $this->get_option( 'countries' ); + + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + } + + /** + * Calculate_shipping function. + * + * @param array $package (default: array()). + */ + public function calculate_shipping( $package = array() ) { + $shipping_total = 0; + + switch ( $this->type ) { + case 'fixed': + $shipping_total = $this->fee; + break; + case 'percent': + $shipping_total = $package['contents_cost'] * ( $this->fee / 100 ); + break; + case 'product': + foreach ( $package['contents'] as $item_id => $values ) { + if ( $values['quantity'] > 0 && $values['data']->needs_shipping() ) { + $shipping_total += $this->fee * $values['quantity']; + } + } + break; + } + + $rate = array( + 'id' => $this->id, + 'label' => $this->title, + 'cost' => $shipping_total, + 'package' => $package, + ); + + $this->add_rate( $rate ); + } + + /** + * Init form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ), + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'Local delivery', 'woocommerce' ), + 'desc_tip' => true, + ), + 'type' => array( + 'title' => __( 'Fee type', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'description' => __( 'How to calculate delivery charges', 'woocommerce' ), + 'default' => 'fixed', + 'options' => array( + 'fixed' => __( 'Fixed amount', 'woocommerce' ), + 'percent' => __( 'Percentage of cart total', 'woocommerce' ), + 'product' => __( 'Fixed amount per product', 'woocommerce' ), + ), + 'desc_tip' => true, + ), + 'fee' => array( + 'title' => __( 'Delivery fee', 'woocommerce' ), + 'type' => 'price', + 'description' => __( 'What fee do you want to charge for local delivery, disregarded if you choose free. Leave blank to disable.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + 'placeholder' => wc_format_localized_price( 0 ), + ), + 'codes' => array( + 'title' => __( 'Allowed ZIP/post codes', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => __( 'What ZIP/post codes are available for local delivery?', 'woocommerce' ), + 'default' => '', + 'description' => __( 'Separate codes with a comma. Accepts wildcards, e.g. P* will match a postcode of PE30. Also accepts a pattern, e.g. NG1___ would match NG1 1AA but not NG10 1AA', 'woocommerce' ), + 'placeholder' => 'e.g. 12345, 56789', + ), + 'availability' => array( + 'title' => __( 'Method availability', 'woocommerce' ), + 'type' => 'select', + 'default' => 'all', + 'class' => 'availability wc-enhanced-select', + 'options' => array( + 'all' => __( 'All allowed countries', 'woocommerce' ), + 'specific' => __( 'Specific Countries', 'woocommerce' ), + ), + ), + 'countries' => array( + 'title' => __( 'Specific countries', 'woocommerce' ), + 'type' => 'multiselect', + 'class' => 'wc-enhanced-select', + 'css' => 'width: 400px;', + 'default' => '', + 'options' => WC()->countries->get_shipping_countries(), + 'custom_attributes' => array( + 'data-placeholder' => __( 'Select some countries', 'woocommerce' ), + ), + ), + ); + } +} diff --git a/includes/shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php b/includes/shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php new file mode 100644 index 0000000..dc0608e --- /dev/null +++ b/includes/shipping/legacy-local-pickup/class-wc-shipping-legacy-local-pickup.php @@ -0,0 +1,232 @@ +id = 'legacy_local_pickup'; + $this->method_title = __( 'Local pickup (legacy)', 'woocommerce' ); + /* translators: %s: Admin shipping settings URL */ + $this->method_description = '' . sprintf( __( 'This method is deprecated in 2.6.0 and will be removed in future versions - we recommend disabling it and instead setting up a new rate within your Shipping zones.', 'woocommerce' ), admin_url( 'admin.php?page=wc-settings&tab=shipping' ) ) . ''; + $this->init(); + } + + /** + * Process and redirect if disabled. + */ + public function process_admin_options() { + parent::process_admin_options(); + + if ( 'no' === $this->settings['enabled'] ) { + wp_redirect( admin_url( 'admin.php?page=wc-settings&tab=shipping§ion=options' ) ); + exit; + } + } + + /** + * Return the name of the option in the WP DB. + * + * @since 2.6.0 + * @return string + */ + public function get_option_key() { + return $this->plugin_id . 'local_pickup_settings'; + } + + /** + * Init function. + */ + public function init() { + + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->enabled = $this->get_option( 'enabled' ); + $this->title = $this->get_option( 'title' ); + $this->codes = $this->get_option( 'codes' ); + $this->availability = $this->get_option( 'availability' ); + $this->countries = $this->get_option( 'countries' ); + + // Actions. + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + } + + /** + * Calculate shipping. + * + * @param array $package Package information. + */ + public function calculate_shipping( $package = array() ) { + $rate = array( + 'id' => $this->id, + 'label' => $this->title, + 'package' => $package, + ); + $this->add_rate( $rate ); + } + + /** + * Initialize form fields. + */ + public function init_form_fields() { + $this->form_fields = array( + 'enabled' => array( + 'title' => __( 'Enable', 'woocommerce' ), + 'type' => 'checkbox', + 'label' => __( 'Once disabled, this legacy method will no longer be available.', 'woocommerce' ), + 'default' => 'no', + ), + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'Local pickup', 'woocommerce' ), + 'desc_tip' => true, + ), + 'codes' => array( + 'title' => __( 'Allowed ZIP/post codes', 'woocommerce' ), + 'type' => 'text', + 'desc_tip' => __( 'What ZIP/post codes are available for local pickup?', 'woocommerce' ), + 'default' => '', + 'description' => __( 'Separate codes with a comma. Accepts wildcards, e.g. P* will match a postcode of PE30. Also accepts a pattern, e.g. NG1___ would match NG1 1AA but not NG10 1AA', 'woocommerce' ), + 'placeholder' => 'e.g. 12345, 56789', + ), + 'availability' => array( + 'title' => __( 'Method availability', 'woocommerce' ), + 'type' => 'select', + 'default' => 'all', + 'class' => 'availability wc-enhanced-select', + 'options' => array( + 'all' => __( 'All allowed countries', 'woocommerce' ), + 'specific' => __( 'Specific countries', 'woocommerce' ), + ), + ), + 'countries' => array( + 'title' => __( 'Specific countries', 'woocommerce' ), + 'type' => 'multiselect', + 'class' => 'wc-enhanced-select', + 'css' => 'width: 400px;', + 'default' => '', + 'options' => WC()->countries->get_shipping_countries(), + 'custom_attributes' => array( + 'data-placeholder' => __( 'Select some countries', 'woocommerce' ), + ), + ), + ); + } + + /** + * Get postcodes for this method. + * + * @return array + */ + public function get_valid_postcodes() { + $codes = array(); + + if ( '' !== $this->codes ) { + foreach ( explode( ',', $this->codes ) as $code ) { + $codes[] = strtoupper( trim( $code ) ); + } + } + + return $codes; + } + + /** + * See if a given postcode matches valid postcodes. + * + * @param string $postcode Postcode to check. + * @param string $country code Code of the country to check postcode against. + * @return boolean + */ + public function is_valid_postcode( $postcode, $country ) { + $codes = $this->get_valid_postcodes(); + $postcode = $this->clean( $postcode ); + $formatted_postcode = wc_format_postcode( $postcode, $country ); + + if ( in_array( $postcode, $codes, true ) || in_array( $formatted_postcode, $codes, true ) ) { + return true; + } + + // Pattern matching. + foreach ( $codes as $c ) { + $pattern = '/^' . str_replace( '_', '[0-9a-zA-Z]', preg_quote( $c ) ) . '$/i'; + if ( preg_match( $pattern, $postcode ) ) { + return true; + } + } + + // Wildcard search. + $wildcard_postcode = $formatted_postcode . '*'; + $postcode_length = strlen( $formatted_postcode ); + + for ( $i = 0; $i < $postcode_length; $i++ ) { + if ( in_array( $wildcard_postcode, $codes, true ) ) { + return true; + } + $wildcard_postcode = substr( $wildcard_postcode, 0, -2 ) . '*'; + } + + return false; + } + + /** + * See if the method is available. + * + * @param array $package Package information. + * @return bool + */ + public function is_available( $package ) { + $is_available = 'yes' === $this->enabled; + + if ( $is_available && $this->get_valid_postcodes() ) { + $is_available = $this->is_valid_postcode( $package['destination']['postcode'], $package['destination']['country'] ); + } + + if ( $is_available ) { + if ( 'specific' === $this->availability ) { + $ship_to_countries = $this->countries; + } else { + $ship_to_countries = array_keys( WC()->countries->get_shipping_countries() ); + } + if ( is_array( $ship_to_countries ) && ! in_array( $package['destination']['country'], $ship_to_countries, true ) ) { + $is_available = false; + } + } + + return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this ); + } + + /** + * Clean function. + * + * @access public + * @param mixed $code Code. + * @return string + */ + public function clean( $code ) { + return str_replace( '-', '', sanitize_title( $code ) ) . ( strstr( $code, '*' ) ? '*' : '' ); + } +} diff --git a/includes/shipping/local-pickup/class-wc-shipping-local-pickup.php b/includes/shipping/local-pickup/class-wc-shipping-local-pickup.php new file mode 100644 index 0000000..a64c699 --- /dev/null +++ b/includes/shipping/local-pickup/class-wc-shipping-local-pickup.php @@ -0,0 +1,106 @@ +id = 'local_pickup'; + $this->instance_id = absint( $instance_id ); + $this->method_title = __( 'Local pickup', 'woocommerce' ); + $this->method_description = __( 'Allow customers to pick up orders themselves. By default, when using local pickup store base taxes will apply regardless of customer address.', 'woocommerce' ); + $this->supports = array( + 'shipping-zones', + 'instance-settings', + 'instance-settings-modal', + ); + $this->init(); + } + + /** + * Initialize local pickup. + */ + public function init() { + + // Load the settings. + $this->init_form_fields(); + $this->init_settings(); + + // Define user set variables. + $this->title = $this->get_option( 'title' ); + $this->tax_status = $this->get_option( 'tax_status' ); + $this->cost = $this->get_option( 'cost' ); + + // Actions. + add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) ); + } + + /** + * Calculate local pickup shipping. + * + * @param array $package Package information. + */ + public function calculate_shipping( $package = array() ) { + $this->add_rate( + array( + 'label' => $this->title, + 'package' => $package, + 'cost' => $this->cost, + ) + ); + } + + /** + * Init form fields. + */ + public function init_form_fields() { + $this->instance_form_fields = array( + 'title' => array( + 'title' => __( 'Title', 'woocommerce' ), + 'type' => 'text', + 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ), + 'default' => __( 'Local pickup', 'woocommerce' ), + 'desc_tip' => true, + ), + 'tax_status' => array( + 'title' => __( 'Tax status', 'woocommerce' ), + 'type' => 'select', + 'class' => 'wc-enhanced-select', + 'default' => 'taxable', + 'options' => array( + 'taxable' => __( 'Taxable', 'woocommerce' ), + 'none' => _x( 'None', 'Tax status', 'woocommerce' ), + ), + ), + 'cost' => array( + 'title' => __( 'Cost', 'woocommerce' ), + 'type' => 'text', + 'placeholder' => '0', + 'description' => __( 'Optional cost for local pickup.', 'woocommerce' ), + 'default' => '', + 'desc_tip' => true, + ), + ); + } +} diff --git a/includes/shortcodes/class-wc-shortcode-cart.php b/includes/shortcodes/class-wc-shortcode-cart.php new file mode 100644 index 0000000..3784fb7 --- /dev/null +++ b/includes/shortcodes/class-wc-shortcode-cart.php @@ -0,0 +1,102 @@ +shipping()->reset_shipping(); + + $address = array(); + + $address['country'] = isset( $_POST['calc_shipping_country'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_country'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. + $address['state'] = isset( $_POST['calc_shipping_state'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_state'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. + $address['postcode'] = isset( $_POST['calc_shipping_postcode'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_postcode'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. + $address['city'] = isset( $_POST['calc_shipping_city'] ) ? wc_clean( wp_unslash( $_POST['calc_shipping_city'] ) ) : ''; // WPCS: input var ok, CSRF ok, sanitization ok. + + $address = apply_filters( 'woocommerce_cart_calculate_shipping_address', $address ); + + if ( $address['postcode'] && ! WC_Validation::is_postcode( $address['postcode'], $address['country'] ) ) { + throw new Exception( __( 'Please enter a valid postcode / ZIP.', 'woocommerce' ) ); + } elseif ( $address['postcode'] ) { + $address['postcode'] = wc_format_postcode( $address['postcode'], $address['country'] ); + } + + if ( $address['country'] ) { + if ( ! WC()->customer->get_billing_first_name() ) { + WC()->customer->set_billing_location( $address['country'], $address['state'], $address['postcode'], $address['city'] ); + } + WC()->customer->set_shipping_location( $address['country'], $address['state'], $address['postcode'], $address['city'] ); + } else { + WC()->customer->set_billing_address_to_base(); + WC()->customer->set_shipping_address_to_base(); + } + + WC()->customer->set_calculated_shipping( true ); + WC()->customer->save(); + + wc_add_notice( __( 'Shipping costs updated.', 'woocommerce' ), 'notice' ); + + do_action( 'woocommerce_calculated_shipping' ); + + } catch ( Exception $e ) { + if ( ! empty( $e ) ) { + wc_add_notice( $e->getMessage(), 'error' ); + } + } + } + + /** + * Output the cart shortcode. + * + * @param array $atts Shortcode attributes. + */ + public static function output( $atts ) { + if ( ! apply_filters( 'woocommerce_output_cart_shortcode_content', true ) ) { + return; + } + + // Constants. + wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); + + $atts = shortcode_atts( array(), $atts, 'woocommerce_cart' ); + $nonce_value = wc_get_var( $_REQUEST['woocommerce-shipping-calculator-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + // Update Shipping. Nonce check uses new value and old value (woocommerce-cart). @todo remove in 4.0. + if ( ! empty( $_POST['calc_shipping'] ) && ( wp_verify_nonce( $nonce_value, 'woocommerce-shipping-calculator' ) || wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) ) { // WPCS: input var ok. + self::calculate_shipping(); + + // Also calc totals before we check items so subtotals etc are up to date. + WC()->cart->calculate_totals(); + } + + // Check cart items are valid. + do_action( 'woocommerce_check_cart_items' ); + + // Calc totals. + WC()->cart->calculate_totals(); + + if ( WC()->cart->is_empty() ) { + wc_get_template( 'cart/cart-empty.php' ); + } else { + wc_get_template( 'cart/cart.php' ); + } + } +} diff --git a/includes/shortcodes/class-wc-shortcode-checkout.php b/includes/shortcodes/class-wc-shortcode-checkout.php new file mode 100644 index 0000000..4f6450e --- /dev/null +++ b/includes/shortcodes/class-wc-shortcode-checkout.php @@ -0,0 +1,297 @@ +cart ) ) { + return; + } + + // Backwards compatibility with old pay and thanks link arguments. + if ( isset( $_GET['order'] ) && isset( $_GET['key'] ) ) { // WPCS: input var ok, CSRF ok. + wc_deprecated_argument( __CLASS__ . '->' . __FUNCTION__, '2.1', '"order" is no longer used to pass an order ID. Use the order-pay or order-received endpoint instead.' ); + + // Get the order to work out what we are showing. + $order_id = absint( $_GET['order'] ); // WPCS: input var ok. + $order = wc_get_order( $order_id ); + + if ( $order && $order->has_status( 'pending' ) ) { + $wp->query_vars['order-pay'] = absint( $_GET['order'] ); // WPCS: input var ok. + } else { + $wp->query_vars['order-received'] = absint( $_GET['order'] ); // WPCS: input var ok. + } + } + + // Handle checkout actions. + if ( ! empty( $wp->query_vars['order-pay'] ) ) { + + self::order_pay( $wp->query_vars['order-pay'] ); + + } elseif ( isset( $wp->query_vars['order-received'] ) ) { + + self::order_received( $wp->query_vars['order-received'] ); + + } else { + + self::checkout(); + + } + } + + /** + * Show the pay page. + * + * @throws Exception When validate fails. + * @param int $order_id Order ID. + */ + private static function order_pay( $order_id ) { + + do_action( 'before_woocommerce_pay' ); + + $order_id = absint( $order_id ); + + // Pay for existing order. + if ( isset( $_GET['pay_for_order'], $_GET['key'] ) && $order_id ) { // WPCS: input var ok, CSRF ok. + try { + $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok. + $order = wc_get_order( $order_id ); + + // Order or payment link is invalid. + if ( ! $order || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) { + throw new Exception( __( 'Sorry, this order is invalid and cannot be paid for.', 'woocommerce' ) ); + } + + // Logged out customer does not have permission to pay for this order. + if ( ! current_user_can( 'pay_for_order', $order_id ) && ! is_user_logged_in() ) { + echo '
    ' . esc_html__( 'Please log in to your account below to continue to the payment form.', 'woocommerce' ) . '
    '; + woocommerce_login_form( + array( + 'redirect' => $order->get_checkout_payment_url(), + ) + ); + return; + } + + // Add notice if logged in customer is trying to pay for guest order. + if ( ! $order->get_user_id() && is_user_logged_in() ) { + // If order has does not have same billing email then current logged in user then show warning. + if ( $order->get_billing_email() !== wp_get_current_user()->user_email ) { + wc_print_notice( __( 'You are paying for a guest order. Please continue with payment only if you recognize this order.', 'woocommerce' ), 'error' ); + } + } + + // Logged in customer trying to pay for someone else's order. + if ( ! current_user_can( 'pay_for_order', $order_id ) ) { + throw new Exception( __( 'This order cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ) ); + } + + // Does not need payment. + if ( ! $order->needs_payment() ) { + /* translators: %s: order status */ + throw new Exception( sprintf( __( 'This order’s status is “%s”—it cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ), wc_get_order_status_name( $order->get_status() ) ) ); + } + + // Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held. + if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) { + $quantities = array(); + + foreach ( $order->get_items() as $item_key => $item ) { + if ( $item && is_callable( array( $item, 'get_product' ) ) ) { + $product = $item->get_product(); + + if ( ! $product ) { + continue; + } + + $quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $item->get_quantity() : $item->get_quantity(); + } + } + + foreach ( $order->get_items() as $item_key => $item ) { + if ( $item && is_callable( array( $item, 'get_product' ) ) ) { + $product = $item->get_product(); + + if ( ! $product ) { + continue; + } + + if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) { + /* translators: %s: product name */ + throw new Exception( sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ) ); + } + + // We only need to check products managing stock, with a limited stock qty. + if ( ! $product->managing_stock() || $product->backorders_allowed() ) { + continue; + } + + // Check stock based on all items in the cart and consider any held stock within pending orders. + $held_stock = wc_get_held_stock_quantity( $product, $order->get_id() ); + $required_stock = $quantities[ $product->get_stock_managed_by_id() ]; + + if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) { + /* translators: 1: product name 2: quantity in stock */ + throw new Exception( sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ) ); + } + } + } + } + + 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, + ) + ); + WC()->customer->save(); + + $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); + + if ( count( $available_gateways ) ) { + current( $available_gateways )->set_current(); + } + + wc_get_template( + 'checkout/form-pay.php', + array( + 'order' => $order, + 'available_gateways' => $available_gateways, + 'order_button_text' => apply_filters( 'woocommerce_pay_order_button_text', __( 'Pay for order', 'woocommerce' ) ), + ) + ); + + } catch ( Exception $e ) { + wc_print_notice( $e->getMessage(), 'error' ); + } + } elseif ( $order_id ) { + + // Pay for order after checkout step. + $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok. + $order = wc_get_order( $order_id ); + + if ( $order && $order->get_id() === $order_id && hash_equals( $order->get_order_key(), $order_key ) ) { + + if ( $order->needs_payment() ) { + + wc_get_template( 'checkout/order-receipt.php', array( 'order' => $order ) ); + + } else { + /* translators: %s: order status */ + wc_print_notice( sprintf( __( 'This order’s status is “%s”—it cannot be paid for. Please contact us if you need assistance.', 'woocommerce' ), wc_get_order_status_name( $order->get_status() ) ), 'error' ); + } + } else { + wc_print_notice( __( 'Sorry, this order is invalid and cannot be paid for.', 'woocommerce' ), 'error' ); + } + } else { + wc_print_notice( __( 'Invalid order.', 'woocommerce' ), 'error' ); + } + + do_action( 'after_woocommerce_pay' ); + } + + /** + * Show the thanks page. + * + * @param int $order_id Order ID. + */ + private static function order_received( $order_id = 0 ) { + $order = false; + + // Get the order. + $order_id = apply_filters( 'woocommerce_thankyou_order_id', absint( $order_id ) ); + $order_key = apply_filters( 'woocommerce_thankyou_order_key', empty( $_GET['key'] ) ? '' : wc_clean( wp_unslash( $_GET['key'] ) ) ); // WPCS: input var ok, CSRF ok. + + if ( $order_id > 0 ) { + $order = wc_get_order( $order_id ); + if ( ! $order || ! hash_equals( $order->get_order_key(), $order_key ) ) { + $order = false; + } + } + + // Empty awaiting payment session. + unset( WC()->session->order_awaiting_payment ); + + // In case order is created from admin, but paid by the actual customer, store the ip address of the payer + // when they visit the payment confirmation page. + if ( $order && $order->is_created_via( 'admin' ) ) { + $order->set_customer_ip_address( WC_Geolocation::get_ip_address() ); + $order->save(); + } + + // Empty current cart. + wc_empty_cart(); + + wc_get_template( 'checkout/thankyou.php', array( 'order' => $order ) ); + } + + /** + * Show the checkout. + */ + private static function checkout() { + // Show non-cart errors. + do_action( 'woocommerce_before_checkout_form_cart_notices' ); + + // Check cart has contents. + if ( WC()->cart->is_empty() && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) { + return; + } + + // Check cart contents for errors. + do_action( 'woocommerce_check_cart_items' ); + + // Calc totals. + WC()->cart->calculate_totals(); + + // Get checkout object. + $checkout = WC()->checkout(); + + if ( empty( $_POST ) && wc_notice_count( 'error' ) > 0 ) { // WPCS: input var ok, CSRF ok. + + wc_get_template( 'checkout/cart-errors.php', array( 'checkout' => $checkout ) ); + wc_clear_notices(); + + } else { + + $non_js_checkout = ! empty( $_POST['woocommerce_checkout_update_totals'] ); // WPCS: input var ok, CSRF ok. + + if ( wc_notice_count( 'error' ) === 0 && $non_js_checkout ) { + wc_add_notice( __( 'The order totals have been updated. Please confirm your order by pressing the "Place order" button at the bottom of the page.', 'woocommerce' ) ); + } + + wc_get_template( 'checkout/form-checkout.php', array( 'checkout' => $checkout ) ); + + } + } +} diff --git a/includes/shortcodes/class-wc-shortcode-my-account.php b/includes/shortcodes/class-wc-shortcode-my-account.php new file mode 100644 index 0000000..b61d169 --- /dev/null +++ b/includes/shortcodes/class-wc-shortcode-my-account.php @@ -0,0 +1,415 @@ +cart ) ) { + return; + } + + if ( ! is_user_logged_in() || isset( $wp->query_vars['lost-password'] ) ) { + $message = apply_filters( 'woocommerce_my_account_message', '' ); + + if ( ! empty( $message ) ) { + wc_add_notice( $message ); + } + + // After password reset, add confirmation message. + if ( ! empty( $_GET['password-reset'] ) ) { // WPCS: input var ok, CSRF ok. + wc_add_notice( __( 'Your password has been reset successfully.', 'woocommerce' ) ); + } + + if ( isset( $wp->query_vars['lost-password'] ) ) { + self::lost_password(); + } else { + wc_get_template( 'myaccount/form-login.php' ); + } + } else { + // Start output buffer since the html may need discarding for BW compatibility. + ob_start(); + + if ( isset( $wp->query_vars['customer-logout'] ) ) { + /* translators: %s: logout url */ + wc_add_notice( sprintf( __( 'Are you sure you want to log out? Confirm and log out', 'woocommerce' ), wc_logout_url() ) ); + } + + // Collect notices before output. + $notices = wc_get_notices(); + + // Output the new account page. + self::my_account( $atts ); + + /** + * Deprecated my-account.php template handling. This code should be + * removed in a future release. + * + * If woocommerce_account_content did not run, this is an old template + * so we need to render the endpoint content again. + */ + if ( ! did_action( 'woocommerce_account_content' ) ) { + if ( ! empty( $wp->query_vars ) ) { + foreach ( $wp->query_vars as $key => $value ) { + if ( 'pagename' === $key ) { + continue; + } + if ( has_action( 'woocommerce_account_' . $key . '_endpoint' ) ) { + ob_clean(); // Clear previous buffer. + wc_set_notices( $notices ); + wc_print_notices(); + do_action( 'woocommerce_account_' . $key . '_endpoint', $value ); + break; + } + } + + wc_deprecated_function( 'Your theme version of my-account.php template', '2.6', 'the latest version, which supports multiple account pages and navigation, from WC 2.6.0' ); + } + } + + // Send output buffer. + ob_end_flush(); + } + } + + /** + * My account page. + * + * @param array $atts Shortcode attributes. + */ + private static function my_account( $atts ) { + $args = shortcode_atts( + array( + 'order_count' => 15, // @deprecated 2.6.0. Keep for backward compatibility. + ), + $atts, + 'woocommerce_my_account' + ); + + wc_get_template( + 'myaccount/my-account.php', + array( + 'current_user' => get_user_by( 'id', get_current_user_id() ), + 'order_count' => 'all' === $args['order_count'] ? -1 : $args['order_count'], + ) + ); + } + + /** + * View order page. + * + * @param int $order_id Order ID. + */ + public static function view_order( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order || ! current_user_can( 'view_order', $order_id ) ) { + echo '
    ' . esc_html__( 'Invalid order.', 'woocommerce' ) . ' ' . esc_html__( 'My account', 'woocommerce' ) . '
    '; + + return; + } + + // Backwards compatibility. + $status = new stdClass(); + $status->name = wc_get_order_status_name( $order->get_status() ); + + wc_get_template( + 'myaccount/view-order.php', + array( + 'status' => $status, // @deprecated 2.2. + 'order' => $order, + 'order_id' => $order->get_id(), + ) + ); + } + + /** + * Edit account details page. + */ + public static function edit_account() { + wc_get_template( 'myaccount/form-edit-account.php', array( 'user' => get_user_by( 'id', get_current_user_id() ) ) ); + } + + /** + * Edit address page. + * + * @param string $load_address Type of address to load. + */ + public static function edit_address( $load_address = 'billing' ) { + $current_user = wp_get_current_user(); + $load_address = sanitize_key( $load_address ); + $country = get_user_meta( get_current_user_id(), $load_address . '_country', true ); + + if ( ! $country ) { + $country = WC()->countries->get_base_country(); + } + + if ( 'billing' === $load_address ) { + $allowed_countries = WC()->countries->get_allowed_countries(); + + if ( ! array_key_exists( $country, $allowed_countries ) ) { + $country = current( array_keys( $allowed_countries ) ); + } + } + + if ( 'shipping' === $load_address ) { + $allowed_countries = WC()->countries->get_shipping_countries(); + + if ( ! array_key_exists( $country, $allowed_countries ) ) { + $country = current( array_keys( $allowed_countries ) ); + } + } + + $address = WC()->countries->get_address_fields( $country, $load_address . '_' ); + + // Enqueue scripts. + wp_enqueue_script( 'wc-country-select' ); + wp_enqueue_script( 'wc-address-i18n' ); + + // Prepare values. + foreach ( $address as $key => $field ) { + + $value = get_user_meta( get_current_user_id(), $key, true ); + + if ( ! $value ) { + switch ( $key ) { + case 'billing_email': + case 'shipping_email': + $value = $current_user->user_email; + break; + } + } + + $address[ $key ]['value'] = apply_filters( 'woocommerce_my_account_edit_address_field_value', $value, $key, $load_address ); + } + + wc_get_template( + 'myaccount/form-edit-address.php', + array( + 'load_address' => $load_address, + 'address' => apply_filters( 'woocommerce_address_to_edit', $address, $load_address ), + ) + ); + } + + /** + * Lost password page handling. + */ + public static function lost_password() { + /** + * After sending the reset link, don't show the form again. + */ + if ( ! empty( $_GET['reset-link-sent'] ) ) { // WPCS: input var ok, CSRF ok. + return wc_get_template( 'myaccount/lost-password-confirmation.php' ); + + /** + * Process reset key / login from email confirmation link + */ + } elseif ( ! empty( $_GET['show-reset-form'] ) ) { // WPCS: input var ok, CSRF ok. + if ( isset( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ) && 0 < strpos( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ], ':' ) ) { // @codingStandardsIgnoreLine + list( $rp_id, $rp_key ) = array_map( 'wc_clean', explode( ':', wp_unslash( $_COOKIE[ 'wp-resetpass-' . COOKIEHASH ] ), 2 ) ); // @codingStandardsIgnoreLine + $userdata = get_userdata( absint( $rp_id ) ); + $rp_login = $userdata ? $userdata->user_login : ''; + $user = self::check_password_reset_key( $rp_key, $rp_login ); + + // Reset key / login is correct, display reset password form with hidden key / login values. + if ( is_object( $user ) ) { + return wc_get_template( + 'myaccount/form-reset-password.php', + array( + 'key' => $rp_key, + 'login' => $rp_login, + ) + ); + } + } + } + + // Show lost password form by default. + wc_get_template( + 'myaccount/form-lost-password.php', + array( + 'form' => 'lost_password', + ) + ); + } + + /** + * Handles sending password retrieval email to customer. + * + * Based on retrieve_password() in core wp-login.php. + * + * @uses $wpdb WordPress Database object + * @return bool True: when finish. False: on error + */ + public static function retrieve_password() { + $login = isset( $_POST['user_login'] ) ? sanitize_user( wp_unslash( $_POST['user_login'] ) ) : ''; // WPCS: input var ok, CSRF ok. + + if ( empty( $login ) ) { + + wc_add_notice( __( 'Enter a username or email address.', 'woocommerce' ), 'error' ); + + return false; + + } else { + // Check on username first, as customers can use emails as usernames. + $user_data = get_user_by( 'login', $login ); + } + + // If no user found, check if it login is email and lookup user based on email. + if ( ! $user_data && is_email( $login ) && apply_filters( 'woocommerce_get_username_from_email', true ) ) { + $user_data = get_user_by( 'email', $login ); + } + + $errors = new WP_Error(); + + do_action( 'lostpassword_post', $errors, $user_data ); + + if ( $errors->get_error_code() ) { + wc_add_notice( $errors->get_error_message(), 'error' ); + + return false; + } + + if ( ! $user_data ) { + wc_add_notice( __( 'Invalid username or email.', 'woocommerce' ), 'error' ); + + return false; + } + + if ( is_multisite() && ! is_user_member_of_blog( $user_data->ID, get_current_blog_id() ) ) { + wc_add_notice( __( 'Invalid username or email.', 'woocommerce' ), 'error' ); + + return false; + } + + // Redefining user_login ensures we return the right case in the email. + $user_login = $user_data->user_login; + + do_action( 'retrieve_password', $user_login ); + + $allow = apply_filters( 'allow_password_reset', true, $user_data->ID ); + + if ( ! $allow ) { + + wc_add_notice( __( 'Password reset is not allowed for this user', 'woocommerce' ), 'error' ); + + return false; + + } elseif ( is_wp_error( $allow ) ) { + + wc_add_notice( $allow->get_error_message(), 'error' ); + + return false; + } + + // Get password reset key (function introduced in WordPress 4.4). + $key = get_password_reset_key( $user_data ); + + // Send email notification. + WC()->mailer(); // Load email classes. + do_action( 'woocommerce_reset_password_notification', $user_login, $key ); + + return true; + } + + /** + * Retrieves a user row based on password reset key and login. + * + * @uses $wpdb WordPress Database object. + * @param string $key Hash to validate sending user's password. + * @param string $login The user login. + * @return WP_User|bool User's database row on success, false for invalid keys + */ + public static function check_password_reset_key( $key, $login ) { + // Check for the password reset key. + // Get user data or an error message in case of invalid or expired key. + $user = check_password_reset_key( $key, $login ); + + if ( is_wp_error( $user ) ) { + wc_add_notice( __( 'This key is invalid or has already been used. Please reset your password again if needed.', 'woocommerce' ), 'error' ); + return false; + } + + return $user; + } + + /** + * Handles resetting the user's password. + * + * @param object $user The user. + * @param string $new_pass New password for the user in plaintext. + */ + public static function reset_password( $user, $new_pass ) { + do_action( 'password_reset', $user, $new_pass ); + + wp_set_password( $new_pass, $user->ID ); + self::set_reset_password_cookie(); + + if ( ! apply_filters( 'woocommerce_disable_password_change_notification', false ) ) { + wp_password_change_notification( $user ); + } + } + + /** + * Set or unset the cookie. + * + * @param string $value Cookie value. + */ + public static function set_reset_password_cookie( $value = '' ) { + $rp_cookie = 'wp-resetpass-' . COOKIEHASH; + $rp_path = isset( $_SERVER['REQUEST_URI'] ) ? current( explode( '?', wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) : ''; // WPCS: input var ok, sanitization ok. + + if ( $value ) { + setcookie( $rp_cookie, $value, 0, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); + } else { + setcookie( $rp_cookie, ' ', time() - YEAR_IN_SECONDS, $rp_path, COOKIE_DOMAIN, is_ssl(), true ); + } + } + + /** + * Show the add payment method page. + */ + public static function add_payment_method() { + if ( ! is_user_logged_in() ) { + wp_safe_redirect( wc_get_page_permalink( 'myaccount' ) ); + exit(); + } else { + do_action( 'before_woocommerce_add_payment_method' ); + + wc_get_template( 'myaccount/form-add-payment-method.php' ); + + do_action( 'after_woocommerce_add_payment_method' ); + } + } +} diff --git a/includes/shortcodes/class-wc-shortcode-order-tracking.php b/includes/shortcodes/class-wc-shortcode-order-tracking.php new file mode 100644 index 0000000..6798539 --- /dev/null +++ b/includes/shortcodes/class-wc-shortcode-order-tracking.php @@ -0,0 +1,71 @@ +cart ) ) { + return; + } + + $atts = shortcode_atts( array(), $atts, 'woocommerce_order_tracking' ); + $nonce_value = wc_get_var( $_REQUEST['woocommerce-order-tracking-nonce'], wc_get_var( $_REQUEST['_wpnonce'], '' ) ); // @codingStandardsIgnoreLine. + + if ( isset( $_REQUEST['orderid'] ) && wp_verify_nonce( $nonce_value, 'woocommerce-order_tracking' ) ) { // WPCS: input var ok. + + $order_id = empty( $_REQUEST['orderid'] ) ? 0 : ltrim( wc_clean( wp_unslash( $_REQUEST['orderid'] ) ), '#' ); // WPCS: input var ok. + $order_email = empty( $_REQUEST['order_email'] ) ? '' : sanitize_email( wp_unslash( $_REQUEST['order_email'] ) ); // WPCS: input var ok. + + if ( ! $order_id ) { + wc_print_notice( __( 'Please enter a valid order ID', 'woocommerce' ), 'error' ); + } elseif ( ! $order_email ) { + wc_print_notice( __( 'Please enter a valid email address', 'woocommerce' ), 'error' ); + } else { + $order = wc_get_order( apply_filters( 'woocommerce_shortcode_order_tracking_order_id', $order_id ) ); + + if ( $order && $order->get_id() && strtolower( $order->get_billing_email() ) === strtolower( $order_email ) ) { + do_action( 'woocommerce_track_order', $order->get_id() ); + wc_get_template( + 'order/tracking.php', + array( + 'order' => $order, + ) + ); + return; + } else { + wc_print_notice( __( 'Sorry, the order could not be found. Please contact us if you are having difficulty finding your order details.', 'woocommerce' ), 'error' ); + } + } + } + + wc_get_template( 'order/form-tracking.php' ); + } +} diff --git a/includes/shortcodes/class-wc-shortcode-products.php b/includes/shortcodes/class-wc-shortcode-products.php new file mode 100644 index 0000000..25ac822 --- /dev/null +++ b/includes/shortcodes/class-wc-shortcode-products.php @@ -0,0 +1,703 @@ +type = $type; + $this->attributes = $this->parse_attributes( $attributes ); + $this->query_args = $this->parse_query_args(); + } + + /** + * Get shortcode attributes. + * + * @since 3.2.0 + * @return array + */ + public function get_attributes() { + return $this->attributes; + } + + /** + * Get query args. + * + * @since 3.2.0 + * @return array + */ + public function get_query_args() { + return $this->query_args; + } + + /** + * Get shortcode type. + * + * @since 3.2.0 + * @return string + */ + public function get_type() { + return $this->type; + } + + /** + * Get shortcode content. + * + * @since 3.2.0 + * @return string + */ + public function get_content() { + return $this->product_loop(); + } + + /** + * Parse attributes. + * + * @since 3.2.0 + * @param array $attributes Shortcode attributes. + * @return array + */ + protected function parse_attributes( $attributes ) { + $attributes = $this->parse_legacy_attributes( $attributes ); + + $attributes = shortcode_atts( + array( + 'limit' => '-1', // Results limit. + 'columns' => '', // Number of columns. + 'rows' => '', // Number of rows. If defined, limit will be ignored. + 'orderby' => '', // menu_order, title, date, rand, price, popularity, rating, or id. + 'order' => '', // ASC or DESC. + 'ids' => '', // Comma separated IDs. + 'skus' => '', // Comma separated SKUs. + 'category' => '', // Comma separated category slugs or ids. + 'cat_operator' => 'IN', // Operator to compare categories. Possible values are 'IN', 'NOT IN', 'AND'. + 'attribute' => '', // Single attribute slug. + 'terms' => '', // Comma separated term slugs or ids. + 'terms_operator' => 'IN', // Operator to compare terms. Possible values are 'IN', 'NOT IN', 'AND'. + 'tag' => '', // Comma separated tag slugs. + 'tag_operator' => 'IN', // Operator to compare tags. Possible values are 'IN', 'NOT IN', 'AND'. + 'visibility' => 'visible', // Product visibility setting. Possible values are 'visible', 'catalog', 'search', 'hidden'. + 'class' => '', // HTML class. + 'page' => 1, // Page for pagination. + 'paginate' => false, // Should results be paginated. + 'cache' => true, // Should shortcode output be cached. + ), + $attributes, + $this->type + ); + + if ( ! absint( $attributes['columns'] ) ) { + $attributes['columns'] = wc_get_default_products_per_row(); + } + + return $attributes; + } + + /** + * Parse legacy attributes. + * + * @since 3.2.0 + * @param array $attributes Attributes. + * @return array + */ + protected function parse_legacy_attributes( $attributes ) { + $mapping = array( + 'per_page' => 'limit', + 'operator' => 'cat_operator', + 'filter' => 'terms', + ); + + foreach ( $mapping as $old => $new ) { + if ( isset( $attributes[ $old ] ) ) { + $attributes[ $new ] = $attributes[ $old ]; + unset( $attributes[ $old ] ); + } + } + + return $attributes; + } + + /** + * Parse query args. + * + * @since 3.2.0 + * @return array + */ + protected function parse_query_args() { + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'ignore_sticky_posts' => true, + 'no_found_rows' => false === wc_string_to_bool( $this->attributes['paginate'] ), + 'orderby' => empty( $_GET['orderby'] ) ? $this->attributes['orderby'] : wc_clean( wp_unslash( $_GET['orderby'] ) ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ); + + $orderby_value = explode( '-', $query_args['orderby'] ); + $orderby = esc_attr( $orderby_value[0] ); + $order = ! empty( $orderby_value[1] ) ? $orderby_value[1] : strtoupper( $this->attributes['order'] ); + $query_args['orderby'] = $orderby; + $query_args['order'] = $order; + + if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { + $this->attributes['page'] = absint( empty( $_GET['product-page'] ) ? 1 : $_GET['product-page'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + } + + if ( ! empty( $this->attributes['rows'] ) ) { + $this->attributes['limit'] = $this->attributes['columns'] * $this->attributes['rows']; + } + + $ordering_args = WC()->query->get_catalog_ordering_args( $query_args['orderby'], $query_args['order'] ); + $query_args['orderby'] = $ordering_args['orderby']; + $query_args['order'] = $ordering_args['order']; + if ( $ordering_args['meta_key'] ) { + $query_args['meta_key'] = $ordering_args['meta_key']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + } + $query_args['posts_per_page'] = intval( $this->attributes['limit'] ); + if ( 1 < $this->attributes['page'] ) { + $query_args['paged'] = absint( $this->attributes['page'] ); + } + $query_args['meta_query'] = WC()->query->get_meta_query(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $query_args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + + // Visibility. + $this->set_visibility_query_args( $query_args ); + + // SKUs. + $this->set_skus_query_args( $query_args ); + + // IDs. + $this->set_ids_query_args( $query_args ); + + // Set specific types query args. + if ( method_exists( $this, "set_{$this->type}_query_args" ) ) { + $this->{"set_{$this->type}_query_args"}( $query_args ); + } + + // Attributes. + $this->set_attributes_query_args( $query_args ); + + // Categories. + $this->set_categories_query_args( $query_args ); + + // Tags. + $this->set_tags_query_args( $query_args ); + + $query_args = apply_filters( 'woocommerce_shortcode_products_query', $query_args, $this->attributes, $this->type ); + + // Always query only IDs. + $query_args['fields'] = 'ids'; + + return $query_args; + } + + /** + * Set skus query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_skus_query_args( &$query_args ) { + if ( ! empty( $this->attributes['skus'] ) ) { + $skus = array_map( 'trim', explode( ',', $this->attributes['skus'] ) ); + $query_args['meta_query'][] = array( + 'key' => '_sku', + 'value' => 1 === count( $skus ) ? $skus[0] : $skus, + 'compare' => 1 === count( $skus ) ? '=' : 'IN', + ); + } + } + + /** + * Set ids query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_ids_query_args( &$query_args ) { + if ( ! empty( $this->attributes['ids'] ) ) { + $ids = array_map( 'trim', explode( ',', $this->attributes['ids'] ) ); + + if ( 1 === count( $ids ) ) { + $query_args['p'] = $ids[0]; + } else { + $query_args['post__in'] = $ids; + } + } + } + + /** + * Set attributes query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_attributes_query_args( &$query_args ) { + if ( ! empty( $this->attributes['attribute'] ) || ! empty( $this->attributes['terms'] ) ) { + $taxonomy = strstr( $this->attributes['attribute'], 'pa_' ) ? sanitize_title( $this->attributes['attribute'] ) : 'pa_' . sanitize_title( $this->attributes['attribute'] ); + $terms = $this->attributes['terms'] ? array_map( 'sanitize_title', explode( ',', $this->attributes['terms'] ) ) : array(); + $field = 'slug'; + + if ( $terms && is_numeric( $terms[0] ) ) { + $field = 'term_id'; + $terms = array_map( 'absint', $terms ); + // Check numeric slugs. + foreach ( $terms as $term ) { + $the_term = get_term_by( 'slug', $term, $taxonomy ); + if ( false !== $the_term ) { + $terms[] = $the_term->term_id; + } + } + } + + // If no terms were specified get all products that are in the attribute taxonomy. + if ( ! $terms ) { + $terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'fields' => 'ids', + ) + ); + $field = 'term_id'; + } + + // We always need to search based on the slug as well, this is to accommodate numeric slugs. + $query_args['tax_query'][] = array( + 'taxonomy' => $taxonomy, + 'terms' => $terms, + 'field' => $field, + 'operator' => $this->attributes['terms_operator'], + ); + } + } + + /** + * Set categories query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_categories_query_args( &$query_args ) { + if ( ! empty( $this->attributes['category'] ) ) { + $categories = array_map( 'sanitize_title', explode( ',', $this->attributes['category'] ) ); + $field = 'slug'; + + if ( is_numeric( $categories[0] ) ) { + $field = 'term_id'; + $categories = array_map( 'absint', $categories ); + // Check numeric slugs. + foreach ( $categories as $cat ) { + $the_cat = get_term_by( 'slug', $cat, 'product_cat' ); + if ( false !== $the_cat ) { + $categories[] = $the_cat->term_id; + } + } + } + + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_cat', + 'terms' => $categories, + 'field' => $field, + 'operator' => $this->attributes['cat_operator'], + + /* + * When cat_operator is AND, the children categories should be excluded, + * as only products belonging to all the children categories would be selected. + */ + 'include_children' => 'AND' === $this->attributes['cat_operator'] ? false : true, + ); + } + } + + /** + * Set tags query args. + * + * @since 3.3.0 + * @param array $query_args Query args. + */ + protected function set_tags_query_args( &$query_args ) { + if ( ! empty( $this->attributes['tag'] ) ) { + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_tag', + 'terms' => array_map( 'sanitize_title', explode( ',', $this->attributes['tag'] ) ), + 'field' => 'slug', + 'operator' => $this->attributes['tag_operator'], + ); + } + } + + /** + * Set sale products query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_sale_products_query_args( &$query_args ) { + $query_args['post__in'] = array_merge( array( 0 ), wc_get_product_ids_on_sale() ); + } + + /** + * Set best selling products query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_best_selling_products_query_args( &$query_args ) { + $query_args['meta_key'] = 'total_sales'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + $query_args['order'] = 'DESC'; + $query_args['orderby'] = 'meta_value_num'; + } + + /** + * Set top rated products query args. + * + * @since 3.6.5 + * @param array $query_args Query args. + */ + protected function set_top_rated_products_query_args( &$query_args ) { + $query_args['meta_key'] = '_wc_average_rating'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + $query_args['order'] = 'DESC'; + $query_args['orderby'] = 'meta_value_num'; + } + + /** + * Set visibility as hidden. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_hidden_query_args( &$query_args ) { + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => array( 'exclude-from-catalog', 'exclude-from-search' ), + 'field' => 'name', + 'operator' => 'AND', + 'include_children' => false, + ); + } + + /** + * Set visibility as catalog. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_catalog_query_args( &$query_args ) { + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-search', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-catalog', + 'field' => 'name', + 'operator' => 'NOT IN', + 'include_children' => false, + ); + } + + /** + * Set visibility as search. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_search_query_args( &$query_args ) { + $this->custom_visibility = true; + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-catalog', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'exclude-from-search', + 'field' => 'name', + 'operator' => 'NOT IN', + 'include_children' => false, + ); + } + + /** + * Set visibility as featured. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_featured_query_args( &$query_args ) { + $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'terms' => 'featured', + 'field' => 'name', + 'operator' => 'IN', + 'include_children' => false, + ); + } + + /** + * Set visibility query args. + * + * @since 3.2.0 + * @param array $query_args Query args. + */ + protected function set_visibility_query_args( &$query_args ) { + if ( method_exists( $this, 'set_visibility_' . $this->attributes['visibility'] . '_query_args' ) ) { + $this->{'set_visibility_' . $this->attributes['visibility'] . '_query_args'}( $query_args ); + } else { + $query_args['tax_query'] = array_merge( $query_args['tax_query'], WC()->query->get_tax_query() ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + } + } + + /** + * Set product as visible when querying for hidden products. + * + * @since 3.2.0 + * @param bool $visibility Product visibility. + * @return bool + */ + public function set_product_as_visible( $visibility ) { + return $this->custom_visibility ? true : $visibility; + } + + /** + * Get wrapper classes. + * + * @since 3.2.0 + * @param int $columns Number of columns. + * @return array + */ + protected function get_wrapper_classes( $columns ) { + $classes = array( 'woocommerce' ); + + if ( 'product' !== $this->type ) { + $classes[] = 'columns-' . $columns; + } + + $classes[] = $this->attributes['class']; + + return $classes; + } + + /** + * Generate and return the transient name for this shortcode based on the query args. + * + * @since 3.3.0 + * @return string + */ + protected function get_transient_name() { + $transient_name = 'wc_product_loop_' . md5( wp_json_encode( $this->query_args ) . $this->type ); + + if ( 'rand' === $this->query_args['orderby'] ) { + // When using rand, we'll cache a number of random queries and pull those to avoid querying rand on each page load. + $rand_index = wp_rand( 0, max( 1, absint( apply_filters( 'woocommerce_product_query_max_rand_cache_count', 5 ) ) ) ); + $transient_name .= $rand_index; + } + + return $transient_name; + } + + /** + * Run the query and return an array of data, including queried ids and pagination information. + * + * @since 3.3.0 + * @return object Object with the following props; ids, per_page, found_posts, max_num_pages, current_page + */ + protected function get_query_results() { + $transient_name = $this->get_transient_name(); + $transient_version = WC_Cache_Helper::get_transient_version( 'product_query' ); + $cache = wc_string_to_bool( $this->attributes['cache'] ) === true; + $transient_value = $cache ? get_transient( $transient_name ) : false; + + if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { + $results = $transient_value['value']; + } else { + $query = new WP_Query( $this->query_args ); + + $paginated = ! $query->get( 'no_found_rows' ); + + $results = (object) array( + 'ids' => wp_parse_id_list( $query->posts ), + 'total' => $paginated ? (int) $query->found_posts : count( $query->posts ), + 'total_pages' => $paginated ? (int) $query->max_num_pages : 1, + 'per_page' => (int) $query->get( 'posts_per_page' ), + 'current_page' => $paginated ? (int) max( 1, $query->get( 'paged', 1 ) ) : 1, + ); + + if ( $cache ) { + $transient_value = array( + 'version' => $transient_version, + 'value' => $results, + ); + set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); + } + } + + // Remove ordering query arguments which may have been added by get_catalog_ordering_args. + WC()->query->remove_ordering_args(); + + /** + * Filter shortcode products query results. + * + * @since 4.0.0 + * @param stdClass $results Query results. + * @param WC_Shortcode_Products $this WC_Shortcode_Products instance. + */ + return apply_filters( 'woocommerce_shortcode_products_query_results', $results, $this ); + } + + /** + * Loop over found products. + * + * @since 3.2.0 + * @return string + */ + protected function product_loop() { + $columns = absint( $this->attributes['columns'] ); + $classes = $this->get_wrapper_classes( $columns ); + $products = $this->get_query_results(); + + ob_start(); + + if ( $products && $products->ids ) { + // Prime caches to reduce future queries. + if ( is_callable( '_prime_post_caches' ) ) { + _prime_post_caches( $products->ids ); + } + + // Setup the loop. + wc_setup_loop( + array( + 'columns' => $columns, + 'name' => $this->type, + 'is_shortcode' => true, + 'is_search' => false, + 'is_paginated' => wc_string_to_bool( $this->attributes['paginate'] ), + 'total' => $products->total, + 'total_pages' => $products->total_pages, + 'per_page' => $products->per_page, + 'current_page' => $products->current_page, + ) + ); + + $original_post = $GLOBALS['post']; + + do_action( "woocommerce_shortcode_before_{$this->type}_loop", $this->attributes ); + + // Fire standard shop loop hooks when paginating results so we can show result counts and so on. + if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { + do_action( 'woocommerce_before_shop_loop' ); + } + + woocommerce_product_loop_start(); + + if ( wc_get_loop_prop( 'total' ) ) { + foreach ( $products->ids as $product_id ) { + $GLOBALS['post'] = get_post( $product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + setup_postdata( $GLOBALS['post'] ); + + // Set custom product visibility when quering hidden products. + add_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); + + // Render product template. + wc_get_template_part( 'content', 'product' ); + + // Restore product visibility. + remove_action( 'woocommerce_product_is_visible', array( $this, 'set_product_as_visible' ) ); + } + } + + $GLOBALS['post'] = $original_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + woocommerce_product_loop_end(); + + // Fire standard shop loop hooks when paginating results so we can show result counts and so on. + if ( wc_string_to_bool( $this->attributes['paginate'] ) ) { + do_action( 'woocommerce_after_shop_loop' ); + } + + do_action( "woocommerce_shortcode_after_{$this->type}_loop", $this->attributes ); + + wp_reset_postdata(); + wc_reset_loop(); + } else { + do_action( "woocommerce_shortcode_{$this->type}_loop_no_results", $this->attributes ); + } + + return '
    ' . ob_get_clean() . '
    '; + } + + /** + * Order by rating. + * + * @since 3.2.0 + * @param array $args Query args. + * @return array + */ + public static function order_by_rating_post_clauses( $args ) { + global $wpdb; + + $args['where'] .= " AND $wpdb->commentmeta.meta_key = 'rating' "; + $args['join'] .= "LEFT JOIN $wpdb->comments ON($wpdb->posts.ID = $wpdb->comments.comment_post_ID) LEFT JOIN $wpdb->commentmeta ON($wpdb->comments.comment_ID = $wpdb->commentmeta.comment_id)"; + $args['orderby'] = "$wpdb->commentmeta.meta_value DESC"; + $args['groupby'] = "$wpdb->posts.ID"; + + return $args; + } +} diff --git a/includes/theme-support/class-wc-twenty-eleven.php b/includes/theme-support/class-wc-twenty-eleven.php new file mode 100644 index 0000000..fb06a7a --- /dev/null +++ b/includes/theme-support/class-wc-twenty-eleven.php @@ -0,0 +1,56 @@ + 150, + 'single_image_width' => 300, + ) + ); + } + + /** + * Open wrappers. + */ + public static function output_content_wrapper() { + echo '
    '; + } + + /** + * Close wrappers. + */ + public static function output_content_wrapper_end() { + echo '
    '; + } +} + +WC_Twenty_Eleven::init(); diff --git a/includes/theme-support/class-wc-twenty-fifteen.php b/includes/theme-support/class-wc-twenty-fifteen.php new file mode 100644 index 0000000..83e1930 --- /dev/null +++ b/includes/theme-support/class-wc-twenty-fifteen.php @@ -0,0 +1,57 @@ + 200, + 'single_image_width' => 350, + ) + ); + } + + /** + * Open wrappers. + */ + public static function output_content_wrapper() { + echo '
    '; + } + + /** + * Close wrappers. + */ + public static function output_content_wrapper_end() { + echo '
    '; + } +} + +WC_Twenty_Fifteen::init(); diff --git a/includes/theme-support/class-wc-twenty-fourteen.php b/includes/theme-support/class-wc-twenty-fourteen.php new file mode 100644 index 0000000..ce04395 --- /dev/null +++ b/includes/theme-support/class-wc-twenty-fourteen.php @@ -0,0 +1,58 @@ + 150, + 'single_image_width' => 300, + ) + ); + } + + /** + * Open wrappers. + */ + public static function output_content_wrapper() { + echo '
    '; + } + + /** + * Close wrappers. + */ + public static function output_content_wrapper_end() { + echo '
    '; + get_sidebar( 'content' ); + } +} + +WC_Twenty_Fourteen::init(); diff --git a/includes/theme-support/class-wc-twenty-nineteen.php b/includes/theme-support/class-wc-twenty-nineteen.php new file mode 100644 index 0000000..9d1f44c --- /dev/null +++ b/includes/theme-support/class-wc-twenty-nineteen.php @@ -0,0 +1,132 @@ + 300, + 'single_image_width' => 450, + ) + ); + + // Tweak Twenty Nineteen features. + add_action( 'wp', array( __CLASS__, 'tweak_theme_features' ) ); + + // Color scheme CSS. + add_filter( 'twentynineteen_custom_colors_css', array( __CLASS__, 'custom_colors_css' ), 10, 3 ); + } + + /** + * Open the Twenty Nineteen wrapper. + */ + public static function output_content_wrapper() { + echo '
    '; + echo '
    '; + } + + /** + * Close the Twenty Nineteen wrapper. + */ + public static function output_content_wrapper_end() { + echo '
    '; + echo '
    '; + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-nineteen.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_nineteen_styles', $styles ); + } + + /** + * Tweak Twenty Nineteen features. + */ + public static function tweak_theme_features() { + if ( is_woocommerce() ) { + add_filter( 'twentynineteen_can_show_post_thumbnail', '__return_false' ); + } + } + + /** + * Filters Twenty Nineteen custom colors CSS. + * + * @param string $css Base theme colors CSS. + * @param int $primary_color The user's selected color hue. + * @param string $saturation Filtered theme color saturation level. + */ + public static function custom_colors_css( $css, $primary_color, $saturation ) { + if ( function_exists( 'register_block_type' ) && is_admin() ) { + return $css; + } + + $lightness = absint( apply_filters( 'twentynineteen_custom_colors_lightness', 33 ) ); + $lightness = $lightness . '%'; + + $css .= ' + .onsale, + .woocommerce-info, + .woocommerce-store-notice { + background-color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); + } + + .woocommerce-tabs ul li.active a { + color: hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); + box-shadow: 0 2px 0 hsl( ' . $primary_color . ', ' . $saturation . ', ' . $lightness . ' ); + } + '; + + return $css; + } +} + +WC_Twenty_Nineteen::init(); diff --git a/includes/theme-support/class-wc-twenty-seventeen.php b/includes/theme-support/class-wc-twenty-seventeen.php new file mode 100644 index 0000000..2093d22 --- /dev/null +++ b/includes/theme-support/class-wc-twenty-seventeen.php @@ -0,0 +1,114 @@ + 250, + 'single_image_width' => 350, + ) + ); + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-seventeen.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_seventeen_styles', $styles ); + } + + /** + * Open the Twenty Seventeen wrapper. + */ + public static function output_content_wrapper() { + echo '
    '; + echo '
    '; + echo '
    '; + } + + /** + * Close the Twenty Seventeen wrapper. + */ + public static function output_content_wrapper_end() { + echo '
    '; + echo '
    '; + get_sidebar(); + echo '
    '; + } + + /** + * Custom colors. + * + * @param string $css Styles. + * @param string $hue Color. + * @param string $saturation Saturation. + * @return string + */ + public static function custom_colors_css( $css, $hue, $saturation ) { + $css .= ' + .colors-custom .select2-container--default .select2-selection--single { + border-color: hsl( ' . $hue . ', ' . $saturation . ', 73% ); + } + .colors-custom .select2-container--default .select2-selection__rendered { + color: hsl( ' . $hue . ', ' . $saturation . ', 40% ); + } + .colors-custom .select2-container--default .select2-selection--single .select2-selection__arrow b { + border-color: hsl( ' . $hue . ', ' . $saturation . ', 40% ) transparent transparent transparent; + } + .colors-custom .select2-container--focus .select2-selection { + border-color: #000; + } + .colors-custom .select2-container--focus .select2-selection--single .select2-selection__arrow b { + border-color: #000 transparent transparent transparent; + } + .colors-custom .select2-container--focus .select2-selection .select2-selection__rendered { + color: #000; + } + '; + return $css; + } +} + +WC_Twenty_Seventeen::init(); diff --git a/includes/theme-support/class-wc-twenty-sixteen.php b/includes/theme-support/class-wc-twenty-sixteen.php new file mode 100644 index 0000000..c9681fa --- /dev/null +++ b/includes/theme-support/class-wc-twenty-sixteen.php @@ -0,0 +1,56 @@ + 250, + 'single_image_width' => 400, + ) + ); + } + + /** + * Open wrappers. + */ + public static function output_content_wrapper() { + echo '
    '; + } + + /** + * Close wrappers. + */ + public static function output_content_wrapper_end() { + echo '
    '; + } +} + +WC_Twenty_Sixteen::init(); diff --git a/includes/theme-support/class-wc-twenty-ten.php b/includes/theme-support/class-wc-twenty-ten.php new file mode 100644 index 0000000..8a9262e --- /dev/null +++ b/includes/theme-support/class-wc-twenty-ten.php @@ -0,0 +1,56 @@ + 200, + 'single_image_width' => 300, + ) + ); + } + + /** + * Open wrappers. + */ + public static function output_content_wrapper() { + echo '
    '; + } + + /** + * Close wrappers. + */ + public static function output_content_wrapper_end() { + echo '
    '; + } +} + +WC_Twenty_Ten::init(); diff --git a/includes/theme-support/class-wc-twenty-thirteen.php b/includes/theme-support/class-wc-twenty-thirteen.php new file mode 100644 index 0000000..4e80b3e --- /dev/null +++ b/includes/theme-support/class-wc-twenty-thirteen.php @@ -0,0 +1,57 @@ + 200, + 'single_image_width' => 300, + ) + ); + } + + /** + * Open wrappers. + */ + public static function output_content_wrapper() { + echo '
    '; + } + + /** + * Close wrappers. + */ + public static function output_content_wrapper_end() { + echo '
    '; + } +} + +WC_Twenty_Thirteen::init(); diff --git a/includes/theme-support/class-wc-twenty-twelve.php b/includes/theme-support/class-wc-twenty-twelve.php new file mode 100644 index 0000000..116dabe --- /dev/null +++ b/includes/theme-support/class-wc-twenty-twelve.php @@ -0,0 +1,57 @@ + 200, + 'single_image_width' => 300, + ) + ); + } + + /** + * Open wrappers. + */ + public static function output_content_wrapper() { + echo '
    '; + } + + /** + * Close wrappers. + */ + public static function output_content_wrapper_end() { + echo '
    '; + } +} + +WC_Twenty_Twelve::init(); diff --git a/includes/theme-support/class-wc-twenty-twenty-one.php b/includes/theme-support/class-wc-twenty-twenty-one.php new file mode 100644 index 0000000..6b568ae --- /dev/null +++ b/includes/theme-support/class-wc-twenty-twenty-one.php @@ -0,0 +1,86 @@ + 450, + 'single_image_width' => 600, + ) + ); + + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_twenty_one_styles', $styles ); + } + + /** + * Enqueue the wp-admin CSS overrides for this theme. + */ + public static function enqueue_admin_styles() { + wp_enqueue_style( + 'woocommerce-twenty-twenty-one-admin', + str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty-one-admin.css', + '', + Constants::get_constant( 'WC_VERSION' ), + 'all' + ); + } + + +} + +WC_Twenty_Twenty_One::init(); diff --git a/includes/theme-support/class-wc-twenty-twenty.php b/includes/theme-support/class-wc-twenty-twenty.php new file mode 100644 index 0000000..47296f6 --- /dev/null +++ b/includes/theme-support/class-wc-twenty-twenty.php @@ -0,0 +1,107 @@ + 450, + 'single_image_width' => 600, + ) + ); + + // Background color change. + add_action( 'after_setup_theme', array( __CLASS__, 'set_white_background' ), 10 ); + + } + + /** + * Open the Twenty Twenty wrapper. + */ + public static function output_content_wrapper() { + echo '
    '; + echo '
    '; + } + + /** + * Close the Twenty Twenty wrapper. + */ + public static function output_content_wrapper_end() { + echo '
    '; + echo '
    '; + } + + /** + * Set background color to white if it's default, otherwise don't touch it. + */ + public static function set_white_background() { + $background = sanitize_hex_color_no_hash( get_theme_mod( 'background_color' ) ); + $background_default = 'f5efe0'; + + // Don't change user's choice of background color. + if ( ! empty( $background ) && $background !== $background_default ) { + return; + } + + // In case default background is found, change it to white. + set_theme_mod( 'background_color', 'fff' ); + } + + /** + * Enqueue CSS for this theme. + * + * @param array $styles Array of registered styles. + * @return array + */ + public static function enqueue_styles( $styles ) { + unset( $styles['woocommerce-general'] ); + + $styles['woocommerce-general'] = array( + 'src' => str_replace( array( 'http:', 'https:' ), '', WC()->plugin_url() ) . '/assets/css/twenty-twenty.css', + 'deps' => '', + 'version' => Constants::get_constant( 'WC_VERSION' ), + 'media' => 'all', + 'has_rtl' => true, + ); + + return apply_filters( 'woocommerce_twenty_twenty_styles', $styles ); + } + +} + +WC_Twenty_Twenty::init(); diff --git a/includes/tracks/class-wc-site-tracking.php b/includes/tracks/class-wc-site-tracking.php new file mode 100644 index 0000000..e924d25 --- /dev/null +++ b/includes/tracks/class-wc-site-tracking.php @@ -0,0 +1,186 @@ + + + + registered['woo-tracks'] ) ) { + return; + } + + $woo_tracks_script = $wp_scripts->registered['woo-tracks']->src; + + ?> + + cap_key ) { + return false; + } + $user_id = $user->ID; + $anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true ); + + // If an id is still not found, create one and save it. + if ( ! $anon_id ) { + $anon_id = self::get_anon_id(); + update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id ); + } + + // Don't set cookie on API requests. + if ( ! Constants::is_true( 'REST_REQUEST' ) && ! Constants::is_true( 'XMLRPC_REQUEST' ) ) { + wc_setcookie( 'tk_ai', $anon_id ); + } + } + + /** + * Record a Tracks event + * + * @param array $event Array of event properties. + * @return bool|WP_Error True on success, WP_Error on failure. + */ + public static function record_event( $event ) { + if ( ! $event instanceof WC_Tracks_Event ) { + $event = new WC_Tracks_Event( $event ); + } + + if ( is_wp_error( $event ) ) { + return $event; + } + + $pixel = $event->build_pixel_url( $event ); + + if ( ! $pixel ) { + return new WP_Error( 'invalid_pixel', 'cannot generate tracks pixel for given input', 400 ); + } + + return self::record_pixel( $pixel ); + } + + /** + * Synchronously request the pixel. + * + * @param string $pixel pixel url and query string. + * @return bool Always returns true. + */ + public static function record_pixel( $pixel ) { + // Add the Request Timestamp and URL terminator just before the HTTP request. + $pixel .= '&_rt=' . self::build_timestamp() . '&_=_'; + + wp_safe_remote_get( + $pixel, + array( + 'blocking' => false, + 'redirection' => 2, + 'httpversion' => '1.1', + 'timeout' => 1, + ) + ); + + return true; + } + + /** + * Create a timestap representing milliseconds since 1970-01-01 + * + * @return string A string representing a timestamp. + */ + public static function build_timestamp() { + $ts = NumberUtil::round( microtime( true ) * 1000 ); + + return number_format( $ts, 0, '', '' ); + } + + /** + * Get a user's identity to send to Tracks. If Jetpack exists, default to its implementation. + * + * @param int $user_id User id. + * @return array Identity properties. + */ + public static function get_identity( $user_id ) { + $jetpack_lib = '/tracks/client.php'; + + if ( class_exists( 'Jetpack' ) && Constants::is_defined( 'JETPACK__VERSION' ) ) { + if ( version_compare( Constants::get_constant( 'JETPACK__VERSION' ), '7.5', '<' ) ) { + if ( file_exists( jetpack_require_lib_dir() . $jetpack_lib ) ) { + include_once jetpack_require_lib_dir() . $jetpack_lib; + if ( function_exists( 'jetpack_tracks_get_identity' ) ) { + return jetpack_tracks_get_identity( $user_id ); + } + } + } else { + $tracking = new Automattic\Jetpack\Tracking(); + return $tracking->tracks_get_identity( $user_id ); + } + } + + // Start with a previously set cookie. + $anon_id = isset( $_COOKIE['tk_ai'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ) : false; + + // If there is no cookie, apply a saved id. + if ( ! $anon_id ) { + $anon_id = get_user_meta( $user_id, '_woocommerce_tracks_anon_id', true ); + } + + // If an id is still not found, create one and save it. + if ( ! $anon_id ) { + $anon_id = self::get_anon_id(); + + update_user_meta( $user_id, '_woocommerce_tracks_anon_id', $anon_id ); + } + + return array( + '_ut' => 'anon', + '_ui' => $anon_id, + ); + } + + /** + * Grabs the user's anon id from cookies, or generates and sets a new one + * + * @return string An anon id for the user + */ + public static function get_anon_id() { + static $anon_id = null; + + if ( ! isset( $anon_id ) ) { + + // Did the browser send us a cookie? + if ( isset( $_COOKIE['tk_ai'] ) ) { + $anon_id = sanitize_text_field( wp_unslash( $_COOKIE['tk_ai'] ) ); + } else { + + $binary = ''; + + // Generate a new anonId and try to save it in the browser's cookies. + // Note that base64-encoding an 18 character string generates a 24-character anon id. + for ( $i = 0; $i < 18; ++$i ) { + $binary .= chr( wp_rand( 0, 255 ) ); + } + + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode + $anon_id = 'woo:' . base64_encode( $binary ); + } + } + + return $anon_id; + } +} + +WC_Tracks_Client::init(); diff --git a/includes/tracks/class-wc-tracks-event.php b/includes/tracks/class-wc-tracks-event.php new file mode 100644 index 0000000..60b89b5 --- /dev/null +++ b/includes/tracks/class-wc-tracks-event.php @@ -0,0 +1,167 @@ +error = $_event; + return; + } + + foreach ( $_event as $key => $value ) { + $this->{$key} = $value; + } + } + + /** + * Record Tracks event + * + * @return bool Always returns true. + */ + public function record() { + if ( wp_doing_ajax() || Constants::is_true( 'REST_REQUEST' ) ) { + return WC_Tracks_Client::record_event( $this ); + } + + return WC_Tracks_Footer_Pixel::record_event( $this ); + } + + /** + * Annotate the event with all relevant info. + * + * @param array $event Event arguments. + * @return bool|WP_Error True on success, WP_Error on failure. + */ + public static function validate_and_sanitize( $event ) { + $event = (object) $event; + + // Required. + if ( ! $event->_en ) { + return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 ); + } + + // Delete non-routable addresses otherwise geoip will discard the record entirely. + if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) { + unset( $event->_via_ip ); + } + + $validated = array( + 'browser_type' => WC_Tracks_Client::BROWSER_TYPE, + ); + + $_event = (object) array_merge( (array) $event, $validated ); + + // If you want to block property names, do it here. + // Make sure we have an event timestamp. + if ( ! isset( $_event->_ts ) ) { + $_event->_ts = WC_Tracks_Client::build_timestamp(); + } + + return $_event; + } + + /** + * Build a pixel URL that will send a Tracks event when fired. + * On error, returns an empty string (''). + * + * @return string A pixel URL or empty string ('') if there were invalid args. + */ + public function build_pixel_url() { + if ( $this->error ) { + return ''; + } + + $args = get_object_vars( $this ); + + // Request Timestamp and URL Terminator must be added just before the HTTP request or not at all. + unset( $args['_rt'], $args['_'] ); + + $validated = self::validate_and_sanitize( $args ); + + if ( is_wp_error( $validated ) ) { + return ''; + } + + return esc_url_raw( WC_Tracks_Client::PIXEL . '?' . http_build_query( $validated ) ); + } + + /** + * Check if event name is valid. + * + * @param string $name Event name. + * @return false|int + */ + public static function event_name_is_valid( $name ) { + return preg_match( self::EVENT_NAME_REGEX, $name ); + } + + /** + * Check if a property name is valid. + * + * @param string $name Event property. + * @return false|int + */ + public static function prop_name_is_valid( $name ) { + return preg_match( self::PROP_NAME_REGEX, $name ); + } + + /** + * Check event names + * + * @param object $event An event object. + */ + public static function scrutinize_event_names( $event ) { + if ( ! self::event_name_is_valid( $event->_en ) ) { + return; + } + + $allowed_key_names = array( + 'anonId', + 'Browser_Type', + ); + + foreach ( array_keys( (array) $event ) as $key ) { + if ( in_array( $key, $allowed_key_names, true ) ) { + continue; + } + if ( ! self::prop_name_is_valid( $key ) ) { + return; + } + } + } +} diff --git a/includes/tracks/class-wc-tracks-footer-pixel.php b/includes/tracks/class-wc-tracks-footer-pixel.php new file mode 100644 index 0000000..3ae6aa2 --- /dev/null +++ b/includes/tracks/class-wc-tracks-footer-pixel.php @@ -0,0 +1,113 @@ +add_event( $event ); + + return true; + } + + /** + * Add a Tracks event to the queue. + * + * @param WC_Tracks_Event $event Event to track. + */ + public function add_event( $event ) { + $this->events[] = $event; + } + + /** + * Add events as tracking pixels to page footer. + */ + public function render_tracking_pixels() { + if ( empty( $this->events ) ) { + return; + } + + foreach ( $this->events as $event ) { + $pixel = $event->build_pixel_url(); + + if ( ! $pixel ) { + continue; + } + + echo ''; + } + + $this->events = array(); + } + + /** + * Fire off API calls for events that weren't converted to pixels. + * + * This handles wp_redirect(). + */ + public function send_tracks_requests() { + if ( empty( $this->events ) ) { + return; + } + + foreach ( $this->events as $event ) { + WC_Tracks_Client::record_event( $event ); + } + } +} diff --git a/includes/tracks/class-wc-tracks.php b/includes/tracks/class-wc-tracks.php new file mode 100644 index 0000000..7528906 --- /dev/null +++ b/includes/tracks/class-wc-tracks.php @@ -0,0 +1,116 @@ + home_url(), + 'blog_lang' => get_user_locale( $user_id ), + 'blog_id' => class_exists( 'Jetpack_Options' ) ? Jetpack_Options::get_option( 'id' ) : null, + 'products_count' => self::get_products_count(), + 'wc_version' => WC()->version, + ); + set_transient( 'wc_tracks_blog_details', $blog_details, DAY_IN_SECONDS ); + } + return $blog_details; + } + + /** + * Gather details from the request to the server. + * + * @return array Server details. + */ + public static function get_server_details() { + $data = array(); + + $data['_via_ua'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; + $data['_via_ip'] = isset( $_SERVER['REMOTE_ADDR'] ) ? wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; + $data['_lg'] = isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) : ''; + $data['_dr'] = isset( $_SERVER['HTTP_REFERER'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : ''; + + $uri = isset( $_SERVER['REQUEST_URI'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + $host = isset( $_SERVER['HTTP_HOST'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; + $data['_dl'] = isset( $_SERVER['REQUEST_SCHEME'] ) ? wc_clean( wp_unslash( $_SERVER['REQUEST_SCHEME'] ) ) . '://' . $host . $uri : ''; + + return $data; + } + + /** + * Record an event in Tracks - this is the preferred way to record events from PHP. + * + * @param string $event_name The name of the event. + * @param array $properties Custom properties to send with the event. + * @return bool|WP_Error True for success or WP_Error if the event pixel could not be fired. + */ + public static function record_event( $event_name, $properties = array() ) { + /** + * Don't track users who don't have tracking enabled. + */ + if ( ! WC_Site_Tracking::is_tracking_enabled() ) { + return false; + } + + $user = wp_get_current_user(); + + // We don't want to track user events during unit tests/CI runs. + if ( $user instanceof WP_User && 'wptests_capabilities' === $user->cap_key ) { + return false; + } + $prefixed_event_name = self::PREFIX . $event_name; + + $data = array( + '_en' => $prefixed_event_name, + '_ts' => WC_Tracks_Client::build_timestamp(), + ); + + $server_details = self::get_server_details(); + $identity = WC_Tracks_Client::get_identity( $user->ID ); + $blog_details = self::get_blog_details( $user->ID ); + + // Allow event props to be filtered to enable adding site-wide props. + $filtered_properties = apply_filters( 'woocommerce_tracks_event_properties', $properties, $prefixed_event_name ); + + // Delete _ui and _ut protected properties. + unset( $filtered_properties['_ui'] ); + unset( $filtered_properties['_ut'] ); + + $event_obj = new WC_Tracks_Event( array_merge( $data, $server_details, $identity, $blog_details, $filtered_properties ) ); + + if ( is_wp_error( $event_obj->error ) ) { + return $event_obj->error; + } + + return $event_obj->record(); + } +} diff --git a/includes/tracks/events/class-wc-admin-setup-wizard-tracking.php b/includes/tracks/events/class-wc-admin-setup-wizard-tracking.php new file mode 100644 index 0000000..158904c --- /dev/null +++ b/includes/tracks/events/class-wc-admin-setup-wizard-tracking.php @@ -0,0 +1,176 @@ +queue as $script ) { + if ( in_array( $script, $allowed, true ) ) { + continue; + } + wp_dequeue_script( $script ); + } + } + + /** + * Track when tracking is opted into and OBW has started. + * + * @param string $option Option name. + * @param string $value Option value. + * + * @deprecated 4.6.0 + */ + public function track_start( $option, $value ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Track the marketing form on submit. + * + * @deprecated 4.6.0 + */ + public function track_ready_next_steps() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Track various events when a step is saved. + * + * @deprecated 4.6.0 + */ + public function add_step_save_events() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Track store setup and store properties on save. + * + * @deprecated 4.6.0 + */ + public function track_store_setup() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Track payment gateways selected. + * + * @deprecated 4.6.0 + */ + public function track_payments() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Track shipping units and whether or not labels are set. + * + * @deprecated 4.6.0 + */ + public function track_shipping() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Track recommended plugins selected for install. + * + * @deprecated 4.6.0 + */ + public function track_recommended() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Tracks when Jetpack is activated through the OBW. + * + * @deprecated 4.6.0 + */ + public function track_jetpack_activate() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Tracks when last next_steps screen is viewed in the OBW. + * + * @deprecated 4.6.0 + */ + public function track_next_steps() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Track skipped steps. + * + * @deprecated 4.6.0 + */ + public function track_skip_step() { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + } + + /** + * Set the OBW steps inside this class instance. + * + * @param array $steps Array of OBW steps. + * + * @deprecated 4.6.0 + */ + public function set_obw_steps( $steps ) { + _deprecated_function( __CLASS__ . '::' . __FUNCTION__, '4.6.0', __( 'Onboarding is maintained in WooCommerce Admin.', 'woocommerce' ) ); + $this->steps = $steps; + + return $steps; + } +} diff --git a/includes/tracks/events/class-wc-coupon-tracking.php b/includes/tracks/events/class-wc-coupon-tracking.php new file mode 100644 index 0000000..c5faffa --- /dev/null +++ b/includes/tracks/events/class-wc-coupon-tracking.php @@ -0,0 +1,39 @@ + $coupon->get_code(), + 'free_shipping' => $coupon->get_free_shipping(), + 'individual_use' => $coupon->get_individual_use(), + 'exclude_sale_items' => $coupon->get_exclude_sale_items(), + 'usage_limits_applied' => 0 < intval( $coupon->get_usage_limit() ) + || 0 < intval( $coupon->get_usage_limit_per_user() ) + || 0 < intval( $coupon->get_limit_usage_to_x_items() ), + ); + + WC_Tracks::record_event( 'coupon_updated', $properties ); + } +} diff --git a/includes/tracks/events/class-wc-coupons-tracking.php b/includes/tracks/events/class-wc-coupons-tracking.php new file mode 100644 index 0000000..cc32e40 --- /dev/null +++ b/includes/tracks/events/class-wc-coupons-tracking.php @@ -0,0 +1,73 @@ +tracks_coupons_bulk_actions(); + + WC_Tracks::record_event( + 'coupons_view', + array( + 'status' => isset( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : 'all', + ) + ); + + if ( isset( $_GET['filter_action'] ) && 'Filter' === sanitize_text_field( wp_unslash( $_GET['filter_action'] ) ) && isset( $_GET['coupon_type'] ) ) { + WC_Tracks::record_event( + 'coupons_filter', + array( + 'filter' => 'coupon_type', + 'value' => sanitize_text_field( wp_unslash( $_GET['coupon_type'] ) ), + ) + ); + } + + if ( isset( $_GET['s'] ) && 0 < strlen( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) ) { + WC_Tracks::record_event( 'coupons_search' ); + } + } + } +} diff --git a/includes/tracks/events/class-wc-extensions-tracking.php b/includes/tracks/events/class-wc-extensions-tracking.php new file mode 100644 index 0000000..498e554 --- /dev/null +++ b/includes/tracks/events/class-wc-extensions-tracking.php @@ -0,0 +1,101 @@ + empty( $_REQUEST['section'] ) ? '_featured' : wc_clean( wp_unslash( $_REQUEST['section'] ) ), + ); + + $event = 'extensions_view'; + if ( 'helper' === $properties['section'] ) { + $event = 'subscriptions_view'; + } + + if ( ! empty( $_REQUEST['search'] ) ) { + $event = 'extensions_view_search'; + $properties['search_term'] = wc_clean( wp_unslash( $_REQUEST['search'] ) ); + } + // phpcs:enable + + WC_Tracks::record_event( $event, $properties ); + } + + /** + * Send a Tracks even when a Helper connection process is initiated. + */ + public function track_helper_connection_start() { + WC_Tracks::record_event( 'extensions_subscriptions_connect' ); + } + + /** + * Send a Tracks even when a Helper connection process is cancelled. + */ + public function track_helper_connection_cancelled() { + WC_Tracks::record_event( 'extensions_subscriptions_cancelled' ); + } + + /** + * Send a Tracks even when a Helper connection process completed successfully. + */ + public function track_helper_connection_complete() { + WC_Tracks::record_event( 'extensions_subscriptions_connected' ); + } + + /** + * Send a Tracks even when a Helper has been disconnected. + */ + public function track_helper_disconnected() { + WC_Tracks::record_event( 'extensions_subscriptions_disconnect' ); + } + + /** + * Send a Tracks even when Helper subscriptions are refreshed. + */ + public function track_helper_subscriptions_refresh() { + WC_Tracks::record_event( 'extensions_subscriptions_update' ); + } + + /** + * Send a Tracks event when addon is installed via the Extensions page. + * + * @param string $addon_id Addon slug. + * @param string $section Extensions tab. + */ + public function track_addon_install( $addon_id, $section ) { + $properties = array( + 'context' => 'extensions', + 'section' => $section, + ); + + if ( 'woocommerce-payments' === $addon_id ) { + WC_Tracks::record_event( 'woocommerce_payments_install', $properties ); + } + } +} diff --git a/includes/tracks/events/class-wc-importer-tracking.php b/includes/tracks/events/class-wc-importer-tracking.php new file mode 100644 index 0000000..648424c --- /dev/null +++ b/includes/tracks/events/class-wc-importer-tracking.php @@ -0,0 +1,83 @@ +track_product_importer_start(); + } + + if ( 'done' === $_REQUEST['step'] ) { + return $this->track_product_importer_complete(); + } + // phpcs:enable + } + + /** + * Send a Tracks event when the product importer is started. + * + * @return void + */ + public function track_product_importer_start() { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( ! isset( $_REQUEST['file'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) { + return; + } + + $properties = array( + 'update_existing' => isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false, + 'delimiter' => empty( $_REQUEST['delimiter'] ) ? ',' : wc_clean( wp_unslash( $_REQUEST['delimiter'] ) ), + ); + // phpcs:enable + + WC_Tracks::record_event( 'product_import_start', $properties ); + } + + /** + * Send a Tracks event when the product importer has finished. + * + * @return void + */ + public function track_product_importer_complete() { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( ! isset( $_REQUEST['nonce'] ) ) { + return; + } + + $properties = array( + '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, + ); + // phpcs:enable + + WC_Tracks::record_event( 'product_import_complete', $properties ); + } +} diff --git a/includes/tracks/events/class-wc-order-tracking.php b/includes/tracks/events/class-wc-order-tracking.php new file mode 100644 index 0000000..9035cb2 --- /dev/null +++ b/includes/tracks/events/class-wc-order-tracking.php @@ -0,0 +1,40 @@ +get_id() ) { + return; + } + $properties = array( + 'current_status' => $order->get_status(), + 'date_created' => $order->get_date_created() ? $order->get_date_created()->format( DateTime::ATOM ) : '', + 'payment_method' => $order->get_payment_method(), + ); + + WC_Tracks::record_event( 'single_order_view', $properties ); + } +} + diff --git a/includes/tracks/events/class-wc-orders-tracking.php b/includes/tracks/events/class-wc-orders-tracking.php new file mode 100644 index 0000000..a889074 --- /dev/null +++ b/includes/tracks/events/class-wc-orders-tracking.php @@ -0,0 +1,178 @@ +id ) { + // we are on the order listing page, and query results are being shown. + WC_Tracks::record_event( 'orders_view_search' ); + } + + return $order_ids; + } + + /** + * Send a Tracks event when the Orders page is viewed. + */ + public function track_orders_view() { + if ( isset( $_GET['post_type'] ) && 'shop_order' === wp_unslash( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + + // phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput + $properties = array( + 'status' => isset( $_GET['post_status'] ) ? sanitize_text_field( $_GET['post_status'] ) : 'all', + ); + // phpcs:enable + + WC_Tracks::record_event( 'orders_view', $properties ); + } + } + + /** + * Send a Tracks event when an order status is changed. + * + * @param int $id Order id. + * @param string $previous_status the old WooCommerce order status. + * @param string $next_status the new WooCommerce order status. + */ + public function track_order_status_change( $id, $previous_status, $next_status ) { + $order = wc_get_order( $id ); + + $properties = array( + 'order_id' => $id, + 'next_status' => $next_status, + 'previous_status' => $previous_status, + 'date_created' => $order->get_date_created() ? $order->get_date_created()->date( 'Y-m-d' ) : '', + 'payment_method' => $order->get_payment_method(), + 'order_total' => $order->get_total(), + ); + + WC_Tracks::record_event( 'orders_edit_status_change', $properties ); + } + + /** + * Send a Tracks event when an order date is changed. + * + * @param int $id Order id. + */ + public function track_created_date_change( $id ) { + $post_type = get_post_type( $id ); + + if ( 'shop_order' !== $post_type ) { + return; + } + + if ( 'auto-draft' === get_post_status( $id ) ) { + return; + } + + $order = wc_get_order( $id ); + $date_created = $order->get_date_created() ? $order->get_date_created()->date( 'Y-m-d H:i:s' ) : ''; + // phpcs:disable WordPress.Security.NonceVerification + $new_date = sprintf( + '%s %2d:%2d:%2d', + isset( $_POST['order_date'] ) ? wc_clean( wp_unslash( $_POST['order_date'] ) ) : '', + isset( $_POST['order_date_hour'] ) ? wc_clean( wp_unslash( $_POST['order_date_hour'] ) ) : '', + isset( $_POST['order_date_minute'] ) ? wc_clean( wp_unslash( $_POST['order_date_minute'] ) ) : '', + isset( $_POST['order_date_second'] ) ? wc_clean( wp_unslash( $_POST['order_date_second'] ) ) : '' + ); + // phpcs:enable + + if ( $new_date !== $date_created ) { + $properties = array( + 'order_id' => $id, + 'status' => $order->get_status(), + ); + + WC_Tracks::record_event( 'order_edit_date_created', $properties ); + } + } + + /** + * Track order actions taken. + * + * @param int $order_id Order ID. + */ + public function track_order_action( $order_id ) { + // phpcs:disable WordPress.Security.NonceVerification + if ( ! empty( $_POST['wc_order_action'] ) ) { + $order = wc_get_order( $order_id ); + $action = wc_clean( wp_unslash( $_POST['wc_order_action'] ) ); + $properties = array( + 'order_id' => $order_id, + 'status' => $order->get_status(), + 'action' => $action, + ); + + WC_Tracks::record_event( 'order_edit_order_action', $properties ); + } + // phpcs:enable + } + + /** + * Track "add order" button on the Edit Order screen. + */ + public function track_add_order_from_edit() { + // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + if ( isset( $_GET['post_type'] ) && 'shop_order' === wp_unslash( $_GET['post_type'] ) ) { + $referer = wp_get_referer(); + + if ( $referer ) { + $referring_page = wp_parse_url( $referer ); + $referring_args = array(); + $post_edit_page = wp_parse_url( admin_url( 'post.php' ) ); + + if ( ! empty( $referring_page['query'] ) ) { + parse_str( $referring_page['query'], $referring_args ); + } + + // Determine if we arrived from an Order Edit screen. + if ( + $post_edit_page['path'] === $referring_page['path'] && + isset( $referring_args['action'] ) && + 'edit' === $referring_args['action'] && + isset( $referring_args['post'] ) && + 'shop_order' === get_post_type( $referring_args['post'] ) + ) { + WC_Tracks::record_event( 'order_edit_add_order' ); + } + } + } + } +} diff --git a/includes/tracks/events/class-wc-products-tracking.php b/includes/tracks/events/class-wc-products-tracking.php new file mode 100644 index 0000000..ec68dfa --- /dev/null +++ b/includes/tracks/events/class-wc-products-tracking.php @@ -0,0 +1,211 @@ +post_type ) { + return; + } + + $properties = array( + 'product_id' => $product_id, + ); + + WC_Tracks::record_event( 'product_edit', $properties ); + } + + /** + * Track the Update button being clicked on the client side. + * This is needed because `track_product_updated` (using the `edit_post` + * hook) is called in response to a number of other triggers. + * + * @param WP_Post $post The post, not used. + */ + public function track_product_updated_client_side( $post ) { + wc_enqueue_js( + " + if ( $( 'h1.wp-heading-inline' ).text().trim() === '" . __( 'Edit product', 'woocommerce' ) . "') { + var initialStockValue = $( '#_stock' ).val(); + var hasRecordedEvent = false; + + $( '#publish' ).on( 'click', function() { + if ( hasRecordedEvent ) { + return; + } + + var currentStockValue = $( '#_stock' ).val(); + var properties = { + product_type: $( '#product-type' ).val(), + is_virtual: $( '#_virtual' ).is( ':checked' ) ? 'Y' : 'N', + is_downloadable: $( '#_downloadable' ).is( ':checked' ) ? 'Y' : 'N', + manage_stock: $( '#_manage_stock' ).is( ':checked' ) ? 'Y' : 'N', + stock_quantity_update: ( initialStockValue != currentStockValue ) ? 'Y' : 'N', + }; + + window.wcTracks.recordEvent( 'product_update', properties ); + hasRecordedEvent = true; + } ); + } + " + ); + } + + /** + * Send a Tracks event when a product is published. + * + * @param string $new_status New post_status. + * @param string $old_status Previous post_status. + * @param object $post WordPress post. + */ + public function track_product_published( $new_status, $old_status, $post ) { + if ( + 'product' !== $post->post_type || + 'publish' !== $new_status || + 'publish' === $old_status + ) { + return; + } + + $properties = array( + 'product_id' => $post->ID, + ); + + WC_Tracks::record_event( 'product_add_publish', $properties ); + } + + /** + * Send a Tracks event when a product category is created. + * + * @param int $category_id Category ID. + */ + public function track_product_category_created( $category_id ) { + // phpcs:disable WordPress.Security.NonceVerification.Missing + // Only track category creation from the edit product screen or the + // category management screen (which both occur via AJAX). + if ( + ! Constants::is_defined( 'DOING_AJAX' ) || + empty( $_POST['action'] ) || + ( + // Product Categories screen. + 'add-tag' !== $_POST['action'] && + // Edit Product screen. + 'add-product_cat' !== $_POST['action'] + ) + ) { + return; + } + + $category = get_term( $category_id, 'product_cat' ); + $properties = array( + 'category_id' => $category_id, + 'parent_id' => $category->parent, + 'page' => ( 'add-tag' === $_POST['action'] ) ? 'categories' : 'product', + ); + // phpcs:enable + + WC_Tracks::record_event( 'product_category_add', $properties ); + } +} diff --git a/includes/tracks/events/class-wc-settings-tracking.php b/includes/tracks/events/class-wc-settings-tracking.php new file mode 100644 index 0000000..c713db5 --- /dev/null +++ b/includes/tracks/events/class-wc-settings-tracking.php @@ -0,0 +1,118 @@ +allowed_options[] = $option['id']; + + // Delay attaching this action since it could get fired a lot. + if ( false === has_action( 'update_option', array( $this, 'track_setting_change' ) ) ) { + add_action( 'update_option', array( $this, 'track_setting_change' ), 10, 3 ); + } + } + + /** + * Add WooCommerce option to a list of updated options. + * + * @param string $option_name Option being updated. + * @param mixed $old_value Old value of option. + * @param mixed $new_value New value of option. + */ + public function track_setting_change( $option_name, $old_value, $new_value ) { + // Make sure this is a WooCommerce option. + if ( ! in_array( $option_name, $this->allowed_options, true ) ) { + return; + } + + // Check to make sure the new value is truly different. + // `woocommerce_price_num_decimals` tends to trigger this + // because form values aren't coerced (e.g. '2' vs. 2). + if ( + is_scalar( $old_value ) && + is_scalar( $new_value ) && + (string) $old_value === (string) $new_value + ) { + return; + } + + $this->updated_options[] = $option_name; + } + + /** + * Send a Tracks event for WooCommerce options that changed values. + */ + public function send_settings_change_event() { + global $current_tab; + + if ( empty( $this->updated_options ) ) { + return; + } + + $properties = array( + 'settings' => implode( ',', $this->updated_options ), + ); + + if ( isset( $current_tab ) ) { + $properties['tab'] = $current_tab; + } + + WC_Tracks::record_event( 'settings_change', $properties ); + } + + /** + * Send a Tracks event for WooCommerce settings page views. + */ + public function track_settings_page_view() { + global $current_tab, $current_section; + + $properties = array( + 'tab' => $current_tab, + 'section' => empty( $current_section ) ? null : $current_section, + ); + + WC_Tracks::record_event( 'settings_view', $properties ); + } +} diff --git a/includes/tracks/events/class-wc-status-tracking.php b/includes/tracks/events/class-wc-status-tracking.php new file mode 100644 index 0000000..839427a --- /dev/null +++ b/includes/tracks/events/class-wc-status-tracking.php @@ -0,0 +1,48 @@ + $tab, + 'tool_used' => isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : null, + ) + ); + + if ( 'status' === $tab ) { + wc_enqueue_js( + " + $( 'a.debug-report' ).on( 'click', function() { + window.wcTracks.recordEvent( 'status_view_reports' ); + } ); + " + ); + } + } + } +} diff --git a/includes/traits/trait-wc-item-totals.php b/includes/traits/trait-wc-item-totals.php new file mode 100644 index 0000000..4bc8c9c --- /dev/null +++ b/includes/traits/trait-wc-item-totals.php @@ -0,0 +1,91 @@ + 'parent', + 'id' => 'term_id', + 'slug' => 'slug', + ); + + /** + * Starts the list before the elements are added. + * + * @see Walker::start_el() + * @since 2.1.0 + * + * @param string $output Passed by reference. Used to append additional content. + * @param object $cat Category. + * @param int $depth Depth of category in reference to parents. + * @param array $args Arguments. + * @param int $current_object_id Current object ID. + */ + public function start_el( &$output, $cat, $depth = 0, $args = array(), $current_object_id = 0 ) { + + if ( ! empty( $args['hierarchical'] ) ) { + $pad = str_repeat( ' ', $depth * 3 ); + } else { + $pad = ''; + } + + $cat_name = apply_filters( 'list_product_cats', $cat->name, $cat ); + $value = ( isset( $args['value'] ) && 'id' === $args['value'] ) ? $cat->term_id : $cat->slug; + $output .= "\t\n"; + } + + /** + * Traverse elements to create list from elements. + * + * Display one element if the element doesn't have any children otherwise, + * display the element and its children. Will only traverse up to the max. + * depth and no ignore elements under that depth. It is possible to set the. + * max depth to include all depths, see walk() method. + * + * This method shouldn't be called directly, use the walk() method instead. + * + * @since 2.5.0 + * + * @param object $element Data object. + * @param array $children_elements List of elements to continue traversing. + * @param int $max_depth Max depth to traverse. + * @param int $depth Depth of current element. + * @param array $args Arguments. + * @param string $output Passed by reference. Used to append additional content. + * @return null Null on failure with no changes to parameters. + */ + public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) { + if ( ! $element || ( 0 === $element->count && ! empty( $args[0]['hide_empty'] ) ) ) { + return; + } + parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output ); + } +} diff --git a/includes/walkers/class-wc-product-cat-list-walker.php b/includes/walkers/class-wc-product-cat-list-walker.php new file mode 100644 index 0000000..3580184 --- /dev/null +++ b/includes/walkers/class-wc-product-cat-list-walker.php @@ -0,0 +1,153 @@ + 'parent', + 'id' => 'term_id', + 'slug' => 'slug', + ); + + /** + * Starts the list before the elements are added. + * + * @see Walker::start_lvl() + * @since 2.1.0 + * + * @param string $output Passed by reference. Used to append additional content. + * @param int $depth Depth of category. Used for tab indentation. + * @param array $args Will only append content if style argument value is 'list'. + */ + public function start_lvl( &$output, $depth = 0, $args = array() ) { + if ( 'list' !== $args['style'] ) { + return; + } + + $indent = str_repeat( "\t", $depth ); + $output .= "$indent
      \n"; + } + + /** + * Ends the list of after the elements are added. + * + * @see Walker::end_lvl() + * @since 2.1.0 + * + * @param string $output Passed by reference. Used to append additional content. + * @param int $depth Depth of category. Used for tab indentation. + * @param array $args Will only append content if style argument value is 'list'. + */ + public function end_lvl( &$output, $depth = 0, $args = array() ) { + if ( 'list' !== $args['style'] ) { + return; + } + + $indent = str_repeat( "\t", $depth ); + $output .= "$indent
    \n"; + } + + /** + * Start the element output. + * + * @see Walker::start_el() + * @since 2.1.0 + * + * @param string $output Passed by reference. Used to append additional content. + * @param object $cat Category. + * @param int $depth Depth of category in reference to parents. + * @param array $args Arguments. + * @param integer $current_object_id Current object ID. + */ + public function start_el( &$output, $cat, $depth = 0, $args = array(), $current_object_id = 0 ) { + $cat_id = intval( $cat->term_id ); + + $output .= '
  • ' . apply_filters( 'list_product_cats', $cat->name, $cat ) . ''; + + if ( $args['show_count'] ) { + $output .= ' (' . $cat->count . ')'; + } + } + + /** + * Ends the element output, if needed. + * + * @see Walker::end_el() + * @since 2.1.0 + * + * @param string $output Passed by reference. Used to append additional content. + * @param object $cat Category. + * @param int $depth Depth of category. Not used. + * @param array $args Only uses 'list' for whether should append to output. + */ + public function end_el( &$output, $cat, $depth = 0, $args = array() ) { + $output .= "
  • \n"; + } + + /** + * Traverse elements to create list from elements. + * + * Display one element if the element doesn't have any children otherwise, + * display the element and its children. Will only traverse up to the max. + * depth and no ignore elements under that depth. It is possible to set the. + * max depth to include all depths, see walk() method. + * + * This method shouldn't be called directly, use the walk() method instead. + * + * @since 2.5.0 + * + * @param object $element Data object. + * @param array $children_elements List of elements to continue traversing. + * @param int $max_depth Max depth to traverse. + * @param int $depth Depth of current element. + * @param array $args Arguments. + * @param string $output Passed by reference. Used to append additional content. + * @return null Null on failure with no changes to parameters. + */ + public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) { + if ( ! $element || ( 0 === $element->count && ! empty( $args[0]['hide_empty'] ) ) ) { + return; + } + parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output ); + } +} diff --git a/includes/wc-account-functions.php b/includes/wc-account-functions.php new file mode 100644 index 0000000..2d67cad --- /dev/null +++ b/includes/wc-account-functions.php @@ -0,0 +1,422 @@ + 0; + $lost_password_endpoint = get_option( 'woocommerce_myaccount_lost_password_endpoint' ); + + if ( $wc_account_page_exists && ! empty( $lost_password_endpoint ) ) { + return wc_get_endpoint_url( $lost_password_endpoint, '', $wc_account_page_url ); + } else { + return $default_url; + } +} + +add_filter( 'lostpassword_url', 'wc_lostpassword_url', 10, 1 ); + +/** + * Get the link to the edit account details page. + * + * @return string + */ +function wc_customer_edit_account_url() { + $edit_account_url = wc_get_endpoint_url( 'edit-account', '', wc_get_page_permalink( 'myaccount' ) ); + + return apply_filters( 'woocommerce_customer_edit_account_url', $edit_account_url ); +} + +/** + * Get the edit address slug translation. + * + * @param string $id Address ID. + * @param bool $flip Flip the array to make it possible to retrieve the values ​​from both sides. + * + * @return string Address slug i18n. + */ +function wc_edit_address_i18n( $id, $flip = false ) { + $slugs = apply_filters( + 'woocommerce_edit_address_slugs', + array( + 'billing' => sanitize_title( _x( 'billing', 'edit-address-slug', 'woocommerce' ) ), + 'shipping' => sanitize_title( _x( 'shipping', 'edit-address-slug', 'woocommerce' ) ), + ) + ); + + if ( $flip ) { + $slugs = array_flip( $slugs ); + } + + if ( ! isset( $slugs[ $id ] ) ) { + return $id; + } + + return $slugs[ $id ]; +} + +/** + * Get My Account menu items. + * + * @since 2.6.0 + * @return array + */ +function wc_get_account_menu_items() { + $endpoints = array( + 'orders' => get_option( 'woocommerce_myaccount_orders_endpoint', 'orders' ), + 'downloads' => get_option( 'woocommerce_myaccount_downloads_endpoint', 'downloads' ), + 'edit-address' => get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ), + 'payment-methods' => get_option( 'woocommerce_myaccount_payment_methods_endpoint', 'payment-methods' ), + 'edit-account' => get_option( 'woocommerce_myaccount_edit_account_endpoint', 'edit-account' ), + 'customer-logout' => get_option( 'woocommerce_logout_endpoint', 'customer-logout' ), + ); + + $items = array( + 'dashboard' => __( 'Dashboard', 'woocommerce' ), + 'orders' => __( 'Orders', 'woocommerce' ), + 'downloads' => __( 'Downloads', 'woocommerce' ), + 'edit-address' => _n( 'Addresses', 'Address', (int) wc_shipping_enabled(), 'woocommerce' ), + 'payment-methods' => __( 'Payment methods', 'woocommerce' ), + 'edit-account' => __( 'Account details', 'woocommerce' ), + 'customer-logout' => __( 'Logout', 'woocommerce' ), + ); + + // Remove missing endpoints. + foreach ( $endpoints as $endpoint_id => $endpoint ) { + if ( empty( $endpoint ) ) { + unset( $items[ $endpoint_id ] ); + } + } + + // Check if payment gateways support add new payment methods. + if ( isset( $items['payment-methods'] ) ) { + $support_payment_methods = false; + foreach ( WC()->payment_gateways->get_available_payment_gateways() as $gateway ) { + if ( $gateway->supports( 'add_payment_method' ) || $gateway->supports( 'tokenization' ) ) { + $support_payment_methods = true; + break; + } + } + + if ( ! $support_payment_methods ) { + unset( $items['payment-methods'] ); + } + } + + return apply_filters( 'woocommerce_account_menu_items', $items, $endpoints ); +} + +/** + * Get account menu item classes. + * + * @since 2.6.0 + * @param string $endpoint Endpoint. + * @return string + */ +function wc_get_account_menu_item_classes( $endpoint ) { + global $wp; + + $classes = array( + 'woocommerce-MyAccount-navigation-link', + 'woocommerce-MyAccount-navigation-link--' . $endpoint, + ); + + // Set current item class. + $current = isset( $wp->query_vars[ $endpoint ] ); + if ( 'dashboard' === $endpoint && ( isset( $wp->query_vars['page'] ) || empty( $wp->query_vars ) ) ) { + $current = true; // Dashboard is not an endpoint, so needs a custom check. + } elseif ( 'orders' === $endpoint && isset( $wp->query_vars['view-order'] ) ) { + $current = true; // When looking at individual order, highlight Orders list item (to signify where in the menu the user currently is). + } elseif ( 'payment-methods' === $endpoint && isset( $wp->query_vars['add-payment-method'] ) ) { + $current = true; + } + + if ( $current ) { + $classes[] = 'is-active'; + } + + $classes = apply_filters( 'woocommerce_account_menu_item_classes', $classes, $endpoint ); + + return implode( ' ', array_map( 'sanitize_html_class', $classes ) ); +} + +/** + * Get account endpoint URL. + * + * @since 2.6.0 + * @param string $endpoint Endpoint. + * @return string + */ +function wc_get_account_endpoint_url( $endpoint ) { + if ( 'dashboard' === $endpoint ) { + return wc_get_page_permalink( 'myaccount' ); + } + + if ( 'customer-logout' === $endpoint ) { + return wc_logout_url(); + } + + return wc_get_endpoint_url( $endpoint, '', wc_get_page_permalink( 'myaccount' ) ); +} + +/** + * Get My Account > Orders columns. + * + * @since 2.6.0 + * @return array + */ +function wc_get_account_orders_columns() { + $columns = apply_filters( + 'woocommerce_account_orders_columns', + array( + 'order-number' => __( 'Order', 'woocommerce' ), + 'order-date' => __( 'Date', 'woocommerce' ), + 'order-status' => __( 'Status', 'woocommerce' ), + 'order-total' => __( 'Total', 'woocommerce' ), + 'order-actions' => __( 'Actions', 'woocommerce' ), + ) + ); + + // Deprecated filter since 2.6.0. + return apply_filters( 'woocommerce_my_account_my_orders_columns', $columns ); +} + +/** + * Get My Account > Downloads columns. + * + * @since 2.6.0 + * @return array + */ +function wc_get_account_downloads_columns() { + $columns = apply_filters( + 'woocommerce_account_downloads_columns', + array( + 'download-product' => __( 'Product', 'woocommerce' ), + 'download-remaining' => __( 'Downloads remaining', 'woocommerce' ), + 'download-expires' => __( 'Expires', 'woocommerce' ), + 'download-file' => __( 'Download', 'woocommerce' ), + 'download-actions' => ' ', + ) + ); + + if ( ! has_filter( 'woocommerce_account_download_actions' ) ) { + unset( $columns['download-actions'] ); + } + + return $columns; +} + +/** + * Get My Account > Payment methods columns. + * + * @since 2.6.0 + * @return array + */ +function wc_get_account_payment_methods_columns() { + return apply_filters( + 'woocommerce_account_payment_methods_columns', + array( + 'method' => __( 'Method', 'woocommerce' ), + 'expires' => __( 'Expires', 'woocommerce' ), + 'actions' => ' ', + ) + ); +} + +/** + * Get My Account > Payment methods types + * + * @since 2.6.0 + * @return array + */ +function wc_get_account_payment_methods_types() { + return apply_filters( + 'woocommerce_payment_methods_types', + array( + 'cc' => __( 'Credit card', 'woocommerce' ), + 'echeck' => __( 'eCheck', 'woocommerce' ), + ) + ); +} + +/** + * Get account orders actions. + * + * @since 3.2.0 + * @param int|WC_Order $order Order instance or ID. + * @return array + */ +function wc_get_account_orders_actions( $order ) { + if ( ! is_object( $order ) ) { + $order_id = absint( $order ); + $order = wc_get_order( $order_id ); + } + + $actions = array( + 'pay' => array( + 'url' => $order->get_checkout_payment_url(), + 'name' => __( 'Pay', 'woocommerce' ), + ), + 'view' => array( + 'url' => $order->get_view_order_url(), + 'name' => __( 'View', 'woocommerce' ), + ), + 'cancel' => array( + 'url' => $order->get_cancel_order_url( wc_get_page_permalink( 'myaccount' ) ), + 'name' => __( 'Cancel', 'woocommerce' ), + ), + ); + + if ( ! $order->needs_payment() ) { + unset( $actions['pay'] ); + } + + if ( ! in_array( $order->get_status(), apply_filters( 'woocommerce_valid_order_statuses_for_cancel', array( 'pending', 'failed' ), $order ), true ) ) { + unset( $actions['cancel'] ); + } + + return apply_filters( 'woocommerce_my_account_my_orders_actions', $actions, $order ); +} + +/** + * Get account formatted address. + * + * @since 3.2.0 + * @param string $address_type Address type. + * Accepts: 'billing' or 'shipping'. + * Default to 'billing'. + * @param int $customer_id Customer ID. + * Default to 0. + * @return string + */ +function wc_get_account_formatted_address( $address_type = 'billing', $customer_id = 0 ) { + $getter = "get_{$address_type}"; + $address = array(); + + if ( 0 === $customer_id ) { + $customer_id = get_current_user_id(); + } + + $customer = new WC_Customer( $customer_id ); + + if ( is_callable( array( $customer, $getter ) ) ) { + $address = $customer->$getter(); + unset( $address['email'], $address['tel'] ); + } + + return WC()->countries->get_formatted_address( apply_filters( 'woocommerce_my_account_my_address_formatted_address', $address, $customer->get_id(), $address_type ) ); +} + +/** + * Returns an array of a user's saved payments list for output on the account tab. + * + * @since 2.6 + * @param array $list List of payment methods passed from wc_get_customer_saved_methods_list(). + * @param int $customer_id The customer to fetch payment methods for. + * @return array Filtered list of customers payment methods. + */ +function wc_get_account_saved_payment_methods_list( $list, $customer_id ) { + $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id ); + foreach ( $payment_tokens as $payment_token ) { + $delete_url = wc_get_endpoint_url( 'delete-payment-method', $payment_token->get_id() ); + $delete_url = wp_nonce_url( $delete_url, 'delete-payment-method-' . $payment_token->get_id() ); + $set_default_url = wc_get_endpoint_url( 'set-default-payment-method', $payment_token->get_id() ); + $set_default_url = wp_nonce_url( $set_default_url, 'set-default-payment-method-' . $payment_token->get_id() ); + + $type = strtolower( $payment_token->get_type() ); + $list[ $type ][] = array( + 'method' => array( + 'gateway' => $payment_token->get_gateway_id(), + ), + 'expires' => esc_html__( 'N/A', 'woocommerce' ), + 'is_default' => $payment_token->is_default(), + 'actions' => array( + 'delete' => array( + 'url' => $delete_url, + 'name' => esc_html__( 'Delete', 'woocommerce' ), + ), + ), + ); + $key = key( array_slice( $list[ $type ], -1, 1, true ) ); + + if ( ! $payment_token->is_default() ) { + $list[ $type ][ $key ]['actions']['default'] = array( + 'url' => $set_default_url, + 'name' => esc_html__( 'Make default', 'woocommerce' ), + ); + } + + $list[ $type ][ $key ] = apply_filters( 'woocommerce_payment_methods_list_item', $list[ $type ][ $key ], $payment_token ); + } + return $list; +} + +add_filter( 'woocommerce_saved_payment_methods_list', 'wc_get_account_saved_payment_methods_list', 10, 2 ); + +/** + * Controls the output for credit cards on the my account page. + * + * @since 2.6 + * @param array $item Individual list item from woocommerce_saved_payment_methods_list. + * @param WC_Payment_Token $payment_token The payment token associated with this method entry. + * @return array Filtered item. + */ +function wc_get_account_saved_payment_methods_list_item_cc( $item, $payment_token ) { + if ( 'cc' !== strtolower( $payment_token->get_type() ) ) { + return $item; + } + + $card_type = $payment_token->get_card_type(); + $item['method']['last4'] = $payment_token->get_last4(); + $item['method']['brand'] = ( ! empty( $card_type ) ? ucfirst( $card_type ) : esc_html__( 'Credit card', 'woocommerce' ) ); + $item['expires'] = $payment_token->get_expiry_month() . '/' . substr( $payment_token->get_expiry_year(), -2 ); + + return $item; +} + +add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_cc', 10, 2 ); + +/** + * Controls the output for eChecks on the my account page. + * + * @since 2.6 + * @param array $item Individual list item from woocommerce_saved_payment_methods_list. + * @param WC_Payment_Token $payment_token The payment token associated with this method entry. + * @return array Filtered item. + */ +function wc_get_account_saved_payment_methods_list_item_echeck( $item, $payment_token ) { + if ( 'echeck' !== strtolower( $payment_token->get_type() ) ) { + return $item; + } + + $item['method']['last4'] = $payment_token->get_last4(); + $item['method']['brand'] = esc_html__( 'eCheck', 'woocommerce' ); + + return $item; +} + +add_filter( 'woocommerce_payment_methods_list_item', 'wc_get_account_saved_payment_methods_list_item_echeck', 10, 2 ); diff --git a/includes/wc-attribute-functions.php b/includes/wc-attribute-functions.php new file mode 100644 index 0000000..535a719 --- /dev/null +++ b/includes/wc-attribute-functions.php @@ -0,0 +1,734 @@ +get_results( "SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name != '' ORDER BY attribute_name ASC;" ); + + set_transient( 'wc_attribute_taxonomies', $raw_attribute_taxonomies ); + } + + /** + * Filter attribute taxonomies. + * + * @param array $attribute_taxonomies Results of the DB query. Each taxonomy is an object. + */ + $raw_attribute_taxonomies = (array) array_filter( apply_filters( 'woocommerce_attribute_taxonomies', $raw_attribute_taxonomies ) ); + + // Index by ID for easer lookups. + $attribute_taxonomies = array(); + + foreach ( $raw_attribute_taxonomies as $result ) { + $attribute_taxonomies[ 'id:' . $result->attribute_id ] = $result; + } + + wp_cache_set( $cache_key, $attribute_taxonomies, 'woocommerce-attributes' ); + + return $attribute_taxonomies; +} + +/** + * Get (cached) attribute taxonomy ID and name pairs. + * + * @since 3.6.0 + * @return array + */ +function wc_get_attribute_taxonomy_ids() { + $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); + $cache_key = $prefix . 'ids'; + $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); + + if ( $cache_value ) { + return $cache_value; + } + + $taxonomy_ids = array_map( 'absint', wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_id', 'attribute_name' ) ); + + wp_cache_set( $cache_key, $taxonomy_ids, 'woocommerce-attributes' ); + + return $taxonomy_ids; +} + +/** + * Get (cached) attribute taxonomy label and name pairs. + * + * @since 3.6.0 + * @return array + */ +function wc_get_attribute_taxonomy_labels() { + $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); + $cache_key = $prefix . 'labels'; + $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); + + if ( $cache_value ) { + return $cache_value; + } + + $taxonomy_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); + + wp_cache_set( $cache_key, $taxonomy_labels, 'woocommerce-attributes' ); + + return $taxonomy_labels; +} + +/** + * Get a product attribute name. + * + * @param string $attribute_name Attribute name. + * @return string + */ +function wc_attribute_taxonomy_name( $attribute_name ) { + return $attribute_name ? 'pa_' . wc_sanitize_taxonomy_name( $attribute_name ) : ''; +} + +/** + * Get the attribute name used when storing values in post meta. + * + * @since 2.6.0 + * @param string $attribute_name Attribute name. + * @return string + */ +function wc_variation_attribute_name( $attribute_name ) { + return 'attribute_' . sanitize_title( $attribute_name ); +} + +/** + * Get a product attribute name by ID. + * + * @since 2.4.0 + * @param int $attribute_id Attribute ID. + * @return string Return an empty string if attribute doesn't exist. + */ +function wc_attribute_taxonomy_name_by_id( $attribute_id ) { + $taxonomy_ids = wc_get_attribute_taxonomy_ids(); + $attribute_name = (string) array_search( $attribute_id, $taxonomy_ids, true ); + return wc_attribute_taxonomy_name( $attribute_name ); +} + +/** + * Get a product attribute ID by name. + * + * @since 2.6.0 + * @param string $name Attribute name. + * @return int + */ +function wc_attribute_taxonomy_id_by_name( $name ) { + $name = wc_attribute_taxonomy_slug( $name ); + $taxonomy_ids = wc_get_attribute_taxonomy_ids(); + + return isset( $taxonomy_ids[ $name ] ) ? $taxonomy_ids[ $name ] : 0; +} + +/** + * Get a product attributes label. + * + * @param string $name Attribute name. + * @param WC_Product $product Product data. + * @return string + */ +function wc_attribute_label( $name, $product = '' ) { + if ( taxonomy_is_product_attribute( $name ) ) { + $slug = wc_attribute_taxonomy_slug( $name ); + $all_labels = wc_get_attribute_taxonomy_labels(); + $label = isset( $all_labels[ $slug ] ) ? $all_labels[ $slug ] : $slug; + } elseif ( $product ) { + if ( $product->is_type( 'variation' ) ) { + $product = wc_get_product( $product->get_parent_id() ); + } + $attributes = array(); + + if ( false !== $product ) { + $attributes = $product->get_attributes(); + } + + // Attempt to get label from product, as entered by the user. + if ( $attributes && isset( $attributes[ sanitize_title( $name ) ] ) ) { + $label = $attributes[ sanitize_title( $name ) ]->get_name(); + } else { + $label = $name; + } + } else { + $label = $name; + } + + return apply_filters( 'woocommerce_attribute_label', $label, $name, $product ); +} + +/** + * Get a product attributes orderby setting. + * + * @param string $name Attribute name. + * @return string + */ +function wc_attribute_orderby( $name ) { + $name = wc_attribute_taxonomy_slug( $name ); + $id = wc_attribute_taxonomy_id_by_name( $name ); + $taxonomies = wc_get_attribute_taxonomies(); + + return apply_filters( 'woocommerce_attribute_orderby', isset( $taxonomies[ 'id:' . $id ] ) ? $taxonomies[ 'id:' . $id ]->attribute_orderby : 'menu_order', $name ); +} + +/** + * Get an array of product attribute taxonomies. + * + * @return array + */ +function wc_get_attribute_taxonomy_names() { + $taxonomy_names = array(); + $attribute_taxonomies = wc_get_attribute_taxonomies(); + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $tax ) { + $taxonomy_names[] = wc_attribute_taxonomy_name( $tax->attribute_name ); + } + } + return $taxonomy_names; +} + +/** + * Get attribute types. + * + * @since 2.4.0 + * @return array + */ +function wc_get_attribute_types() { + return (array) apply_filters( + 'product_attributes_type_selector', + array( + 'select' => __( 'Select', 'woocommerce' ), + ) + ); +} + +/** + * Check if there are custom attribute types. + * + * @since 3.3.2 + * @return bool True if there are custom types, otherwise false. + */ +function wc_has_custom_attribute_types() { + $types = wc_get_attribute_types(); + + return 1 < count( $types ) || ! array_key_exists( 'select', $types ); +} + +/** + * Get attribute type label. + * + * @since 3.0.0 + * @param string $type Attribute type slug. + * @return string + */ +function wc_get_attribute_type_label( $type ) { + $types = wc_get_attribute_types(); + + return isset( $types[ $type ] ) ? $types[ $type ] : __( 'Select', 'woocommerce' ); +} + +/** + * Check if attribute name is reserved. + * https://codex.wordpress.org/Function_Reference/register_taxonomy#Reserved_Terms. + * + * @since 2.4.0 + * @param string $attribute_name Attribute name. + * @return bool + */ +function wc_check_if_attribute_name_is_reserved( $attribute_name ) { + // Forbidden attribute names. + $reserved_terms = array( + 'attachment', + 'attachment_id', + 'author', + 'author_name', + 'calendar', + 'cat', + 'category', + 'category__and', + 'category__in', + 'category__not_in', + 'category_name', + 'comments_per_page', + 'comments_popup', + 'cpage', + 'day', + 'debug', + 'error', + 'exact', + 'feed', + 'hour', + 'link_category', + 'm', + 'minute', + 'monthnum', + 'more', + 'name', + 'nav_menu', + 'nopaging', + 'offset', + 'order', + 'orderby', + 'p', + 'page', + 'page_id', + 'paged', + 'pagename', + 'pb', + 'perm', + 'post', + 'post__in', + 'post__not_in', + 'post_format', + 'post_mime_type', + 'post_status', + 'post_tag', + 'post_type', + 'posts', + 'posts_per_archive_page', + 'posts_per_page', + 'preview', + 'robots', + 's', + 'search', + 'second', + 'sentence', + 'showposts', + 'static', + 'subpost', + 'subpost_id', + 'tag', + 'tag__and', + 'tag__in', + 'tag__not_in', + 'tag_id', + 'tag_slug__and', + 'tag_slug__in', + 'taxonomy', + 'tb', + 'term', + 'type', + 'w', + 'withcomments', + 'withoutcomments', + 'year', + ); + + return in_array( $attribute_name, $reserved_terms, true ); +} + +/** + * Callback for array filter to get visible only. + * + * @since 3.0.0 + * @param WC_Product_Attribute $attribute Attribute data. + * @return bool + */ +function wc_attributes_array_filter_visible( $attribute ) { + return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_visible() && ( ! $attribute->is_taxonomy() || taxonomy_exists( $attribute->get_name() ) ); +} + +/** + * Callback for array filter to get variation attributes only. + * + * @since 3.0.0 + * @param WC_Product_Attribute $attribute Attribute data. + * @return bool + */ +function wc_attributes_array_filter_variation( $attribute ) { + return $attribute && is_a( $attribute, 'WC_Product_Attribute' ) && $attribute->get_variation(); +} + +/** + * Check if an attribute is included in the attributes area of a variation name. + * + * @since 3.0.2 + * @param string $attribute Attribute value to check for. + * @param string $name Product name to check in. + * @return bool + */ +function wc_is_attribute_in_product_name( $attribute, $name ) { + $is_in_name = stristr( $name, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $name ), strrev( ' ' . $attribute ) ); + return apply_filters( 'woocommerce_is_attribute_in_product_name', $is_in_name, $attribute, $name ); +} + +/** + * Callback for array filter to get default attributes. Will allow for '0' string values, but regard all other + * class PHP FALSE equivalents normally. + * + * @since 3.1.0 + * @param mixed $attribute Attribute being considered for exclusion from parent array. + * @return bool + */ +function wc_array_filter_default_attributes( $attribute ) { + return is_scalar( $attribute ) && ( ! empty( $attribute ) || '0' === $attribute ); +} + +/** + * Get attribute data by ID. + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @return stdClass|null + */ +function wc_get_attribute( $id ) { + $attributes = wc_get_attribute_taxonomies(); + + if ( ! isset( $attributes[ 'id:' . $id ] ) ) { + return null; + } + + $data = $attributes[ 'id:' . $id ]; + $attribute = new stdClass(); + $attribute->id = (int) $data->attribute_id; + $attribute->name = $data->attribute_label; + $attribute->slug = wc_attribute_taxonomy_name( $data->attribute_name ); + $attribute->type = $data->attribute_type; + $attribute->order_by = $data->attribute_orderby; + $attribute->has_archives = (bool) $data->attribute_public; + return $attribute; +} + +/** + * Create attribute. + * + * @since 3.2.0 + * @param array $args Attribute arguments { + * Array of attribute parameters. + * + * @type int $id Unique identifier, used to update an attribute. + * @type string $name Attribute name. Always required. + * @type string $slug Attribute alphanumeric identifier. + * @type string $type Type of attribute. + * Core by default accepts: 'select' and 'text'. + * Default to 'select'. + * @type string $order_by Sort order. + * Accepts: 'menu_order', 'name', 'name_num' and 'id'. + * Default to 'menu_order'. + * @type bool $has_archives Enable or disable attribute archives. False by default. + * } + * @return int|WP_Error + */ +function wc_create_attribute( $args ) { + global $wpdb; + + $args = wp_unslash( $args ); + $id = ! empty( $args['id'] ) ? intval( $args['id'] ) : 0; + $format = array( '%s', '%s', '%s', '%s', '%d' ); + + // Name is required. + if ( empty( $args['name'] ) ) { + return new WP_Error( 'missing_attribute_name', __( 'Please, provide an attribute name.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Set the attribute slug. + if ( empty( $args['slug'] ) ) { + $slug = wc_sanitize_taxonomy_name( $args['name'] ); + } else { + $slug = preg_replace( '/^pa\_/', '', wc_sanitize_taxonomy_name( $args['slug'] ) ); + } + + // Validate slug. + if ( strlen( $slug ) >= 28 ) { + /* translators: %s: attribute slug */ + return new WP_Error( 'invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + /* translators: %s: attribute slug */ + return new WP_Error( 'invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( ( 0 === $id && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) || ( isset( $args['old_slug'] ) && $args['old_slug'] !== $slug && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) ) { + /* translators: %s: attribute slug */ + return new WP_Error( 'invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } + + // Validate type. + if ( empty( $args['type'] ) || ! array_key_exists( $args['type'], wc_get_attribute_types() ) ) { + $args['type'] = 'select'; + } + + // Validate order by. + if ( empty( $args['order_by'] ) || ! in_array( $args['order_by'], array( 'menu_order', 'name', 'name_num', 'id' ), true ) ) { + $args['order_by'] = 'menu_order'; + } + + $data = array( + 'attribute_label' => $args['name'], + 'attribute_name' => $slug, + 'attribute_type' => $args['type'], + 'attribute_orderby' => $args['order_by'], + 'attribute_public' => isset( $args['has_archives'] ) ? (int) $args['has_archives'] : 0, + ); + + // Create or update. + if ( 0 === $id ) { + $results = $wpdb->insert( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $data, + $format + ); + + if ( is_wp_error( $results ) ) { + return new WP_Error( 'cannot_create_attribute', $results->get_error_message(), array( 'status' => 400 ) ); + } + + $id = $wpdb->insert_id; + + /** + * Attribute added. + * + * @param int $id Added attribute ID. + * @param array $data Attribute data. + */ + do_action( 'woocommerce_attribute_added', $id, $data ); + } else { + $results = $wpdb->update( + $wpdb->prefix . 'woocommerce_attribute_taxonomies', + $data, + array( 'attribute_id' => $id ), + $format, + array( '%d' ) + ); + + if ( false === $results ) { + return new WP_Error( 'cannot_update_attribute', __( 'Could not update the attribute.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Set old slug to check for database changes. + $old_slug = ! empty( $args['old_slug'] ) ? wc_sanitize_taxonomy_name( $args['old_slug'] ) : $slug; + + /** + * Attribute updated. + * + * @param int $id Added attribute ID. + * @param array $data Attribute data. + * @param string $old_slug Attribute old name. + */ + do_action( 'woocommerce_attribute_updated', $id, $data, $old_slug ); + + if ( $old_slug !== $slug ) { + // Update taxonomies in the wp term taxonomy table. + $wpdb->update( + $wpdb->term_taxonomy, + array( 'taxonomy' => wc_attribute_taxonomy_name( $data['attribute_name'] ) ), + array( 'taxonomy' => 'pa_' . $old_slug ) + ); + + // Update taxonomy ordering term meta. + $wpdb->update( + $wpdb->termmeta, + array( 'meta_key' => 'order_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok. + array( 'meta_key' => 'order_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok. + ); + + // Update product attributes which use this taxonomy. + $old_taxonomy_name = 'pa_' . $old_slug; + $new_taxonomy_name = 'pa_' . $data['attribute_name']; + $old_attribute_key = sanitize_title( $old_taxonomy_name ); // @see WC_Product::set_attributes(). + $new_attribute_key = sanitize_title( $new_taxonomy_name ); // @see WC_Product::set_attributes(). + $metadatas = $wpdb->get_results( + $wpdb->prepare( + "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_product_attributes' AND meta_value LIKE %s", + '%' . $wpdb->esc_like( $old_taxonomy_name ) . '%' + ), + ARRAY_A + ); + foreach ( $metadatas as $metadata ) { + $product_id = $metadata['post_id']; + $unserialized_data = maybe_unserialize( $metadata['meta_value'] ); + + if ( ! $unserialized_data || ! is_array( $unserialized_data ) || ! isset( $unserialized_data[ $old_attribute_key ] ) ) { + continue; + } + + $unserialized_data[ $new_attribute_key ] = $unserialized_data[ $old_attribute_key ]; + unset( $unserialized_data[ $old_attribute_key ] ); + $unserialized_data[ $new_attribute_key ]['name'] = $new_taxonomy_name; + update_post_meta( $product_id, '_product_attributes', wp_slash( $unserialized_data ) ); + } + + // Update variations which use this taxonomy. + $wpdb->update( + $wpdb->postmeta, + array( 'meta_key' => 'attribute_pa_' . sanitize_title( $data['attribute_name'] ) ), // WPCS: slow query ok. + array( 'meta_key' => 'attribute_pa_' . sanitize_title( $old_slug ) ) // WPCS: slow query ok. + ); + } + } + + // Clear cache and flush rewrite rules. + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return $id; +} + +/** + * Update an attribute. + * + * For available args see wc_create_attribute(). + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @param array $args Attribute arguments. + * @return int|WP_Error + */ +function wc_update_attribute( $id, $args ) { + global $wpdb; + + $attribute = wc_get_attribute( $id ); + + $args['id'] = $attribute ? $attribute->id : 0; + + if ( $args['id'] && empty( $args['name'] ) ) { + $args['name'] = $attribute->name; + } + + $args['old_slug'] = $wpdb->get_var( + $wpdb->prepare( + " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", + $args['id'] + ) + ); + + return wc_create_attribute( $args ); +} + +/** + * Delete attribute by ID. + * + * @since 3.2.0 + * @param int $id Attribute ID. + * @return bool + */ +function wc_delete_attribute( $id ) { + global $wpdb; + + $name = $wpdb->get_var( + $wpdb->prepare( + " + SELECT attribute_name + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", + $id + ) + ); + + $taxonomy = wc_attribute_taxonomy_name( $name ); + + /** + * Before deleting an attribute. + * + * @param int $id Attribute ID. + * @param string $name Attribute name. + * @param string $taxonomy Attribute taxonomy name. + */ + do_action( 'woocommerce_before_attribute_delete', $id, $name, $taxonomy ); + + if ( $name && $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d", $id ) ) ) { + if ( taxonomy_exists( $taxonomy ) ) { + $terms = get_terms( $taxonomy, 'orderby=name&hide_empty=0' ); + foreach ( $terms as $term ) { + wp_delete_term( $term->term_id, $taxonomy ); + } + } + + /** + * After deleting an attribute. + * + * @param int $id Attribute ID. + * @param string $name Attribute name. + * @param string $taxonomy Attribute taxonomy name. + */ + do_action( 'woocommerce_attribute_deleted', $id, $name, $taxonomy ); + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + + return true; + } + + return false; +} + +/** + * Get an unprefixed product attribute name. + * + * @since 3.6.0 + * + * @param string $attribute_name Attribute name. + * @return string + */ +function wc_attribute_taxonomy_slug( $attribute_name ) { + $prefix = WC_Cache_Helper::get_cache_prefix( 'woocommerce-attributes' ); + $cache_key = $prefix . 'slug-' . $attribute_name; + $cache_value = wp_cache_get( $cache_key, 'woocommerce-attributes' ); + + if ( $cache_value ) { + return $cache_value; + } + + $attribute_name = wc_sanitize_taxonomy_name( $attribute_name ); + $attribute_slug = 0 === strpos( $attribute_name, 'pa_' ) ? substr( $attribute_name, 3 ) : $attribute_name; + wp_cache_set( $cache_key, $attribute_slug, 'woocommerce-attributes' ); + + return $attribute_slug; +} diff --git a/includes/wc-cart-functions.php b/includes/wc-cart-functions.php new file mode 100644 index 0000000..1d159c7 --- /dev/null +++ b/includes/wc-cart-functions.php @@ -0,0 +1,505 @@ +cart ) || '' === WC()->cart ) { + WC()->cart = new WC_Cart(); + } + WC()->cart->empty_cart( false ); +} + +/** + * Load the persistent cart. + * + * @param string $user_login User login. + * @param WP_User $user User data. + * @deprecated 2.3 + */ +function wc_load_persistent_cart( $user_login, $user ) { + if ( ! $user || ! apply_filters( 'woocommerce_persistent_cart_enabled', true ) ) { + return; + } + + $saved_cart = get_user_meta( $user->ID, '_woocommerce_persistent_cart_' . get_current_blog_id(), true ); + + if ( ! $saved_cart ) { + return; + } + + $cart = WC()->session->cart; + + if ( empty( $cart ) || ! is_array( $cart ) || 0 === count( $cart ) ) { + WC()->session->cart = $saved_cart['cart']; + } +} + +/** + * Retrieves unvalidated referer from '_wp_http_referer' or HTTP referer. + * + * Do not use for redirects, use {@see wp_get_referer()} instead. + * + * @since 2.6.1 + * @return string|false Referer URL on success, false on failure. + */ +function wc_get_raw_referer() { + if ( function_exists( 'wp_get_raw_referer' ) ) { + return wp_get_raw_referer(); + } + + if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { // WPCS: input var ok, CSRF ok. + return wp_unslash( $_REQUEST['_wp_http_referer'] ); // WPCS: input var ok, CSRF ok, sanitization ok. + } elseif ( ! empty( $_SERVER['HTTP_REFERER'] ) ) { // WPCS: input var ok, CSRF ok. + return wp_unslash( $_SERVER['HTTP_REFERER'] ); // WPCS: input var ok, CSRF ok, sanitization ok. + } + + return false; +} + +/** + * Add to cart messages. + * + * @param int|array $products Product ID list or single product ID. + * @param bool $show_qty Should qty's be shown? Added in 2.6.0. + * @param bool $return Return message rather than add it. + * + * @return mixed + */ +function wc_add_to_cart_message( $products, $show_qty = false, $return = false ) { + $titles = array(); + $count = 0; + + if ( ! is_array( $products ) ) { + $products = array( $products => 1 ); + $show_qty = false; + } + + if ( ! $show_qty ) { + $products = array_fill_keys( array_keys( $products ), 1 ); + } + + foreach ( $products as $product_id => $qty ) { + /* translators: %s: product name */ + $titles[] = apply_filters( 'woocommerce_add_to_cart_qty_html', ( $qty > 1 ? absint( $qty ) . ' × ' : '' ), $product_id ) . apply_filters( 'woocommerce_add_to_cart_item_name_in_quotes', sprintf( _x( '“%s”', 'Item name in quotes', 'woocommerce' ), strip_tags( get_the_title( $product_id ) ) ), $product_id ); + $count += $qty; + } + + $titles = array_filter( $titles ); + /* translators: %s: product name */ + $added_text = sprintf( _n( '%s has been added to your cart.', '%s have been added to your cart.', $count, 'woocommerce' ), wc_format_list_of_items( $titles ) ); + + // Output success messages. + if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) { + $return_to = apply_filters( 'woocommerce_continue_shopping_redirect', wc_get_raw_referer() ? wp_validate_redirect( wc_get_raw_referer(), false ) : wc_get_page_permalink( 'shop' ) ); + $message = sprintf( '%s %s', esc_url( $return_to ), esc_html__( 'Continue shopping', 'woocommerce' ), esc_html( $added_text ) ); + } else { + $message = sprintf( '%s %s', esc_url( wc_get_cart_url() ), esc_html__( 'View cart', 'woocommerce' ), esc_html( $added_text ) ); + } + + if ( has_filter( 'wc_add_to_cart_message' ) ) { + wc_deprecated_function( 'The wc_add_to_cart_message filter', '3.0', 'wc_add_to_cart_message_html' ); + $message = apply_filters( 'wc_add_to_cart_message', $message, $product_id ); + } + + $message = apply_filters( 'wc_add_to_cart_message_html', $message, $products, $show_qty ); + + if ( $return ) { + return $message; + } else { + wc_add_notice( $message, apply_filters( 'woocommerce_add_to_cart_notice_type', 'success' ) ); + } +} + +/** + * Comma separate a list of item names, and replace final comma with 'and'. + * + * @param array $items Cart items. + * @return string + */ +function wc_format_list_of_items( $items ) { + $item_string = ''; + + foreach ( $items as $key => $item ) { + $item_string .= $item; + + if ( count( $items ) === $key + 2 ) { + $item_string .= ' ' . __( 'and', 'woocommerce' ) . ' '; + } elseif ( count( $items ) !== $key + 1 ) { + $item_string .= ', '; + } + } + + return $item_string; +} + +/** + * Clear cart after payment. + */ +function wc_clear_cart_after_payment() { + global $wp; + + if ( ! empty( $wp->query_vars['order-received'] ) ) { + + $order_id = absint( $wp->query_vars['order-received'] ); + $order_key = isset( $_GET['key'] ) ? wc_clean( wp_unslash( $_GET['key'] ) ) : ''; // WPCS: input var ok, CSRF ok. + + if ( $order_id > 0 ) { + $order = wc_get_order( $order_id ); + + if ( $order && hash_equals( $order->get_order_key(), $order_key ) ) { + WC()->cart->empty_cart(); + } + } + } + + if ( WC()->session->order_awaiting_payment > 0 ) { + $order = wc_get_order( WC()->session->order_awaiting_payment ); + + if ( $order && $order->get_id() > 0 ) { + // If the order has not failed, or is not pending, the order must have gone through. + if ( ! $order->has_status( array( 'failed', 'pending', 'cancelled' ) ) ) { + WC()->cart->empty_cart(); + } + } + } +} +add_action( 'get_header', 'wc_clear_cart_after_payment' ); + +/** + * Get the subtotal. + */ +function wc_cart_totals_subtotal_html() { + echo WC()->cart->get_cart_subtotal(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get shipping methods. + */ +function wc_cart_totals_shipping_html() { + $packages = WC()->shipping()->get_packages(); + $first = true; + + foreach ( $packages as $i => $package ) { + $chosen_method = isset( WC()->session->chosen_shipping_methods[ $i ] ) ? WC()->session->chosen_shipping_methods[ $i ] : ''; + $product_names = array(); + + if ( count( $packages ) > 1 ) { + foreach ( $package['contents'] as $item_id => $values ) { + $product_names[ $item_id ] = $values['data']->get_name() . ' ×' . $values['quantity']; + } + $product_names = apply_filters( 'woocommerce_shipping_package_details_array', $product_names, $package ); + } + + wc_get_template( + 'cart/cart-shipping.php', + array( + 'package' => $package, + 'available_methods' => $package['rates'], + 'show_package_details' => count( $packages ) > 1, + 'show_shipping_calculator' => is_cart() && apply_filters( 'woocommerce_shipping_show_shipping_calculator', $first, $i, $package ), + 'package_details' => implode( ', ', $product_names ), + /* translators: %d: shipping package number */ + 'package_name' => apply_filters( 'woocommerce_shipping_package_name', ( ( $i + 1 ) > 1 ) ? sprintf( _x( 'Shipping %d', 'shipping packages', 'woocommerce' ), ( $i + 1 ) ) : _x( 'Shipping', 'shipping packages', 'woocommerce' ), $i, $package ), + 'index' => $i, + 'chosen_method' => $chosen_method, + 'formatted_destination' => WC()->countries->get_formatted_address( $package['destination'], ', ' ), + 'has_calculated_shipping' => WC()->customer->has_calculated_shipping(), + ) + ); + + $first = false; + } +} + +/** + * Get taxes total. + */ +function wc_cart_totals_taxes_total_html() { + echo apply_filters( 'woocommerce_cart_totals_taxes_total_html', wc_price( WC()->cart->get_taxes_total() ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get a coupon label. + * + * @param string|WC_Coupon $coupon Coupon data or code. + * @param bool $echo Echo or return. + * + * @return string + */ +function wc_cart_totals_coupon_label( $coupon, $echo = true ) { + if ( is_string( $coupon ) ) { + $coupon = new WC_Coupon( $coupon ); + } + + /* translators: %s: coupon code */ + $label = apply_filters( 'woocommerce_cart_totals_coupon_label', sprintf( esc_html__( 'Coupon: %s', 'woocommerce' ), $coupon->get_code() ), $coupon ); + + if ( $echo ) { + echo $label; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } else { + return $label; + } +} + +/** + * Get coupon display HTML. + * + * @param string|WC_Coupon $coupon Coupon data or code. + */ +function wc_cart_totals_coupon_html( $coupon ) { + if ( is_string( $coupon ) ) { + $coupon = new WC_Coupon( $coupon ); + } + + $discount_amount_html = ''; + + $amount = WC()->cart->get_coupon_discount_amount( $coupon->get_code(), WC()->cart->display_cart_ex_tax ); + $discount_amount_html = '-' . wc_price( $amount ); + + if ( $coupon->get_free_shipping() && empty( $amount ) ) { + $discount_amount_html = __( 'Free shipping coupon', 'woocommerce' ); + } + + $discount_amount_html = apply_filters( 'woocommerce_coupon_discount_amount_html', $discount_amount_html, $coupon ); + $coupon_html = $discount_amount_html . ' ' . __( '[Remove]', 'woocommerce' ) . ''; + + echo wp_kses( apply_filters( 'woocommerce_cart_totals_coupon_html', $coupon_html, $coupon, $discount_amount_html ), array_replace_recursive( wp_kses_allowed_html( 'post' ), array( 'a' => array( 'data-coupon' => true ) ) ) ); // phpcs:ignore PHPCompatibility.PHP.NewFunctions.array_replace_recursiveFound +} + +/** + * Get order total html including inc tax if needed. + */ +function wc_cart_totals_order_total_html() { + $value = '' . WC()->cart->get_total() . ' '; + + // If prices are tax inclusive, show taxes here. + if ( wc_tax_enabled() && WC()->cart->display_prices_including_tax() ) { + $tax_string_array = array(); + $cart_tax_totals = WC()->cart->get_tax_totals(); + + if ( get_option( 'woocommerce_tax_total_display' ) === 'itemized' ) { + foreach ( $cart_tax_totals as $code => $tax ) { + $tax_string_array[] = sprintf( '%s %s', $tax->formatted_amount, $tax->label ); + } + } elseif ( ! empty( $cart_tax_totals ) ) { + $tax_string_array[] = sprintf( '%s %s', wc_price( WC()->cart->get_taxes_total( true, true ) ), WC()->countries->tax_or_vat() ); + } + + if ( ! empty( $tax_string_array ) ) { + $taxable_address = WC()->customer->get_taxable_address(); + if ( WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ) { + $country = WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ]; + /* translators: 1: tax amount 2: country name */ + $tax_text = wp_kses_post( sprintf( __( '(includes %1$s estimated for %2$s)', 'woocommerce' ), implode( ', ', $tax_string_array ), $country ) ); + } else { + /* translators: %s: tax amount */ + $tax_text = wp_kses_post( sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) ); + } + + $value .= '' . $tax_text . ''; + } + } + + echo apply_filters( 'woocommerce_cart_totals_order_total_html', $value ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get the fee value. + * + * @param object $fee Fee data. + */ +function wc_cart_totals_fee_html( $fee ) { + $cart_totals_fee_html = WC()->cart->display_prices_including_tax() ? wc_price( $fee->total + $fee->tax ) : wc_price( $fee->total ); + + echo apply_filters( 'woocommerce_cart_totals_fee_html', $cart_totals_fee_html, $fee ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get a shipping methods full label including price. + * + * @param WC_Shipping_Rate $method Shipping method rate data. + * @return string + */ +function wc_cart_totals_shipping_method_label( $method ) { + $label = $method->get_label(); + $has_cost = 0 < $method->cost; + $hide_cost = ! $has_cost && in_array( $method->get_method_id(), array( 'free_shipping', 'local_pickup' ), true ); + + if ( $has_cost && ! $hide_cost ) { + if ( WC()->cart->display_prices_including_tax() ) { + $label .= ': ' . wc_price( $method->cost + $method->get_shipping_tax() ); + if ( $method->get_shipping_tax() > 0 && ! wc_prices_include_tax() ) { + $label .= ' ' . WC()->countries->inc_tax_or_vat() . ''; + } + } else { + $label .= ': ' . wc_price( $method->cost ); + if ( $method->get_shipping_tax() > 0 && wc_prices_include_tax() ) { + $label .= ' ' . WC()->countries->ex_tax_or_vat() . ''; + } + } + } + + return apply_filters( 'woocommerce_cart_shipping_method_full_label', $label, $method ); +} + +/** + * Round discount. + * + * @param double $value Amount to round. + * @param int $precision DP to round. + * @return float + */ +function wc_cart_round_discount( $value, $precision ) { + return wc_round_discount( $value, $precision ); +} + +/** + * Gets chosen shipping method IDs from chosen_shipping_methods session, without instance IDs. + * + * @since 2.6.2 + * @return string[] + */ +function wc_get_chosen_shipping_method_ids() { + $method_ids = array(); + $chosen_methods = WC()->session->get( 'chosen_shipping_methods', array() ); + foreach ( $chosen_methods as $chosen_method ) { + $chosen_method = explode( ':', $chosen_method ); + $method_ids[] = current( $chosen_method ); + } + return $method_ids; +} + +/** + * Get chosen method for package from session. + * + * @since 3.2.0 + * @param int $key Key of package. + * @param array $package Package data array. + * @return string|bool + */ +function wc_get_chosen_shipping_method_for_package( $key, $package ) { + $chosen_methods = WC()->session->get( 'chosen_shipping_methods' ); + $chosen_method = isset( $chosen_methods[ $key ] ) ? $chosen_methods[ $key ] : false; + $changed = wc_shipping_methods_have_changed( $key, $package ); + + // This is deprecated but here for BW compat. TODO: Remove in 4.0.0. + $method_counts = WC()->session->get( 'shipping_method_counts' ); + + if ( ! empty( $method_counts[ $key ] ) ) { + $method_count = absint( $method_counts[ $key ] ); + } else { + $method_count = 0; + } + + // If not set, not available, or available methods have changed, set to the DEFAULT option. + if ( ! $chosen_method || $changed || ! isset( $package['rates'][ $chosen_method ] ) || count( $package['rates'] ) !== $method_count ) { + $chosen_method = wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ); + $chosen_methods[ $key ] = $chosen_method; + $method_counts[ $key ] = count( $package['rates'] ); + + WC()->session->set( 'chosen_shipping_methods', $chosen_methods ); + WC()->session->set( 'shipping_method_counts', $method_counts ); + + do_action( 'woocommerce_shipping_method_chosen', $chosen_method ); + } + return $chosen_method; +} + +/** + * Choose the default method for a package. + * + * @since 3.2.0 + * @param int $key Key of package. + * @param array $package Package data array. + * @param string $chosen_method Chosen method id. + * @return string + */ +function wc_get_default_shipping_method_for_package( $key, $package, $chosen_method ) { + $rate_keys = array_keys( $package['rates'] ); + $default = current( $rate_keys ); + $coupons = WC()->cart->get_coupons(); + foreach ( $coupons as $coupon ) { + if ( $coupon->get_free_shipping() ) { + foreach ( $rate_keys as $rate_key ) { + if ( 0 === stripos( $rate_key, 'free_shipping' ) ) { + $default = $rate_key; + break; + } + } + break; + } + } + return apply_filters( 'woocommerce_shipping_chosen_method', $default, $package['rates'], $chosen_method ); +} + +/** + * See if the methods have changed since the last request. + * + * @since 3.2.0 + * @param int $key Key of package. + * @param array $package Package data array. + * @return bool + */ +function wc_shipping_methods_have_changed( $key, $package ) { + // Lookup previous methods from session. + $previous_shipping_methods = WC()->session->get( 'previous_shipping_methods' ); + // Get new and old rates. + $new_rates = array_keys( $package['rates'] ); + $prev_rates = isset( $previous_shipping_methods[ $key ] ) ? $previous_shipping_methods[ $key ] : false; + // Update session. + $previous_shipping_methods[ $key ] = $new_rates; + WC()->session->set( 'previous_shipping_methods', $previous_shipping_methods ); + return $new_rates !== $prev_rates; +} + +/** + * Gets a hash of important product data that when changed should cause cart items to be invalidated. + * + * The woocommerce_cart_item_data_to_validate filter can be used to add custom properties. + * + * @param WC_Product $product Product object. + * @return string + */ +function wc_get_cart_item_data_hash( $product ) { + return md5( + wp_json_encode( + apply_filters( + 'woocommerce_cart_item_data_to_validate', + array( + 'type' => $product->get_type(), + 'attributes' => 'variation' === $product->get_type() ? $product->get_variation_attributes() : '', + ), + $product + ) + ) + ); +} diff --git a/includes/wc-conditional-functions.php b/includes/wc-conditional-functions.php new file mode 100644 index 0000000..641066c --- /dev/null +++ b/includes/wc-conditional-functions.php @@ -0,0 +1,496 @@ +query_vars['order-pay'] ); + } +} + +if ( ! function_exists( 'is_wc_endpoint_url' ) ) { + + /** + * Is_wc_endpoint_url - Check if an endpoint is showing. + * + * @param string|false $endpoint Whether endpoint. + * @return bool + */ + function is_wc_endpoint_url( $endpoint = false ) { + global $wp; + + $wc_endpoints = WC()->query->get_query_vars(); + + if ( false !== $endpoint ) { + if ( ! isset( $wc_endpoints[ $endpoint ] ) ) { + return false; + } else { + $endpoint_var = $wc_endpoints[ $endpoint ]; + } + + return isset( $wp->query_vars[ $endpoint_var ] ); + } else { + foreach ( $wc_endpoints as $key => $value ) { + if ( isset( $wp->query_vars[ $key ] ) ) { + return true; + } + } + + return false; + } + } +} + +if ( ! function_exists( 'is_account_page' ) ) { + + /** + * Is_account_page - Returns true when viewing an account page. + * + * @return bool + */ + function is_account_page() { + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) ) || wc_post_content_has_shortcode( 'woocommerce_my_account' ) || apply_filters( 'woocommerce_is_account_page', false ); + } +} + +if ( ! function_exists( 'is_view_order_page' ) ) { + + /** + * Is_view_order_page - Returns true when on the view order page. + * + * @return bool + */ + function is_view_order_page() { + global $wp; + + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['view-order'] ) ); + } +} + +if ( ! function_exists( 'is_edit_account_page' ) ) { + + /** + * Check for edit account page. + * Returns true when viewing the edit account page. + * + * @since 2.5.1 + * @return bool + */ + function is_edit_account_page() { + global $wp; + + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['edit-account'] ) ); + } +} + +if ( ! function_exists( 'is_order_received_page' ) ) { + + /** + * Is_order_received_page - Returns true when viewing the order received page. + * + * @return bool + */ + function is_order_received_page() { + global $wp; + + $page_id = wc_get_page_id( 'checkout' ); + + return apply_filters( 'woocommerce_is_order_received_page', ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['order-received'] ) ) ); + } +} + +if ( ! function_exists( 'is_add_payment_method_page' ) ) { + + /** + * Is_add_payment_method_page - Returns true when viewing the add payment method page. + * + * @return bool + */ + function is_add_payment_method_page() { + global $wp; + + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) && ( isset( $wp->query_vars['payment-methods'] ) || isset( $wp->query_vars['add-payment-method'] ) ) ); + } +} + +if ( ! function_exists( 'is_lost_password_page' ) ) { + + /** + * Is_lost_password_page - Returns true when viewing the lost password page. + * + * @return bool + */ + function is_lost_password_page() { + global $wp; + + $page_id = wc_get_page_id( 'myaccount' ); + + return ( $page_id && is_page( $page_id ) && isset( $wp->query_vars['lost-password'] ) ); + } +} + +if ( ! function_exists( 'is_ajax' ) ) { + + /** + * Is_ajax - Returns true when the page is loaded via ajax. + * + * @return bool + */ + function is_ajax() { + return function_exists( 'wp_doing_ajax' ) ? wp_doing_ajax() : Constants::is_defined( 'DOING_AJAX' ); + } +} + +if ( ! function_exists( 'is_store_notice_showing' ) ) { + + /** + * Is_store_notice_showing - Returns true when store notice is active. + * + * @return bool + */ + function is_store_notice_showing() { + return 'no' !== get_option( 'woocommerce_demo_store', 'no' ); + } +} + +if ( ! function_exists( 'is_filtered' ) ) { + + /** + * Is_filtered - Returns true when filtering products using layered nav or price sliders. + * + * @return bool + */ + function is_filtered() { + return apply_filters( 'woocommerce_is_filtered', ( count( WC_Query::get_layered_nav_chosen_attributes() ) > 0 || isset( $_GET['max_price'] ) || isset( $_GET['min_price'] ) || isset( $_GET['rating_filter'] ) ) ); // WPCS: CSRF ok. + } +} + +if ( ! function_exists( 'taxonomy_is_product_attribute' ) ) { + + /** + * Returns true when the passed taxonomy name is a product attribute. + * + * @uses $wc_product_attributes global which stores taxonomy names upon registration + * @param string $name of the attribute. + * @return bool + */ + function taxonomy_is_product_attribute( $name ) { + global $wc_product_attributes; + + return taxonomy_exists( $name ) && array_key_exists( $name, (array) $wc_product_attributes ); + } +} + +if ( ! function_exists( 'meta_is_product_attribute' ) ) { + + /** + * Returns true when the passed meta name is a product attribute. + * + * @param string $name of the attribute. + * @param string $value of the attribute. + * @param int $product_id to check for attribute. + * @return bool + */ + function meta_is_product_attribute( $name, $value, $product_id ) { + $product = wc_get_product( $product_id ); + + if ( $product && method_exists( $product, 'get_variation_attributes' ) ) { + $variation_attributes = $product->get_variation_attributes(); + $attributes = $product->get_attributes(); + return ( in_array( $name, array_keys( $attributes ), true ) && in_array( $value, $variation_attributes[ $attributes[ $name ]['name'] ], true ) ); + } else { + return false; + } + } +} + +if ( ! function_exists( 'wc_tax_enabled' ) ) { + + /** + * Are store-wide taxes enabled? + * + * @return bool + */ + function wc_tax_enabled() { + return apply_filters( 'wc_tax_enabled', get_option( 'woocommerce_calc_taxes' ) === 'yes' ); + } +} + +if ( ! function_exists( 'wc_shipping_enabled' ) ) { + + /** + * Is shipping enabled? + * + * @return bool + */ + function wc_shipping_enabled() { + return apply_filters( 'wc_shipping_enabled', get_option( 'woocommerce_ship_to_countries' ) !== 'disabled' ); + } +} + +if ( ! function_exists( 'wc_prices_include_tax' ) ) { + + /** + * Are prices inclusive of tax? + * + * @return bool + */ + function wc_prices_include_tax() { + return wc_tax_enabled() && apply_filters( 'woocommerce_prices_include_tax', get_option( 'woocommerce_prices_include_tax' ) === 'yes' ); + } +} + +/** + * Simple check for validating a URL, it must start with http:// or https://. + * and pass FILTER_VALIDATE_URL validation. + * + * @param string $url to check. + * @return bool + */ +function wc_is_valid_url( $url ) { + + // Must start with http:// or https://. + if ( 0 !== strpos( $url, 'http://' ) && 0 !== strpos( $url, 'https://' ) ) { + return false; + } + + // Must pass validation. + if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) { + return false; + } + + return true; +} + +/** + * Check if the home URL is https. If it is, we don't need to do things such as 'force ssl'. + * + * @since 2.4.13 + * @return bool + */ +function wc_site_is_https() { + return false !== strstr( get_option( 'home' ), 'https:' ); +} + +/** + * Check if the checkout is configured for https. Look at options, WP HTTPS plugin, or the permalink itself. + * + * @since 2.5.0 + * @return bool + */ +function wc_checkout_is_https() { + return wc_site_is_https() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) || class_exists( 'WordPressHTTPS' ) || strstr( wc_get_page_permalink( 'checkout' ), 'https:' ); +} + +/** + * Checks whether the content passed contains a specific short code. + * + * @param string $tag Shortcode tag to check. + * @return bool + */ +function wc_post_content_has_shortcode( $tag = '' ) { + global $post; + + return is_singular() && is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, $tag ); +} + +/** + * Check if reviews are enabled. + * + * @since 3.6.0 + * @return bool + */ +function wc_reviews_enabled() { + return 'yes' === get_option( 'woocommerce_enable_reviews' ); +} + +/** + * Check if reviews ratings are enabled. + * + * @since 3.6.0 + * @return bool + */ +function wc_review_ratings_enabled() { + return wc_reviews_enabled() && 'yes' === get_option( 'woocommerce_enable_review_rating' ); +} + +/** + * Check if review ratings are required. + * + * @since 3.6.0 + * @return bool + */ +function wc_review_ratings_required() { + return 'yes' === get_option( 'woocommerce_review_rating_required' ); +} + +/** + * Check if a CSV file is valid. + * + * @since 3.6.5 + * @param string $file File name. + * @param bool $check_path If should check for the path. + * @return bool + */ +function wc_is_file_valid_csv( $file, $check_path = true ) { + /** + * Filter check for CSV file path. + * + * @since 3.6.4 + * @param bool $check_import_file_path If requires file path check. Defaults to true. + */ + $check_import_file_path = apply_filters( 'woocommerce_csv_importer_check_import_file_path', true ); + + if ( $check_path && $check_import_file_path && false !== stripos( $file, '://' ) ) { + return false; + } + + /** + * Filter CSV valid file types. + * + * @since 3.6.5 + * @param array $valid_filetypes List of valid file types. + */ + $valid_filetypes = apply_filters( + 'woocommerce_csv_import_valid_filetypes', + array( + 'csv' => 'text/csv', + 'txt' => 'text/plain', + ) + ); + + $filetype = wp_check_filetype( $file, $valid_filetypes ); + + if ( in_array( $filetype['type'], $valid_filetypes, true ) ) { + return true; + } + + return false; +} diff --git a/includes/wc-core-functions.php b/includes/wc-core-functions.php new file mode 100644 index 0000000..ccc71dc --- /dev/null +++ b/includes/wc-core-functions.php @@ -0,0 +1,2550 @@ + null, + 'customer_id' => null, + 'customer_note' => null, + 'parent' => null, + 'created_via' => null, + 'cart_hash' => null, + 'order_id' => 0, + ); + + try { + $args = wp_parse_args( $args, $default_args ); + $order = new WC_Order( $args['order_id'] ); + + // Update props that were set (not null). + if ( ! is_null( $args['parent'] ) ) { + $order->set_parent_id( absint( $args['parent'] ) ); + } + + if ( ! is_null( $args['status'] ) ) { + $order->set_status( $args['status'] ); + } + + if ( ! is_null( $args['customer_note'] ) ) { + $order->set_customer_note( $args['customer_note'] ); + } + + if ( ! is_null( $args['customer_id'] ) ) { + $order->set_customer_id( is_numeric( $args['customer_id'] ) ? absint( $args['customer_id'] ) : 0 ); + } + + if ( ! is_null( $args['created_via'] ) ) { + $order->set_created_via( sanitize_text_field( $args['created_via'] ) ); + } + + if ( ! is_null( $args['cart_hash'] ) ) { + $order->set_cart_hash( sanitize_text_field( $args['cart_hash'] ) ); + } + + // Set these fields when creating a new order but not when updating an existing order. + if ( ! $args['order_id'] ) { + $order->set_currency( get_woocommerce_currency() ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->set_customer_ip_address( WC_Geolocation::get_ip_address() ); + $order->set_customer_user_agent( wc_get_user_agent() ); + } + + // Update other order props set automatically. + $order->save(); + } catch ( Exception $e ) { + return new WP_Error( 'error', $e->getMessage() ); + } + + return $order; +} + +/** + * Update an order. Uses wc_create_order. + * + * @param array $args Order arguments. + * @return WC_Order|WP_Error + */ +function wc_update_order( $args ) { + if ( empty( $args['order_id'] ) ) { + return new WP_Error( __( 'Invalid order ID.', 'woocommerce' ) ); + } + return wc_create_order( $args ); +} + +/** + * Given a path, this will convert any of the subpaths into their corresponding tokens. + * + * @since 4.3.0 + * @param string $path The absolute path to tokenize. + * @param array $path_tokens An array keyed with the token, containing paths that should be replaced. + * @return string The tokenized path. + */ +function wc_tokenize_path( $path, $path_tokens ) { + // Order most to least specific so that the token can encompass as much of the path as possible. + uasort( + $path_tokens, + function ( $a, $b ) { + $a = strlen( $a ); + $b = strlen( $b ); + + if ( $a > $b ) { + return -1; + } + + if ( $b > $a ) { + return 1; + } + + return 0; + } + ); + + foreach ( $path_tokens as $token => $token_path ) { + if ( 0 !== strpos( $path, $token_path ) ) { + continue; + } + + $path = str_replace( $token_path, '{{' . $token . '}}', $path ); + } + + return $path; +} + +/** + * Given a tokenized path, this will expand the tokens to their full path. + * + * @since 4.3.0 + * @param string $path The absolute path to expand. + * @param array $path_tokens An array keyed with the token, containing paths that should be expanded. + * @return string The absolute path. + */ +function wc_untokenize_path( $path, $path_tokens ) { + foreach ( $path_tokens as $token => $token_path ) { + $path = str_replace( '{{' . $token . '}}', $token_path, $path ); + } + + return $path; +} + +/** + * Fetches an array containing all of the configurable path constants to be used in tokenization. + * + * @return array The key is the define and the path is the constant. + */ +function wc_get_path_define_tokens() { + $defines = array( + 'ABSPATH', + 'WP_CONTENT_DIR', + 'WP_PLUGIN_DIR', + 'WPMU_PLUGIN_DIR', + 'PLUGINDIR', + 'WP_THEME_DIR', + ); + + $path_tokens = array(); + foreach ( $defines as $define ) { + if ( defined( $define ) ) { + $path_tokens[ $define ] = constant( $define ); + } + } + + return apply_filters( 'woocommerce_get_path_define_tokens', $path_tokens ); +} + +/** + * Get template part (for templates like the shop-loop). + * + * WC_TEMPLATE_DEBUG_MODE will prevent overrides in themes from taking priority. + * + * @param mixed $slug Template slug. + * @param string $name Template name (default: ''). + */ +function wc_get_template_part( $slug, $name = '' ) { + $cache_key = sanitize_key( implode( '-', array( 'template-part', $slug, $name, Constants::get_constant( 'WC_VERSION' ) ) ) ); + $template = (string) wp_cache_get( $cache_key, 'woocommerce' ); + + if ( ! $template ) { + if ( $name ) { + $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template( + array( + "{$slug}-{$name}.php", + WC()->template_path() . "{$slug}-{$name}.php", + ) + ); + + if ( ! $template ) { + $fallback = WC()->plugin_path() . "/templates/{$slug}-{$name}.php"; + $template = file_exists( $fallback ) ? $fallback : ''; + } + } + + if ( ! $template ) { + // If template file doesn't exist, look in yourtheme/slug.php and yourtheme/woocommerce/slug.php. + $template = WC_TEMPLATE_DEBUG_MODE ? '' : locate_template( + array( + "{$slug}.php", + WC()->template_path() . "{$slug}.php", + ) + ); + } + + // Don't cache the absolute path so that it can be shared between web servers with different paths. + $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() ); + + wc_set_template_cache( $cache_key, $cache_path ); + } else { + // Make sure that the absolute path to the template is resolved. + $template = wc_untokenize_path( $template, wc_get_path_define_tokens() ); + } + + // Allow 3rd party plugins to filter template file from their plugin. + $template = apply_filters( 'wc_get_template_part', $template, $slug, $name ); + + if ( $template ) { + load_template( $template, false ); + } +} + +/** + * Get other templates (e.g. product attributes) passing attributes and including the file. + * + * @param string $template_name Template name. + * @param array $args Arguments. (default: array). + * @param string $template_path Template path. (default: ''). + * @param string $default_path Default path. (default: ''). + */ +function wc_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) { + $cache_key = sanitize_key( implode( '-', array( 'template', $template_name, $template_path, $default_path, Constants::get_constant( 'WC_VERSION' ) ) ) ); + $template = (string) wp_cache_get( $cache_key, 'woocommerce' ); + + if ( ! $template ) { + $template = wc_locate_template( $template_name, $template_path, $default_path ); + + // Don't cache the absolute path so that it can be shared between web servers with different paths. + $cache_path = wc_tokenize_path( $template, wc_get_path_define_tokens() ); + + wc_set_template_cache( $cache_key, $cache_path ); + } else { + // Make sure that the absolute path to the template is resolved. + $template = wc_untokenize_path( $template, wc_get_path_define_tokens() ); + } + + // Allow 3rd party plugin filter template file from their plugin. + $filter_template = apply_filters( 'wc_get_template', $template, $template_name, $args, $template_path, $default_path ); + + if ( $filter_template !== $template ) { + if ( ! file_exists( $filter_template ) ) { + /* translators: %s template */ + wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%s does not exist.', 'woocommerce' ), '' . $filter_template . '' ), '2.1' ); + return; + } + $template = $filter_template; + } + + $action_args = array( + 'template_name' => $template_name, + 'template_path' => $template_path, + 'located' => $template, + 'args' => $args, + ); + + if ( ! empty( $args ) && is_array( $args ) ) { + if ( isset( $args['action_args'] ) ) { + wc_doing_it_wrong( + __FUNCTION__, + __( 'action_args should not be overwritten when calling wc_get_template.', 'woocommerce' ), + '3.6.0' + ); + unset( $args['action_args'] ); + } + extract( $args ); // @codingStandardsIgnoreLine + } + + do_action( 'woocommerce_before_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] ); + + include $action_args['located']; + + do_action( 'woocommerce_after_template_part', $action_args['template_name'], $action_args['template_path'], $action_args['located'], $action_args['args'] ); +} + +/** + * Like wc_get_template, but returns the HTML instead of outputting. + * + * @see wc_get_template + * @since 2.5.0 + * @param string $template_name Template name. + * @param array $args Arguments. (default: array). + * @param string $template_path Template path. (default: ''). + * @param string $default_path Default path. (default: ''). + * + * @return string + */ +function wc_get_template_html( $template_name, $args = array(), $template_path = '', $default_path = '' ) { + ob_start(); + wc_get_template( $template_name, $args, $template_path, $default_path ); + return ob_get_clean(); +} +/** + * Locate a template and return the path for inclusion. + * + * This is the load order: + * + * yourtheme/$template_path/$template_name + * yourtheme/$template_name + * $default_path/$template_name + * + * @param string $template_name Template name. + * @param string $template_path Template path. (default: ''). + * @param string $default_path Default path. (default: ''). + * @return string + */ +function wc_locate_template( $template_name, $template_path = '', $default_path = '' ) { + if ( ! $template_path ) { + $template_path = WC()->template_path(); + } + + if ( ! $default_path ) { + $default_path = WC()->plugin_path() . '/templates/'; + } + + // Look within passed path within the theme - this is priority. + if ( false !== strpos( $template_name, 'product_cat' ) || false !== strpos( $template_name, 'product_tag' ) ) { + $cs_template = str_replace( '_', '-', $template_name ); + $template = locate_template( + array( + trailingslashit( $template_path ) . $cs_template, + $cs_template, + ) + ); + } + + if ( empty( $template ) ) { + $template = locate_template( + array( + trailingslashit( $template_path ) . $template_name, + $template_name, + ) + ); + } + + // Get default template/. + if ( ! $template || WC_TEMPLATE_DEBUG_MODE ) { + if ( empty( $cs_template ) ) { + $template = $default_path . $template_name; + } else { + $template = $default_path . $cs_template; + } + } + + // Return what we found. + return apply_filters( 'woocommerce_locate_template', $template, $template_name, $template_path ); +} + +/** + * Add a template to the template cache. + * + * @since 4.3.0 + * @param string $cache_key Object cache key. + * @param string $template Located template. + */ +function wc_set_template_cache( $cache_key, $template ) { + wp_cache_set( $cache_key, $template, 'woocommerce' ); + + $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' ); + if ( is_array( $cached_templates ) ) { + $cached_templates[] = $cache_key; + } else { + $cached_templates = array( $cache_key ); + } + + wp_cache_set( 'cached_templates', $cached_templates, 'woocommerce' ); +} + +/** + * Clear the template cache. + * + * @since 4.3.0 + */ +function wc_clear_template_cache() { + $cached_templates = wp_cache_get( 'cached_templates', 'woocommerce' ); + if ( is_array( $cached_templates ) ) { + foreach ( $cached_templates as $cache_key ) { + wp_cache_delete( $cache_key, 'woocommerce' ); + } + + wp_cache_delete( 'cached_templates', 'woocommerce' ); + } +} + +/** + * Get Base Currency Code. + * + * @return string + */ +function get_woocommerce_currency() { + return apply_filters( 'woocommerce_currency', get_option( 'woocommerce_currency' ) ); +} + +/** + * Get full list of currency codes. + * + * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) + * + * @return array + */ +function get_woocommerce_currencies() { + static $currencies; + + if ( ! isset( $currencies ) ) { + $currencies = array_unique( + apply_filters( + 'woocommerce_currencies', + array( + 'AED' => __( 'United Arab Emirates dirham', 'woocommerce' ), + 'AFN' => __( 'Afghan afghani', 'woocommerce' ), + 'ALL' => __( 'Albanian lek', 'woocommerce' ), + 'AMD' => __( 'Armenian dram', 'woocommerce' ), + 'ANG' => __( 'Netherlands Antillean guilder', 'woocommerce' ), + 'AOA' => __( 'Angolan kwanza', 'woocommerce' ), + 'ARS' => __( 'Argentine peso', 'woocommerce' ), + 'AUD' => __( 'Australian dollar', 'woocommerce' ), + 'AWG' => __( 'Aruban florin', 'woocommerce' ), + 'AZN' => __( 'Azerbaijani manat', 'woocommerce' ), + 'BAM' => __( 'Bosnia and Herzegovina convertible mark', 'woocommerce' ), + 'BBD' => __( 'Barbadian dollar', 'woocommerce' ), + 'BDT' => __( 'Bangladeshi taka', 'woocommerce' ), + 'BGN' => __( 'Bulgarian lev', 'woocommerce' ), + 'BHD' => __( 'Bahraini dinar', 'woocommerce' ), + 'BIF' => __( 'Burundian franc', 'woocommerce' ), + 'BMD' => __( 'Bermudian dollar', 'woocommerce' ), + 'BND' => __( 'Brunei dollar', 'woocommerce' ), + 'BOB' => __( 'Bolivian boliviano', 'woocommerce' ), + 'BRL' => __( 'Brazilian real', 'woocommerce' ), + 'BSD' => __( 'Bahamian dollar', 'woocommerce' ), + 'BTC' => __( 'Bitcoin', 'woocommerce' ), + 'BTN' => __( 'Bhutanese ngultrum', 'woocommerce' ), + 'BWP' => __( 'Botswana pula', 'woocommerce' ), + 'BYR' => __( 'Belarusian ruble (old)', 'woocommerce' ), + 'BYN' => __( 'Belarusian ruble', 'woocommerce' ), + 'BZD' => __( 'Belize dollar', 'woocommerce' ), + 'CAD' => __( 'Canadian dollar', 'woocommerce' ), + 'CDF' => __( 'Congolese franc', 'woocommerce' ), + 'CHF' => __( 'Swiss franc', 'woocommerce' ), + 'CLP' => __( 'Chilean peso', 'woocommerce' ), + 'CNY' => __( 'Chinese yuan', 'woocommerce' ), + 'COP' => __( 'Colombian peso', 'woocommerce' ), + 'CRC' => __( 'Costa Rican colón', 'woocommerce' ), + 'CUC' => __( 'Cuban convertible peso', 'woocommerce' ), + 'CUP' => __( 'Cuban peso', 'woocommerce' ), + 'CVE' => __( 'Cape Verdean escudo', 'woocommerce' ), + 'CZK' => __( 'Czech koruna', 'woocommerce' ), + 'DJF' => __( 'Djiboutian franc', 'woocommerce' ), + 'DKK' => __( 'Danish krone', 'woocommerce' ), + 'DOP' => __( 'Dominican peso', 'woocommerce' ), + 'DZD' => __( 'Algerian dinar', 'woocommerce' ), + 'EGP' => __( 'Egyptian pound', 'woocommerce' ), + 'ERN' => __( 'Eritrean nakfa', 'woocommerce' ), + 'ETB' => __( 'Ethiopian birr', 'woocommerce' ), + 'EUR' => __( 'Euro', 'woocommerce' ), + 'FJD' => __( 'Fijian dollar', 'woocommerce' ), + 'FKP' => __( 'Falkland Islands pound', 'woocommerce' ), + 'GBP' => __( 'Pound sterling', 'woocommerce' ), + 'GEL' => __( 'Georgian lari', 'woocommerce' ), + 'GGP' => __( 'Guernsey pound', 'woocommerce' ), + 'GHS' => __( 'Ghana cedi', 'woocommerce' ), + 'GIP' => __( 'Gibraltar pound', 'woocommerce' ), + 'GMD' => __( 'Gambian dalasi', 'woocommerce' ), + 'GNF' => __( 'Guinean franc', 'woocommerce' ), + 'GTQ' => __( 'Guatemalan quetzal', 'woocommerce' ), + 'GYD' => __( 'Guyanese dollar', 'woocommerce' ), + 'HKD' => __( 'Hong Kong dollar', 'woocommerce' ), + 'HNL' => __( 'Honduran lempira', 'woocommerce' ), + 'HRK' => __( 'Croatian kuna', 'woocommerce' ), + 'HTG' => __( 'Haitian gourde', 'woocommerce' ), + 'HUF' => __( 'Hungarian forint', 'woocommerce' ), + 'IDR' => __( 'Indonesian rupiah', 'woocommerce' ), + 'ILS' => __( 'Israeli new shekel', 'woocommerce' ), + 'IMP' => __( 'Manx pound', 'woocommerce' ), + 'INR' => __( 'Indian rupee', 'woocommerce' ), + 'IQD' => __( 'Iraqi dinar', 'woocommerce' ), + 'IRR' => __( 'Iranian rial', 'woocommerce' ), + 'IRT' => __( 'Iranian toman', 'woocommerce' ), + 'ISK' => __( 'Icelandic króna', 'woocommerce' ), + 'JEP' => __( 'Jersey pound', 'woocommerce' ), + 'JMD' => __( 'Jamaican dollar', 'woocommerce' ), + 'JOD' => __( 'Jordanian dinar', 'woocommerce' ), + 'JPY' => __( 'Japanese yen', 'woocommerce' ), + 'KES' => __( 'Kenyan shilling', 'woocommerce' ), + 'KGS' => __( 'Kyrgyzstani som', 'woocommerce' ), + 'KHR' => __( 'Cambodian riel', 'woocommerce' ), + 'KMF' => __( 'Comorian franc', 'woocommerce' ), + 'KPW' => __( 'North Korean won', 'woocommerce' ), + 'KRW' => __( 'South Korean won', 'woocommerce' ), + 'KWD' => __( 'Kuwaiti dinar', 'woocommerce' ), + 'KYD' => __( 'Cayman Islands dollar', 'woocommerce' ), + 'KZT' => __( 'Kazakhstani tenge', 'woocommerce' ), + 'LAK' => __( 'Lao kip', 'woocommerce' ), + 'LBP' => __( 'Lebanese pound', 'woocommerce' ), + 'LKR' => __( 'Sri Lankan rupee', 'woocommerce' ), + 'LRD' => __( 'Liberian dollar', 'woocommerce' ), + 'LSL' => __( 'Lesotho loti', 'woocommerce' ), + 'LYD' => __( 'Libyan dinar', 'woocommerce' ), + 'MAD' => __( 'Moroccan dirham', 'woocommerce' ), + 'MDL' => __( 'Moldovan leu', 'woocommerce' ), + 'MGA' => __( 'Malagasy ariary', 'woocommerce' ), + 'MKD' => __( 'Macedonian denar', 'woocommerce' ), + 'MMK' => __( 'Burmese kyat', 'woocommerce' ), + 'MNT' => __( 'Mongolian tögrög', 'woocommerce' ), + 'MOP' => __( 'Macanese pataca', 'woocommerce' ), + 'MRU' => __( 'Mauritanian ouguiya', 'woocommerce' ), + 'MUR' => __( 'Mauritian rupee', 'woocommerce' ), + 'MVR' => __( 'Maldivian rufiyaa', 'woocommerce' ), + 'MWK' => __( 'Malawian kwacha', 'woocommerce' ), + 'MXN' => __( 'Mexican peso', 'woocommerce' ), + 'MYR' => __( 'Malaysian ringgit', 'woocommerce' ), + 'MZN' => __( 'Mozambican metical', 'woocommerce' ), + 'NAD' => __( 'Namibian dollar', 'woocommerce' ), + 'NGN' => __( 'Nigerian naira', 'woocommerce' ), + 'NIO' => __( 'Nicaraguan córdoba', 'woocommerce' ), + 'NOK' => __( 'Norwegian krone', 'woocommerce' ), + 'NPR' => __( 'Nepalese rupee', 'woocommerce' ), + 'NZD' => __( 'New Zealand dollar', 'woocommerce' ), + 'OMR' => __( 'Omani rial', 'woocommerce' ), + 'PAB' => __( 'Panamanian balboa', 'woocommerce' ), + 'PEN' => __( 'Sol', 'woocommerce' ), + 'PGK' => __( 'Papua New Guinean kina', 'woocommerce' ), + 'PHP' => __( 'Philippine peso', 'woocommerce' ), + 'PKR' => __( 'Pakistani rupee', 'woocommerce' ), + 'PLN' => __( 'Polish złoty', 'woocommerce' ), + 'PRB' => __( 'Transnistrian ruble', 'woocommerce' ), + 'PYG' => __( 'Paraguayan guaraní', 'woocommerce' ), + 'QAR' => __( 'Qatari riyal', 'woocommerce' ), + 'RON' => __( 'Romanian leu', 'woocommerce' ), + 'RSD' => __( 'Serbian dinar', 'woocommerce' ), + 'RUB' => __( 'Russian ruble', 'woocommerce' ), + 'RWF' => __( 'Rwandan franc', 'woocommerce' ), + 'SAR' => __( 'Saudi riyal', 'woocommerce' ), + 'SBD' => __( 'Solomon Islands dollar', 'woocommerce' ), + 'SCR' => __( 'Seychellois rupee', 'woocommerce' ), + 'SDG' => __( 'Sudanese pound', 'woocommerce' ), + 'SEK' => __( 'Swedish krona', 'woocommerce' ), + 'SGD' => __( 'Singapore dollar', 'woocommerce' ), + 'SHP' => __( 'Saint Helena pound', 'woocommerce' ), + 'SLL' => __( 'Sierra Leonean leone', 'woocommerce' ), + 'SOS' => __( 'Somali shilling', 'woocommerce' ), + 'SRD' => __( 'Surinamese dollar', 'woocommerce' ), + 'SSP' => __( 'South Sudanese pound', 'woocommerce' ), + 'STN' => __( 'São Tomé and Príncipe dobra', 'woocommerce' ), + 'SYP' => __( 'Syrian pound', 'woocommerce' ), + 'SZL' => __( 'Swazi lilangeni', 'woocommerce' ), + 'THB' => __( 'Thai baht', 'woocommerce' ), + 'TJS' => __( 'Tajikistani somoni', 'woocommerce' ), + 'TMT' => __( 'Turkmenistan manat', 'woocommerce' ), + 'TND' => __( 'Tunisian dinar', 'woocommerce' ), + 'TOP' => __( 'Tongan paʻanga', 'woocommerce' ), + 'TRY' => __( 'Turkish lira', 'woocommerce' ), + 'TTD' => __( 'Trinidad and Tobago dollar', 'woocommerce' ), + 'TWD' => __( 'New Taiwan dollar', 'woocommerce' ), + 'TZS' => __( 'Tanzanian shilling', 'woocommerce' ), + 'UAH' => __( 'Ukrainian hryvnia', 'woocommerce' ), + 'UGX' => __( 'Ugandan shilling', 'woocommerce' ), + 'USD' => __( 'United States (US) dollar', 'woocommerce' ), + 'UYU' => __( 'Uruguayan peso', 'woocommerce' ), + 'UZS' => __( 'Uzbekistani som', 'woocommerce' ), + 'VEF' => __( 'Venezuelan bolívar', 'woocommerce' ), + 'VES' => __( 'Bolívar soberano', 'woocommerce' ), + 'VND' => __( 'Vietnamese đồng', 'woocommerce' ), + 'VUV' => __( 'Vanuatu vatu', 'woocommerce' ), + 'WST' => __( 'Samoan tālā', 'woocommerce' ), + 'XAF' => __( 'Central African CFA franc', 'woocommerce' ), + 'XCD' => __( 'East Caribbean dollar', 'woocommerce' ), + 'XOF' => __( 'West African CFA franc', 'woocommerce' ), + 'XPF' => __( 'CFP franc', 'woocommerce' ), + 'YER' => __( 'Yemeni rial', 'woocommerce' ), + 'ZAR' => __( 'South African rand', 'woocommerce' ), + 'ZMW' => __( 'Zambian kwacha', 'woocommerce' ), + ) + ) + ); + } + + return $currencies; +} + +/** + * Get all available Currency symbols. + * + * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) + * + * @since 4.1.0 + * @return array + */ +function get_woocommerce_currency_symbols() { + + $symbols = apply_filters( + 'woocommerce_currency_symbols', + array( + 'AED' => 'د.إ', + 'AFN' => '؋', + 'ALL' => 'L', + 'AMD' => 'AMD', + 'ANG' => 'ƒ', + 'AOA' => 'Kz', + 'ARS' => '$', + 'AUD' => '$', + 'AWG' => 'Afl.', + 'AZN' => 'AZN', + 'BAM' => 'KM', + 'BBD' => '$', + 'BDT' => '৳ ', + 'BGN' => 'лв.', + 'BHD' => '.د.ب', + 'BIF' => 'Fr', + 'BMD' => '$', + 'BND' => '$', + 'BOB' => 'Bs.', + 'BRL' => 'R$', + 'BSD' => '$', + 'BTC' => '฿', + 'BTN' => 'Nu.', + 'BWP' => 'P', + 'BYR' => 'Br', + 'BYN' => 'Br', + 'BZD' => '$', + 'CAD' => '$', + 'CDF' => 'Fr', + 'CHF' => 'CHF', + 'CLP' => '$', + 'CNY' => '¥', + 'COP' => '$', + 'CRC' => '₡', + 'CUC' => '$', + 'CUP' => '$', + 'CVE' => '$', + 'CZK' => 'Kč', + 'DJF' => 'Fr', + 'DKK' => 'DKK', + 'DOP' => 'RD$', + 'DZD' => 'د.ج', + 'EGP' => 'EGP', + 'ERN' => 'Nfk', + 'ETB' => 'Br', + 'EUR' => '€', + 'FJD' => '$', + 'FKP' => '£', + 'GBP' => '£', + 'GEL' => '₾', + 'GGP' => '£', + 'GHS' => '₵', + 'GIP' => '£', + 'GMD' => 'D', + 'GNF' => 'Fr', + 'GTQ' => 'Q', + 'GYD' => '$', + 'HKD' => '$', + 'HNL' => 'L', + 'HRK' => 'kn', + 'HTG' => 'G', + 'HUF' => 'Ft', + 'IDR' => 'Rp', + 'ILS' => '₪', + 'IMP' => '£', + 'INR' => '₹', + 'IQD' => 'ع.د', + 'IRR' => '﷼', + 'IRT' => 'تومان', + 'ISK' => 'kr.', + 'JEP' => '£', + 'JMD' => '$', + 'JOD' => 'د.ا', + 'JPY' => '¥', + 'KES' => 'KSh', + 'KGS' => 'сом', + 'KHR' => '៛', + 'KMF' => 'Fr', + 'KPW' => '₩', + 'KRW' => '₩', + 'KWD' => 'د.ك', + 'KYD' => '$', + 'KZT' => '₸', + 'LAK' => '₭', + 'LBP' => 'ل.ل', + 'LKR' => 'රු', + 'LRD' => '$', + 'LSL' => 'L', + 'LYD' => 'ل.د', + 'MAD' => 'د.م.', + 'MDL' => 'MDL', + 'MGA' => 'Ar', + 'MKD' => 'ден', + 'MMK' => 'Ks', + 'MNT' => '₮', + 'MOP' => 'P', + 'MRU' => 'UM', + 'MUR' => '₨', + 'MVR' => '.ރ', + 'MWK' => 'MK', + 'MXN' => '$', + 'MYR' => 'RM', + 'MZN' => 'MT', + 'NAD' => 'N$', + 'NGN' => '₦', + 'NIO' => 'C$', + 'NOK' => 'kr', + 'NPR' => '₨', + 'NZD' => '$', + 'OMR' => 'ر.ع.', + 'PAB' => 'B/.', + 'PEN' => 'S/', + 'PGK' => 'K', + 'PHP' => '₱', + 'PKR' => '₨', + 'PLN' => 'zł', + 'PRB' => 'р.', + 'PYG' => '₲', + 'QAR' => 'ر.ق', + 'RMB' => '¥', + 'RON' => 'lei', + 'RSD' => 'рсд', + 'RUB' => '₽', + 'RWF' => 'Fr', + 'SAR' => 'ر.س', + 'SBD' => '$', + 'SCR' => '₨', + 'SDG' => 'ج.س.', + 'SEK' => 'kr', + 'SGD' => '$', + 'SHP' => '£', + 'SLL' => 'Le', + 'SOS' => 'Sh', + 'SRD' => '$', + 'SSP' => '£', + 'STN' => 'Db', + 'SYP' => 'ل.س', + 'SZL' => 'L', + 'THB' => '฿', + 'TJS' => 'ЅМ', + 'TMT' => 'm', + 'TND' => 'د.ت', + 'TOP' => 'T$', + 'TRY' => '₺', + 'TTD' => '$', + 'TWD' => 'NT$', + 'TZS' => 'Sh', + 'UAH' => '₴', + 'UGX' => 'UGX', + 'USD' => '$', + 'UYU' => '$', + 'UZS' => 'UZS', + 'VEF' => 'Bs F', + 'VES' => 'Bs.S', + 'VND' => '₫', + 'VUV' => 'Vt', + 'WST' => 'T', + 'XAF' => 'CFA', + 'XCD' => '$', + 'XOF' => 'CFA', + 'XPF' => 'Fr', + 'YER' => '﷼', + 'ZAR' => 'R', + 'ZMW' => 'ZK', + ) + ); + + return $symbols; +} + +/** + * Get Currency symbol. + * + * Currency symbols and names should follow the Unicode CLDR recommendation (http://cldr.unicode.org/translation/currency-names) + * + * @param string $currency Currency. (default: ''). + * @return string + */ +function get_woocommerce_currency_symbol( $currency = '' ) { + if ( ! $currency ) { + $currency = get_woocommerce_currency(); + } + + $symbols = get_woocommerce_currency_symbols(); + + $currency_symbol = isset( $symbols[ $currency ] ) ? $symbols[ $currency ] : ''; + + return apply_filters( 'woocommerce_currency_symbol', $currency_symbol, $currency ); +} + +/** + * Send HTML emails from WooCommerce. + * + * @param mixed $to Receiver. + * @param mixed $subject Subject. + * @param mixed $message Message. + * @param string $headers Headers. (default: "Content-Type: text/html\r\n"). + * @param string $attachments Attachments. (default: ""). + * @return bool + */ +function wc_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = '' ) { + $mailer = WC()->mailer(); + + return $mailer->send( $to, $subject, $message, $headers, $attachments ); +} + +/** + * Return "theme support" values from the current theme, if set. + * + * @since 3.3.0 + * @param string $prop Name of prop (or key::subkey for arrays of props) if you want a specific value. Leave blank to get all props as an array. + * @param mixed $default Optional value to return if the theme does not declare support for a prop. + * @return mixed Value of prop(s). + */ +function wc_get_theme_support( $prop = '', $default = null ) { + $theme_support = get_theme_support( 'woocommerce' ); + $theme_support = is_array( $theme_support ) ? $theme_support[0] : false; + + if ( ! $theme_support ) { + return $default; + } + + if ( $prop ) { + $prop_stack = explode( '::', $prop ); + $prop_key = array_shift( $prop_stack ); + + if ( isset( $theme_support[ $prop_key ] ) ) { + $value = $theme_support[ $prop_key ]; + + if ( count( $prop_stack ) ) { + foreach ( $prop_stack as $prop_key ) { + if ( is_array( $value ) && isset( $value[ $prop_key ] ) ) { + $value = $value[ $prop_key ]; + } else { + $value = $default; + break; + } + } + } + } else { + $value = $default; + } + + return $value; + } + + return $theme_support; +} + +/** + * Get an image size by name or defined dimensions. + * + * The returned variable is filtered by woocommerce_get_image_size_{image_size} filter to + * allow 3rd party customisation. + * + * Sizes defined by the theme take priority over settings. Settings are hidden when a theme + * defines sizes. + * + * @param array|string $image_size Name of the image size to get, or an array of dimensions. + * @return array Array of dimensions including width, height, and cropping mode. Cropping mode is 0 for no crop, and 1 for hard crop. + */ +function wc_get_image_size( $image_size ) { + $cache_key = 'size-' . ( is_array( $image_size ) ? implode( '-', $image_size ) : $image_size ); + $size = wp_cache_get( $cache_key, 'woocommerce' ); + + if ( $size ) { + return $size; + } + + $size = array( + 'width' => 600, + 'height' => 600, + 'crop' => 1, + ); + + if ( is_array( $image_size ) ) { + $size = array( + 'width' => isset( $image_size[0] ) ? absint( $image_size[0] ) : 600, + 'height' => isset( $image_size[1] ) ? absint( $image_size[1] ) : 600, + 'crop' => isset( $image_size[2] ) ? absint( $image_size[2] ) : 1, + ); + $image_size = $size['width'] . '_' . $size['height']; + } else { + $image_size = str_replace( 'woocommerce_', '', $image_size ); + + // Legacy size mapping. + if ( 'shop_single' === $image_size ) { + $image_size = 'single'; + } elseif ( 'shop_catalog' === $image_size ) { + $image_size = 'thumbnail'; + } elseif ( 'shop_thumbnail' === $image_size ) { + $image_size = 'gallery_thumbnail'; + } + + if ( 'single' === $image_size ) { + $size['width'] = absint( wc_get_theme_support( 'single_image_width', get_option( 'woocommerce_single_image_width', 600 ) ) ); + $size['height'] = ''; + $size['crop'] = 0; + + } elseif ( 'gallery_thumbnail' === $image_size ) { + $size['width'] = absint( wc_get_theme_support( 'gallery_thumbnail_image_width', 100 ) ); + $size['height'] = $size['width']; + $size['crop'] = 1; + + } elseif ( 'thumbnail' === $image_size ) { + $size['width'] = absint( wc_get_theme_support( 'thumbnail_image_width', get_option( 'woocommerce_thumbnail_image_width', 300 ) ) ); + $cropping = get_option( 'woocommerce_thumbnail_cropping', '1:1' ); + + if ( 'uncropped' === $cropping ) { + $size['height'] = ''; + $size['crop'] = 0; + } elseif ( 'custom' === $cropping ) { + $width = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_width', '4' ) ); + $height = max( 1, (float) get_option( 'woocommerce_thumbnail_cropping_custom_height', '3' ) ); + $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) ); + $size['crop'] = 1; + } else { + $cropping_split = explode( ':', $cropping ); + $width = max( 1, (float) current( $cropping_split ) ); + $height = max( 1, (float) end( $cropping_split ) ); + $size['height'] = absint( NumberUtil::round( ( $size['width'] / $width ) * $height ) ); + $size['crop'] = 1; + } + } + } + + $size = apply_filters( 'woocommerce_get_image_size_' . $image_size, $size ); + + wp_cache_set( $cache_key, $size, 'woocommerce' ); + + return $size; +} + +/** + * Queue some JavaScript code to be output in the footer. + * + * @param string $code Code. + */ +function wc_enqueue_js( $code ) { + global $wc_queued_js; + + if ( empty( $wc_queued_js ) ) { + $wc_queued_js = ''; + } + + $wc_queued_js .= "\n" . $code . "\n"; +} + +/** + * Output any queued javascript code in the footer. + */ +function wc_print_js() { + global $wc_queued_js; + + if ( ! empty( $wc_queued_js ) ) { + // Sanitize. + $wc_queued_js = wp_check_invalid_utf8( $wc_queued_js ); + $wc_queued_js = preg_replace( '/&#(x)?0*(?(1)27|39);?/i', "'", $wc_queued_js ); + $wc_queued_js = str_replace( "\r", '', $wc_queued_js ); + + $js = "\n\n"; + + /** + * Queued jsfilter. + * + * @since 2.6.0 + * @param string $js JavaScript code. + */ + echo apply_filters( 'woocommerce_queued_js', $js ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + unset( $wc_queued_js ); + } +} + +/** + * Set a cookie - wrapper for setcookie using WP constants. + * + * @param string $name Name of the cookie being set. + * @param string $value Value of the cookie. + * @param integer $expire Expiry of the cookie. + * @param bool $secure Whether the cookie should be served only over https. + * @param bool $httponly Whether the cookie is only accessible over HTTP, not scripting languages like JavaScript. @since 3.6.0. + */ +function wc_setcookie( $name, $value, $expire = 0, $secure = false, $httponly = false ) { + if ( ! headers_sent() ) { + setcookie( $name, $value, $expire, COOKIEPATH ? COOKIEPATH : '/', COOKIE_DOMAIN, $secure, apply_filters( 'woocommerce_cookie_httponly', $httponly, $name, $value, $expire, $secure ) ); + } elseif ( Constants::is_true( 'WP_DEBUG' ) ) { + headers_sent( $file, $line ); + trigger_error( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}", E_USER_NOTICE ); // @codingStandardsIgnoreLine + } +} + +/** + * Get the URL to the WooCommerce REST API. + * + * @since 2.1 + * @param string $path an endpoint to include in the URL. + * @return string the URL. + */ +function get_woocommerce_api_url( $path ) { + if ( Constants::is_defined( 'WC_API_REQUEST_VERSION' ) ) { + $version = Constants::get_constant( 'WC_API_REQUEST_VERSION' ); + } else { + $version = substr( WC_API::VERSION, 0, 1 ); + } + + $url = get_home_url( null, "wc-api/v{$version}/", is_ssl() ? 'https' : 'http' ); + + if ( ! empty( $path ) && is_string( $path ) ) { + $url .= ltrim( $path, '/' ); + } + + return $url; +} + +/** + * Get a log file path. + * + * @since 2.2 + * + * @param string $handle name. + * @return string the log file path. + */ +function wc_get_log_file_path( $handle ) { + return WC_Log_Handler_File::get_log_file_path( $handle ); +} + +/** + * Get a log file name. + * + * @since 3.3 + * + * @param string $handle Name. + * @return string The log file name. + */ +function wc_get_log_file_name( $handle ) { + return WC_Log_Handler_File::get_log_file_name( $handle ); +} + +/** + * Recursively get page children. + * + * @param int $page_id Page ID. + * @return int[] + */ +function wc_get_page_children( $page_id ) { + $page_ids = get_posts( + array( + 'post_parent' => $page_id, + 'post_type' => 'page', + 'numberposts' => -1, // @codingStandardsIgnoreLine + 'post_status' => 'any', + 'fields' => 'ids', + ) + ); + + if ( ! empty( $page_ids ) ) { + foreach ( $page_ids as $page_id ) { + $page_ids = array_merge( $page_ids, wc_get_page_children( $page_id ) ); + } + } + + return $page_ids; +} + +/** + * Flushes rewrite rules when the shop page (or it's children) gets saved. + */ +function flush_rewrite_rules_on_shop_page_save() { + $screen = get_current_screen(); + $screen_id = $screen ? $screen->id : ''; + + // Check if this is the edit page. + if ( 'page' !== $screen_id ) { + return; + } + + // Check if page is edited. + if ( empty( $_GET['post'] ) || empty( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + + $post_id = intval( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $shop_page_id = wc_get_page_id( 'shop' ); + + if ( $shop_page_id === $post_id || in_array( $post_id, wc_get_page_children( $shop_page_id ), true ) ) { + do_action( 'woocommerce_flush_rewrite_rules' ); + } +} +add_action( 'admin_footer', 'flush_rewrite_rules_on_shop_page_save' ); + +/** + * Various rewrite rule fixes. + * + * @since 2.2 + * @param array $rules Rules. + * @return array + */ +function wc_fix_rewrite_rules( $rules ) { + global $wp_rewrite; + + $permalinks = wc_get_permalink_structure(); + + // Fix the rewrite rules when the product permalink have %product_cat% flag. + if ( preg_match( '`/(.+)(/%product_cat%)`', $permalinks['product_rewrite_slug'], $matches ) ) { + foreach ( $rules as $rule => $rewrite ) { + if ( preg_match( '`^' . preg_quote( $matches[1], '`' ) . '/\(`', $rule ) && preg_match( '/^(index\.php\?product_cat)(?!(.*product))/', $rewrite ) ) { + unset( $rules[ $rule ] ); + } + } + } + + // If the shop page is used as the base, we need to handle shop page subpages to avoid 404s. + if ( ! $permalinks['use_verbose_page_rules'] ) { + return $rules; + } + + $shop_page_id = wc_get_page_id( 'shop' ); + if ( $shop_page_id ) { + $page_rewrite_rules = array(); + $subpages = wc_get_page_children( $shop_page_id ); + + // Subpage rules. + foreach ( $subpages as $subpage ) { + $uri = get_page_uri( $subpage ); + $page_rewrite_rules[ $uri . '/?$' ] = 'index.php?pagename=' . $uri; + $wp_generated_rewrite_rules = $wp_rewrite->generate_rewrite_rules( $uri, EP_PAGES, true, true, false, false ); + foreach ( $wp_generated_rewrite_rules as $key => $value ) { + $wp_generated_rewrite_rules[ $key ] = $value . '&pagename=' . $uri; + } + $page_rewrite_rules = array_merge( $page_rewrite_rules, $wp_generated_rewrite_rules ); + } + + // Merge with rules. + $rules = array_merge( $page_rewrite_rules, $rules ); + } + + return $rules; +} +add_filter( 'rewrite_rules_array', 'wc_fix_rewrite_rules' ); + +/** + * Prevent product attachment links from breaking when using complex rewrite structures. + * + * @param string $link Link. + * @param int $post_id Post ID. + * @return string + */ +function wc_fix_product_attachment_link( $link, $post_id ) { + $parent_type = get_post_type( wp_get_post_parent_id( $post_id ) ); + if ( 'product' === $parent_type || 'product_variation' === $parent_type ) { + $link = home_url( '/?attachment_id=' . $post_id ); + } + return $link; +} +add_filter( 'attachment_link', 'wc_fix_product_attachment_link', 10, 2 ); + +/** + * Protect downloads from ms-files.php in multisite. + * + * @param string $rewrite rewrite rules. + * @return string + */ +function wc_ms_protect_download_rewite_rules( $rewrite ) { + if ( ! is_multisite() || 'redirect' === get_option( 'woocommerce_file_download_method' ) ) { + return $rewrite; + } + + $rule = "\n# WooCommerce Rules - Protect Files from ms-files.php\n\n"; + $rule .= "\n"; + $rule .= "RewriteEngine On\n"; + $rule .= "RewriteCond %{QUERY_STRING} file=woocommerce_uploads/ [NC]\n"; + $rule .= "RewriteRule /ms-files.php$ - [F]\n"; + $rule .= "\n\n"; + + return $rule . $rewrite; +} +add_filter( 'mod_rewrite_rules', 'wc_ms_protect_download_rewite_rules' ); + +/** + * Formats a string in the format COUNTRY:STATE into an array. + * + * @since 2.3.0 + * @param string $country_string Country string. + * @return array + */ +function wc_format_country_state_string( $country_string ) { + if ( strstr( $country_string, ':' ) ) { + list( $country, $state ) = explode( ':', $country_string ); + } else { + $country = $country_string; + $state = ''; + } + return array( + 'country' => $country, + 'state' => $state, + ); +} + +/** + * Get the store's base location. + * + * @since 2.3.0 + * @return array + */ +function wc_get_base_location() { + $default = apply_filters( 'woocommerce_get_base_location', get_option( 'woocommerce_default_country', 'US:CA' ) ); + + return wc_format_country_state_string( $default ); +} + +/** + * Get the customer's default location. + * + * Filtered, and set to base location or left blank. If cache-busting, + * this should only be used when 'location' is set in the querystring. + * + * @since 2.3.0 + * @return array + */ +function wc_get_customer_default_location() { + $set_default_location_to = get_option( 'woocommerce_default_customer_address', 'base' ); + $default_location = '' === $set_default_location_to ? '' : get_option( 'woocommerce_default_country', 'US:CA' ); + $location = wc_format_country_state_string( apply_filters( 'woocommerce_customer_default_location', $default_location ) ); + + // Geolocation takes priority if used and if geolocation is possible. + if ( 'geolocation' === $set_default_location_to || 'geolocation_ajax' === $set_default_location_to ) { + $ua = wc_get_user_agent(); + + // Exclude common bots from geolocation by user agent. + if ( ! stristr( $ua, 'bot' ) && ! stristr( $ua, 'spider' ) && ! stristr( $ua, 'crawl' ) ) { + $geolocation = WC_Geolocation::geolocate_ip( '', true, false ); + + if ( ! empty( $geolocation['country'] ) ) { + $location = $geolocation; + } + } + } + + // Once we have a location, ensure it's valid, otherwise fallback to a valid location. + $allowed_country_codes = WC()->countries->get_allowed_countries(); + + if ( ! empty( $location['country'] ) && ! array_key_exists( $location['country'], $allowed_country_codes ) ) { + $location['country'] = current( array_keys( $allowed_country_codes ) ); + $location['state'] = ''; + } + + return apply_filters( 'woocommerce_customer_default_location_array', $location ); +} + +/** + * Get user agent string. + * + * @since 3.0.0 + * @return string + */ +function wc_get_user_agent() { + return isset( $_SERVER['HTTP_USER_AGENT'] ) ? wc_clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // @codingStandardsIgnoreLine +} + +/** + * Generate a rand hash. + * + * @since 2.4.0 + * @return string + */ +function wc_rand_hash() { + if ( ! function_exists( 'openssl_random_pseudo_bytes' ) ) { + return sha1( wp_rand() ); + } + + return bin2hex( openssl_random_pseudo_bytes( 20 ) ); // @codingStandardsIgnoreLine +} + +/** + * WC API - Hash. + * + * @since 2.4.0 + * @param string $data Message to be hashed. + * @return string + */ +function wc_api_hash( $data ) { + return hash_hmac( 'sha256', $data, 'wc-api' ); +} + +/** + * Find all possible combinations of values from the input array and return in a logical order. + * + * @since 2.5.0 + * @param array $input Input. + * @return array + */ +function wc_array_cartesian( $input ) { + $input = array_filter( $input ); + $results = array(); + $indexes = array(); + $index = 0; + + // Generate indexes from keys and values so we have a logical sort order. + foreach ( $input as $key => $values ) { + foreach ( $values as $value ) { + $indexes[ $key ][ $value ] = $index++; + } + } + + // Loop over the 2D array of indexes and generate all combinations. + foreach ( $indexes as $key => $values ) { + // When result is empty, fill with the values of the first looped array. + if ( empty( $results ) ) { + foreach ( $values as $value ) { + $results[] = array( $key => $value ); + } + } else { + // Second and subsequent input sub-array merging. + foreach ( $results as $result_key => $result ) { + foreach ( $values as $value ) { + // If the key is not set, we can set it. + if ( ! isset( $results[ $result_key ][ $key ] ) ) { + $results[ $result_key ][ $key ] = $value; + } else { + // If the key is set, we can add a new combination to the results array. + $new_combination = $results[ $result_key ]; + $new_combination[ $key ] = $value; + $results[] = $new_combination; + } + } + } + } + } + + // Sort the indexes. + arsort( $results ); + + // Convert indexes back to values. + foreach ( $results as $result_key => $result ) { + $converted_values = array(); + + // Sort the values. + arsort( $results[ $result_key ] ); + + // Convert the values. + foreach ( $results[ $result_key ] as $key => $value ) { + $converted_values[ $key ] = array_search( $value, $indexes[ $key ], true ); + } + + $results[ $result_key ] = $converted_values; + } + + return $results; +} + +/** + * Run a MySQL transaction query, if supported. + * + * @since 2.5.0 + * @param string $type Types: start (default), commit, rollback. + * @param bool $force use of transactions. + */ +function wc_transaction_query( $type = 'start', $force = false ) { + global $wpdb; + + $wpdb->hide_errors(); + + wc_maybe_define_constant( 'WC_USE_TRANSACTIONS', true ); + + if ( Constants::is_true( 'WC_USE_TRANSACTIONS' ) || $force ) { + switch ( $type ) { + case 'commit': + $wpdb->query( 'COMMIT' ); + break; + case 'rollback': + $wpdb->query( 'ROLLBACK' ); + break; + default: + $wpdb->query( 'START TRANSACTION' ); + break; + } + } +} + +/** + * Gets the url to the cart page. + * + * @since 2.5.0 + * + * @return string Url to cart page + */ +function wc_get_cart_url() { + return apply_filters( 'woocommerce_get_cart_url', wc_get_page_permalink( 'cart' ) ); +} + +/** + * Gets the url to the checkout page. + * + * @since 2.5.0 + * + * @return string Url to checkout page + */ +function wc_get_checkout_url() { + $checkout_url = wc_get_page_permalink( 'checkout' ); + if ( $checkout_url ) { + // Force SSL if needed. + if ( is_ssl() || 'yes' === get_option( 'woocommerce_force_ssl_checkout' ) ) { + $checkout_url = str_replace( 'http:', 'https:', $checkout_url ); + } + } + + return apply_filters( 'woocommerce_get_checkout_url', $checkout_url ); +} + +/** + * Register a shipping method. + * + * @since 1.5.7 + * @param string|object $shipping_method class name (string) or a class object. + */ +function woocommerce_register_shipping_method( $shipping_method ) { + WC()->shipping()->register_shipping_method( $shipping_method ); +} + +if ( ! function_exists( 'wc_get_shipping_zone' ) ) { + /** + * Get the shipping zone matching a given package from the cart. + * + * @since 2.6.0 + * @uses WC_Shipping_Zones::get_zone_matching_package + * @param array $package Shipping package. + * @return WC_Shipping_Zone + */ + function wc_get_shipping_zone( $package ) { + return WC_Shipping_Zones::get_zone_matching_package( $package ); + } +} + +/** + * Get a nice name for credit card providers. + * + * @since 2.6.0 + * @param string $type Provider Slug/Type. + * @return string + */ +function wc_get_credit_card_type_label( $type ) { + // Normalize. + $type = strtolower( $type ); + $type = str_replace( '-', ' ', $type ); + $type = str_replace( '_', ' ', $type ); + + $labels = apply_filters( + 'woocommerce_credit_card_type_labels', + array( + 'mastercard' => __( 'MasterCard', 'woocommerce' ), + 'visa' => __( 'Visa', 'woocommerce' ), + 'discover' => __( 'Discover', 'woocommerce' ), + 'american express' => __( 'American Express', 'woocommerce' ), + 'diners' => __( 'Diners', 'woocommerce' ), + 'jcb' => __( 'JCB', 'woocommerce' ), + ) + ); + + return apply_filters( 'woocommerce_get_credit_card_type_label', ( array_key_exists( $type, $labels ) ? $labels[ $type ] : ucfirst( $type ) ) ); +} + +/** + * Outputs a "back" link so admin screens can easily jump back a page. + * + * @param string $label Title of the page to return to. + * @param string $url URL of the page to return to. + */ +function wc_back_link( $label, $url ) { + echo ''; +} + +/** + * Display a WooCommerce help tip. + * + * @since 2.5.0 + * + * @param string $tip Help tip text. + * @param bool $allow_html Allow sanitized HTML if true or escape. + * @return string + */ +function wc_help_tip( $tip, $allow_html = false ) { + if ( $allow_html ) { + $tip = wc_sanitize_tooltip( $tip ); + } else { + $tip = esc_attr( $tip ); + } + + return ''; +} + +/** + * Return a list of potential postcodes for wildcard searching. + * + * @since 2.6.0 + * @param string $postcode Postcode. + * @param string $country Country to format postcode for matching. + * @return string[] + */ +function wc_get_wildcard_postcodes( $postcode, $country = '' ) { + $formatted_postcode = wc_format_postcode( $postcode, $country ); + $length = function_exists( 'mb_strlen' ) ? mb_strlen( $formatted_postcode ) : strlen( $formatted_postcode ); + $postcodes = array( + $postcode, + $formatted_postcode, + $formatted_postcode . '*', + ); + + for ( $i = 0; $i < $length; $i ++ ) { + $postcodes[] = ( function_exists( 'mb_substr' ) ? mb_substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) : substr( $formatted_postcode, 0, ( $i + 1 ) * -1 ) ) . '*'; + } + + return $postcodes; +} + +/** + * Used by shipping zones and taxes to compare a given $postcode to stored + * postcodes to find matches for numerical ranges, and wildcards. + * + * @since 2.6.0 + * @param string $postcode Postcode you want to match against stored postcodes. + * @param array $objects Array of postcode objects from Database. + * @param string $object_id_key DB column name for the ID. + * @param string $object_compare_key DB column name for the value. + * @param string $country Country from which this postcode belongs. Allows for formatting. + * @return array Array of matching object ID and matching values. + */ +function wc_postcode_location_matcher( $postcode, $objects, $object_id_key, $object_compare_key, $country = '' ) { + $postcode = wc_normalize_postcode( $postcode ); + $wildcard_postcodes = array_map( 'wc_clean', wc_get_wildcard_postcodes( $postcode, $country ) ); + $matches = array(); + + foreach ( $objects as $object ) { + $object_id = $object->$object_id_key; + $compare_against = $object->$object_compare_key; + + // Handle postcodes containing ranges. + if ( strstr( $compare_against, '...' ) ) { + $range = array_map( 'trim', explode( '...', $compare_against ) ); + + if ( 2 !== count( $range ) ) { + continue; + } + + list( $min, $max ) = $range; + + // If the postcode is non-numeric, make it numeric. + if ( ! is_numeric( $min ) || ! is_numeric( $max ) ) { + $compare = wc_make_numeric_postcode( $postcode ); + $min = str_pad( wc_make_numeric_postcode( $min ), strlen( $compare ), '0' ); + $max = str_pad( wc_make_numeric_postcode( $max ), strlen( $compare ), '0' ); + } else { + $compare = $postcode; + } + + if ( $compare >= $min && $compare <= $max ) { + $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array(); + $matches[ $object_id ][] = $compare_against; + } + } elseif ( in_array( $compare_against, $wildcard_postcodes, true ) ) { + // Wildcard and standard comparison. + $matches[ $object_id ] = isset( $matches[ $object_id ] ) ? $matches[ $object_id ] : array(); + $matches[ $object_id ][] = $compare_against; + } + } + + return $matches; +} + +/** + * Gets number of shipping methods currently enabled. Used to identify if + * shipping is configured. + * + * @since 2.6.0 + * @param bool $include_legacy Count legacy shipping methods too. + * @param bool $enabled_only Whether non-legacy shipping methods should be + * restricted to enabled ones. It doesn't affect + * legacy shipping methods. @since 4.3.0. + * @return int + */ +function wc_get_shipping_method_count( $include_legacy = false, $enabled_only = false ) { + global $wpdb; + + $transient_name = $include_legacy ? 'wc_shipping_method_count_legacy' : 'wc_shipping_method_count'; + $transient_version = WC_Cache_Helper::get_transient_version( 'shipping' ); + $transient_value = get_transient( $transient_name ); + + if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { + return absint( $transient_value['value'] ); + } + + $where_clause = $enabled_only ? 'WHERE is_enabled=1' : ''; + $method_count = absint( $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods ${where_clause}" ) ); + + if ( $include_legacy ) { + // Count activated methods that don't support shipping zones. + $methods = WC()->shipping()->get_shipping_methods(); + + foreach ( $methods as $method ) { + if ( isset( $method->enabled ) && 'yes' === $method->enabled && ! $method->supports( 'shipping-zones' ) ) { + $method_count++; + } + } + } + + $transient_value = array( + 'version' => $transient_version, + 'value' => $method_count, + ); + + set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); + + return $method_count; +} + +/** + * Wrapper for set_time_limit to see if it is enabled. + * + * @since 2.6.0 + * @param int $limit Time limit. + */ +function wc_set_time_limit( $limit = 0 ) { + if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved + @set_time_limit( $limit ); // @codingStandardsIgnoreLine + } +} + +/** + * Wrapper for nocache_headers which also disables page caching. + * + * @since 3.2.4 + */ +function wc_nocache_headers() { + WC_Cache_Helper::set_nocache_constants(); + nocache_headers(); +} + +/** + * Used to sort products attributes with uasort. + * + * @since 2.6.0 + * @param array $a First attribute to compare. + * @param array $b Second attribute to compare. + * @return int + */ +function wc_product_attribute_uasort_comparison( $a, $b ) { + $a_position = is_null( $a ) ? null : $a['position']; + $b_position = is_null( $b ) ? null : $b['position']; + return wc_uasort_comparison( $a_position, $b_position ); +} + +/** + * Used to sort shipping zone methods with uasort. + * + * @since 3.0.0 + * @param array $a First shipping zone method to compare. + * @param array $b Second shipping zone method to compare. + * @return int + */ +function wc_shipping_zone_method_order_uasort_comparison( $a, $b ) { + return wc_uasort_comparison( $a->method_order, $b->method_order ); +} + +/** + * User to sort checkout fields based on priority with uasort. + * + * @since 3.5.1 + * @param array $a First field to compare. + * @param array $b Second field to compare. + * @return int + */ +function wc_checkout_fields_uasort_comparison( $a, $b ) { + /* + * We are not guaranteed to get a priority + * setting. So don't compare if they don't + * exist. + */ + if ( ! isset( $a['priority'], $b['priority'] ) ) { + return 0; + } + + return wc_uasort_comparison( $a['priority'], $b['priority'] ); +} + +/** + * User to sort two values with ausort. + * + * @since 3.5.1 + * @param int $a First value to compare. + * @param int $b Second value to compare. + * @return int + */ +function wc_uasort_comparison( $a, $b ) { + if ( $a === $b ) { + return 0; + } + return ( $a < $b ) ? -1 : 1; +} + +/** + * Sort values based on ascii, usefull for special chars in strings. + * + * @param string $a First value. + * @param string $b Second value. + * @return int + */ +function wc_ascii_uasort_comparison( $a, $b ) { + // 'setlocale' is required for compatibility with PHP 8. + // Without it, 'iconv' will return '?'s instead of transliterated characters. + $prev_locale = setlocale( LC_CTYPE, 0 ); + setlocale( LC_ALL, 'C.UTF-8' ); + + // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged + if ( function_exists( 'iconv' ) && defined( 'ICONV_IMPL' ) && @strcasecmp( ICONV_IMPL, 'unknown' ) !== 0 ) { + $a = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $a ); + $b = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $b ); + } + // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged + + setlocale( LC_ALL, $prev_locale ); + return strcmp( $a, $b ); +} + +/** + * Sort array according to current locale rules and maintaining index association. + * By default tries to use Collator from PHP Internationalization Functions if available. + * If PHP Collator class doesn't exists it fallback to removing accepts from a array + * and by sorting with `uasort( $data, 'strcmp' )` giving support for ASCII values. + * + * @since 4.6.0 + * @param array $data List of values to sort. + * @param string $locale Locale. + * @return array + */ +function wc_asort_by_locale( &$data, $locale = '' ) { + // Use Collator if PHP Internationalization Functions (php-intl) is available. + if ( class_exists( 'Collator' ) ) { + try { + $locale = $locale ? $locale : get_locale(); + $collator = new Collator( $locale ); + $collator->asort( $data, Collator::SORT_STRING ); + return $data; + } catch ( IntlException $e ) { + /* + * Just skip if some error got caused. + * It may be caused in installations that doesn't include ICU TZData. + */ + if ( Constants::is_true( 'WP_DEBUG' ) ) { + error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + sprintf( + 'An unexpected error occurred while trying to use PHP Intl Collator class, it may be caused by an incorrect installation of PHP Intl and ICU, and could be fixed by reinstallaing PHP Intl, see more details about PHP Intl installation: %1$s. Error message: %2$s', + 'https://www.php.net/manual/en/intl.installation.php', + $e->getMessage() + ) + ); + } + } + } + + $raw_data = $data; + + array_walk( + $data, + function ( &$value ) { + $value = remove_accents( html_entity_decode( $value ) ); + } + ); + + uasort( $data, 'strcmp' ); + + foreach ( $data as $key => $val ) { + $data[ $key ] = $raw_data[ $key ]; + } + + return $data; +} + +/** + * Get rounding mode for internal tax calculations. + * + * @since 3.2.4 + * @return int + */ +function wc_get_tax_rounding_mode() { + $constant = WC_TAX_ROUNDING_MODE; + + if ( 'auto' === $constant ) { + return 'yes' === get_option( 'woocommerce_prices_include_tax', 'no' ) ? PHP_ROUND_HALF_DOWN : PHP_ROUND_HALF_UP; + } + + return intval( $constant ); +} + +/** + * Get rounding precision for internal WC calculations. + * Will increase the precision of wc_get_price_decimals by 2 decimals, unless WC_ROUNDING_PRECISION is set to a higher number. + * + * @since 2.6.3 + * @return int + */ +function wc_get_rounding_precision() { + $precision = wc_get_price_decimals() + 2; + if ( absint( WC_ROUNDING_PRECISION ) > $precision ) { + $precision = absint( WC_ROUNDING_PRECISION ); + } + return $precision; +} + +/** + * Add precision to a number and return a number. + * + * @since 3.2.0 + * @param float $value Number to add precision to. + * @param bool $round If should round after adding precision. + * @return int|float + */ +function wc_add_number_precision( $value, $round = true ) { + $cent_precision = pow( 10, wc_get_price_decimals() ); + $value = $value * $cent_precision; + return $round ? NumberUtil::round( $value, wc_get_rounding_precision() - wc_get_price_decimals() ) : $value; +} + +/** + * Remove precision from a number and return a float. + * + * @since 3.2.0 + * @param float $value Number to add precision to. + * @return float + */ +function wc_remove_number_precision( $value ) { + $cent_precision = pow( 10, wc_get_price_decimals() ); + return $value / $cent_precision; +} + +/** + * Add precision to an array of number and return an array of int. + * + * @since 3.2.0 + * @param array $value Number to add precision to. + * @param bool $round Should we round after adding precision?. + * @return int|array + */ +function wc_add_number_precision_deep( $value, $round = true ) { + if ( ! is_array( $value ) ) { + return wc_add_number_precision( $value, $round ); + } + + foreach ( $value as $key => $sub_value ) { + $value[ $key ] = wc_add_number_precision_deep( $sub_value, $round ); + } + + return $value; +} + +/** + * Remove precision from an array of number and return an array of int. + * + * @since 3.2.0 + * @param array $value Number to add precision to. + * @return int|array + */ +function wc_remove_number_precision_deep( $value ) { + if ( ! is_array( $value ) ) { + return wc_remove_number_precision( $value ); + } + + foreach ( $value as $key => $sub_value ) { + $value[ $key ] = wc_remove_number_precision_deep( $sub_value ); + } + + return $value; +} + +/** + * Get a shared logger instance. + * + * Use the woocommerce_logging_class filter to change the logging class. You may provide one of the following: + * - a class name which will be instantiated as `new $class` with no arguments + * - an instance which will be used directly as the logger + * In either case, the class or instance *must* implement WC_Logger_Interface. + * + * @see WC_Logger_Interface + * + * @return WC_Logger + */ +function wc_get_logger() { + static $logger = null; + + $class = apply_filters( 'woocommerce_logging_class', 'WC_Logger' ); + + if ( null !== $logger && is_string( $class ) && is_a( $logger, $class ) ) { + return $logger; + } + + $implements = class_implements( $class ); + + if ( is_array( $implements ) && in_array( 'WC_Logger_Interface', $implements, true ) ) { + $logger = is_object( $class ) ? $class : new $class(); + } else { + wc_doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: 1: class name 2: woocommerce_logging_class 3: WC_Logger_Interface */ + __( 'The class %1$s provided by %2$s filter must implement %3$s.', 'woocommerce' ), + '' . esc_html( is_object( $class ) ? get_class( $class ) : $class ) . '', + 'woocommerce_logging_class', + 'WC_Logger_Interface' + ), + '3.0' + ); + + $logger = is_a( $logger, 'WC_Logger' ) ? $logger : new WC_Logger(); + } + + return $logger; +} + +/** + * Trigger logging cleanup using the logging class. + * + * @since 3.4.0 + */ +function wc_cleanup_logs() { + $logger = wc_get_logger(); + + if ( is_callable( array( $logger, 'clear_expired_logs' ) ) ) { + $logger->clear_expired_logs(); + } +} +add_action( 'woocommerce_cleanup_logs', 'wc_cleanup_logs' ); + +/** + * Prints human-readable information about a variable. + * + * Some server environments block some debugging functions. This function provides a safe way to + * turn an expression into a printable, readable form without calling blocked functions. + * + * @since 3.0 + * + * @param mixed $expression The expression to be printed. + * @param bool $return Optional. Default false. Set to true to return the human-readable string. + * @return string|bool False if expression could not be printed. True if the expression was printed. + * If $return is true, a string representation will be returned. + */ +function wc_print_r( $expression, $return = false ) { + $alternatives = array( + array( + 'func' => 'print_r', + 'args' => array( $expression, true ), + ), + array( + 'func' => 'var_export', + 'args' => array( $expression, true ), + ), + array( + 'func' => 'json_encode', + 'args' => array( $expression ), + ), + array( + 'func' => 'serialize', + 'args' => array( $expression ), + ), + ); + + $alternatives = apply_filters( 'woocommerce_print_r_alternatives', $alternatives, $expression ); + + foreach ( $alternatives as $alternative ) { + if ( function_exists( $alternative['func'] ) ) { + $res = $alternative['func']( ...$alternative['args'] ); + if ( $return ) { + return $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + echo $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + return true; + } + } + + return false; +} + +/** + * Registers the default log handler. + * + * @since 3.0 + * @param array $handlers Handlers. + * @return array + */ +function wc_register_default_log_handler( $handlers ) { + $handler_class = Constants::get_constant( 'WC_LOG_HANDLER' ); + if ( ! class_exists( $handler_class ) ) { + $handler_class = WC_Log_Handler_File::class; + } + + array_push( $handlers, new $handler_class() ); + + return $handlers; +} +add_filter( 'woocommerce_register_log_handlers', 'wc_register_default_log_handler' ); + +/** + * Based on wp_list_pluck, this calls a method instead of returning a property. + * + * @since 3.0.0 + * @param array $list List of objects or arrays. + * @param int|string $callback_or_field Callback method from the object to place instead of the entire object. + * @param int|string $index_key Optional. Field from the object to use as keys for the new array. + * Default null. + * @return array Array of values. + */ +function wc_list_pluck( $list, $callback_or_field, $index_key = null ) { + // Use wp_list_pluck if this isn't a callback. + $first_el = current( $list ); + if ( ! is_object( $first_el ) || ! is_callable( array( $first_el, $callback_or_field ) ) ) { + return wp_list_pluck( $list, $callback_or_field, $index_key ); + } + if ( ! $index_key ) { + /* + * This is simple. Could at some point wrap array_column() + * if we knew we had an array of arrays. + */ + foreach ( $list as $key => $value ) { + $list[ $key ] = $value->{$callback_or_field}(); + } + return $list; + } + + /* + * When index_key is not set for a particular item, push the value + * to the end of the stack. This is how array_column() behaves. + */ + $newlist = array(); + foreach ( $list as $value ) { + // Get index. @since 3.2.0 this supports a callback. + if ( is_callable( array( $value, $index_key ) ) ) { + $newlist[ $value->{$index_key}() ] = $value->{$callback_or_field}(); + } elseif ( isset( $value->$index_key ) ) { + $newlist[ $value->$index_key ] = $value->{$callback_or_field}(); + } else { + $newlist[] = $value->{$callback_or_field}(); + } + } + return $newlist; +} + +/** + * Get permalink settings for things like products and taxonomies. + * + * As of 3.3.0, the permalink settings are stored to the option instead of + * being blank and inheritting from the locale. This speeds up page loading + * times by negating the need to switch locales on each page load. + * + * This is more inline with WP core behavior which does not localize slugs. + * + * @since 3.0.0 + * @return array + */ +function wc_get_permalink_structure() { + $saved_permalinks = (array) get_option( 'woocommerce_permalinks', array() ); + $permalinks = wp_parse_args( + array_filter( $saved_permalinks ), + array( + 'product_base' => _x( 'product', 'slug', 'woocommerce' ), + 'category_base' => _x( 'product-category', 'slug', 'woocommerce' ), + 'tag_base' => _x( 'product-tag', 'slug', 'woocommerce' ), + 'attribute_base' => '', + 'use_verbose_page_rules' => false, + ) + ); + + if ( $saved_permalinks !== $permalinks ) { + update_option( 'woocommerce_permalinks', $permalinks ); + } + + $permalinks['product_rewrite_slug'] = untrailingslashit( $permalinks['product_base'] ); + $permalinks['category_rewrite_slug'] = untrailingslashit( $permalinks['category_base'] ); + $permalinks['tag_rewrite_slug'] = untrailingslashit( $permalinks['tag_base'] ); + $permalinks['attribute_rewrite_slug'] = untrailingslashit( $permalinks['attribute_base'] ); + + return $permalinks; +} + +/** + * Switch WooCommerce to site language. + * + * @since 3.1.0 + */ +function wc_switch_to_site_locale() { + if ( function_exists( 'switch_to_locale' ) ) { + switch_to_locale( get_locale() ); + + // Filter on plugin_locale so load_plugin_textdomain loads the correct locale. + add_filter( 'plugin_locale', 'get_locale' ); + + // Init WC locale. + WC()->load_plugin_textdomain(); + } +} + +/** + * Switch WooCommerce language to original. + * + * @since 3.1.0 + */ +function wc_restore_locale() { + if ( function_exists( 'restore_previous_locale' ) ) { + restore_previous_locale(); + + // Remove filter. + remove_filter( 'plugin_locale', 'get_locale' ); + + // Init WC locale. + WC()->load_plugin_textdomain(); + } +} + +/** + * Convert plaintext phone number to clickable phone number. + * + * Remove formatting and allow "+". + * Example and specs: https://developer.mozilla.org/en/docs/Web/HTML/Element/a#Creating_a_phone_link + * + * @since 3.1.0 + * + * @param string $phone Content to convert phone number. + * @return string Content with converted phone number. + */ +function wc_make_phone_clickable( $phone ) { + $number = trim( preg_replace( '/[^\d|\+]/', '', $phone ) ); + + return $number ? '' . esc_html( $phone ) . '' : ''; +} + +/** + * Get an item of post data if set, otherwise return a default value. + * + * @since 3.0.9 + * @param string $key Meta key. + * @param string $default Default value. + * @return mixed Value sanitized by wc_clean. + */ +function wc_get_post_data_by_key( $key, $default = '' ) { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing + return wc_clean( wp_unslash( wc_get_var( $_POST[ $key ], $default ) ) ); +} + +/** + * Get data if set, otherwise return a default value or null. Prevents notices when data is not set. + * + * @since 3.2.0 + * @param mixed $var Variable. + * @param string $default Default value. + * @return mixed + */ +function wc_get_var( &$var, $default = null ) { + return isset( $var ) ? $var : $default; +} + +/** + * Read in WooCommerce headers when reading plugin headers. + * + * @since 3.2.0 + * @param array $headers Headers. + * @return array + */ +function wc_enable_wc_plugin_headers( $headers ) { + if ( ! class_exists( 'WC_Plugin_Updates' ) ) { + include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php'; + } + + // WC requires at least - allows developers to define which version of WooCommerce the plugin requires to run. + $headers[] = WC_Plugin_Updates::VERSION_REQUIRED_HEADER; + + // WC tested up to - allows developers to define which version of WooCommerce they have tested up to. + $headers[] = WC_Plugin_Updates::VERSION_TESTED_HEADER; + + // Woo - This is used in WooCommerce extensions and is picked up by the helper. + $headers[] = 'Woo'; + + return $headers; +} +add_filter( 'extra_theme_headers', 'wc_enable_wc_plugin_headers' ); +add_filter( 'extra_plugin_headers', 'wc_enable_wc_plugin_headers' ); + +/** + * Prevent auto-updating the WooCommerce plugin on major releases if there are untested extensions active. + * + * @since 3.2.0 + * @param bool $should_update If should update. + * @param object $plugin Plugin data. + * @return bool + */ +function wc_prevent_dangerous_auto_updates( $should_update, $plugin ) { + if ( ! isset( $plugin->plugin, $plugin->new_version ) ) { + return $should_update; + } + + if ( 'woocommerce/woocommerce.php' !== $plugin->plugin ) { + return $should_update; + } + + if ( ! class_exists( 'WC_Plugin_Updates' ) ) { + include_once dirname( __FILE__ ) . '/admin/plugin-updates/class-wc-plugin-updates.php'; + } + + $new_version = wc_clean( $plugin->new_version ); + $plugin_updates = new WC_Plugin_Updates(); + $version_type = Constants::get_constant( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ); + if ( ! is_string( $version_type ) ) { + $version_type = 'none'; + } + $untested_plugins = $plugin_updates->get_untested_plugins( $new_version, $version_type ); + if ( ! empty( $untested_plugins ) ) { + return false; + } + + return $should_update; +} +add_filter( 'auto_update_plugin', 'wc_prevent_dangerous_auto_updates', 99, 2 ); + +/** + * Delete expired transients. + * + * Deletes all expired transients. The multi-table delete syntax is used. + * to delete the transient record from table a, and the corresponding. + * transient_timeout record from table b. + * + * Based on code inside core's upgrade_network() function. + * + * @since 3.2.0 + * @return int Number of transients that were cleared. + */ +function wc_delete_expired_transients() { + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b + WHERE a.option_name LIKE %s + AND a.option_name NOT LIKE %s + AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) + AND b.option_value < %d"; + $rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) ); + + $sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b + WHERE a.option_name LIKE %s + AND a.option_name NOT LIKE %s + AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) ) + AND b.option_value < %d"; + $rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + return absint( $rows + $rows2 ); +} +add_action( 'woocommerce_installed', 'wc_delete_expired_transients' ); + +/** + * Make a URL relative, if possible. + * + * @since 3.2.0 + * @param string $url URL to make relative. + * @return string + */ +function wc_get_relative_url( $url ) { + return wc_is_external_resource( $url ) ? $url : str_replace( array( 'http://', 'https://' ), '//', $url ); +} + +/** + * See if a resource is remote. + * + * @since 3.2.0 + * @param string $url URL to check. + * @return bool + */ +function wc_is_external_resource( $url ) { + $wp_base = str_replace( array( 'http://', 'https://' ), '//', get_home_url( null, '/', 'http' ) ); + + return strstr( $url, '://' ) && ! strstr( $url, $wp_base ); +} + +/** + * See if theme/s is activate or not. + * + * @since 3.3.0 + * @param string|array $theme Theme name or array of theme names to check. + * @return boolean + */ +function wc_is_active_theme( $theme ) { + return is_array( $theme ) ? in_array( get_template(), $theme, true ) : get_template() === $theme; +} + +/** + * Is the site using a default WP theme? + * + * @return boolean + */ +function wc_is_wp_default_theme_active() { + return wc_is_active_theme( + array( + 'twentytwentyone', + 'twentytwenty', + 'twentynineteen', + 'twentyseventeen', + 'twentysixteen', + 'twentyfifteen', + 'twentyfourteen', + 'twentythirteen', + 'twentyeleven', + 'twentytwelve', + 'twentyten', + ) + ); +} + +/** + * Cleans up session data - cron callback. + * + * @since 3.3.0 + */ +function wc_cleanup_session_data() { + $session_class = apply_filters( 'woocommerce_session_handler', 'WC_Session_Handler' ); + $session = new $session_class(); + + if ( is_callable( array( $session, 'cleanup_sessions' ) ) ) { + $session->cleanup_sessions(); + } +} +add_action( 'woocommerce_cleanup_sessions', 'wc_cleanup_session_data' ); + +/** + * Convert a decimal (e.g. 3.5) to a fraction (e.g. 7/2). + * From: https://www.designedbyaturtle.co.uk/2015/converting-a-decimal-to-a-fraction-in-php/ + * + * @param float $decimal the decimal number. + * @return array|bool a 1/2 would be [1, 2] array (this can be imploded with '/' to form a string). + */ +function wc_decimal_to_fraction( $decimal ) { + if ( 0 > $decimal || ! is_numeric( $decimal ) ) { + // Negative digits need to be passed in as positive numbers and prefixed as negative once the response is imploded. + return false; + } + + if ( 0 === $decimal ) { + return array( 0, 1 ); + } + + $tolerance = 1.e-4; + $numerator = 1; + $h2 = 0; + $denominator = 0; + $k2 = 1; + $b = 1 / $decimal; + + do { + $b = 1 / $b; + $a = floor( $b ); + $aux = $numerator; + $numerator = $a * $numerator + $h2; + $h2 = $aux; + $aux = $denominator; + $denominator = $a * $denominator + $k2; + $k2 = $aux; + $b = $b - $a; + } while ( abs( $decimal - $numerator / $denominator ) > $decimal * $tolerance ); + + return array( $numerator, $denominator ); +} + +/** + * Round discount. + * + * @param double $value Amount to round. + * @param int $precision DP to round. + * @return float + */ +function wc_round_discount( $value, $precision ) { + if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { + return NumberUtil::round( $value, $precision, WC_DISCOUNT_ROUNDING_MODE ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound + } + + if ( PHP_ROUND_HALF_DOWN === WC_DISCOUNT_ROUNDING_MODE ) { + return wc_legacy_round_half_down( $value, $precision ); + } + + return NumberUtil::round( $value, $precision ); +} + +/** + * Return the html selected attribute if stringified $value is found in array of stringified $options + * or if stringified $value is the same as scalar stringified $options. + * + * @param string|int $value Value to find within options. + * @param string|int|array $options Options to go through when looking for value. + * @return string + */ +function wc_selected( $value, $options ) { + if ( is_array( $options ) ) { + $options = array_map( 'strval', $options ); + return selected( in_array( (string) $value, $options, true ), true, false ); + } + + return selected( $value, $options, false ); +} + +/** + * Retrieves the MySQL server version. Based on $wpdb. + * + * @since 3.4.1 + * @return array Vesion information. + */ +function wc_get_server_database_version() { + global $wpdb; + + if ( empty( $wpdb->is_mysql ) ) { + return array( + 'string' => '', + 'number' => '', + ); + } + + // phpcs:disable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved + if ( $wpdb->use_mysqli ) { + $server_info = mysqli_get_server_info( $wpdb->dbh ); + } else { + $server_info = mysql_get_server_info( $wpdb->dbh ); + } + // phpcs:enable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved + + return array( + 'string' => $server_info, + 'number' => preg_replace( '/([^\d.]+).*/', '', $server_info ), + ); +} + +/** + * Initialize and load the cart functionality. + * + * @since 3.6.4 + * @return void + */ +function wc_load_cart() { + if ( ! did_action( 'before_woocommerce_init' ) || doing_action( 'before_woocommerce_init' ) ) { + /* translators: 1: wc_load_cart 2: woocommerce_init */ + wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s action.', 'woocommerce' ), 'wc_load_cart', 'woocommerce_init' ), '3.7' ); + return; + } + + // Ensure dependencies are loaded in all contexts. + include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; + include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; + + WC()->initialize_session(); + WC()->initialize_cart(); +} + +/** + * Test whether the context of execution comes from async action scheduler. + * + * @since 4.0.0 + * @return bool + */ +function wc_is_running_from_async_action_scheduler() { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action']; +} + +/** + * Polyfill for wp_cache_get_multiple for WP versions before 5.5. + * + * @param array $keys Array of keys to get from group. + * @param string $group Optional. Where the cache contents are grouped. Default empty. + * @param bool $force Optional. Whether to force an update of the local cache from the persistent + * cache. Default false. + * @return array|bool Array of values. + */ +function wc_cache_get_multiple( $keys, $group = '', $force = false ) { + if ( function_exists( 'wp_cache_get_multiple' ) ) { + return wp_cache_get_multiple( $keys, $group, $force ); + } + $values = array(); + foreach ( $keys as $key ) { + $values[ $key ] = wp_cache_get( $key, $group, $force ); + } + return $values; +} diff --git a/includes/wc-coupon-functions.php b/includes/wc-coupon-functions.php new file mode 100644 index 0000000..28ef64a --- /dev/null +++ b/includes/wc-coupon-functions.php @@ -0,0 +1,111 @@ + __( 'Percentage discount', 'woocommerce' ), + 'fixed_cart' => __( 'Fixed cart discount', 'woocommerce' ), + 'fixed_product' => __( 'Fixed product discount', 'woocommerce' ), + ) + ); +} + +/** + * Get a coupon type's name. + * + * @param string $type Coupon type. + * @return string + */ +function wc_get_coupon_type( $type = '' ) { + $types = wc_get_coupon_types(); + return isset( $types[ $type ] ) ? $types[ $type ] : ''; +} + +/** + * Coupon types that apply to individual products. Controls which validation rules will apply. + * + * @since 2.5.0 + * @return array + */ +function wc_get_product_coupon_types() { + return (array) apply_filters( 'woocommerce_product_coupon_types', array( 'fixed_product', 'percent' ) ); +} + +/** + * Coupon types that apply to the cart as a whole. Controls which validation rules will apply. + * + * @since 2.5.0 + * @return array + */ +function wc_get_cart_coupon_types() { + return (array) apply_filters( 'woocommerce_cart_coupon_types', array( 'fixed_cart' ) ); +} + +/** + * Check if coupons are enabled. + * Filterable. + * + * @since 2.5.0 + * + * @return bool + */ +function wc_coupons_enabled() { + return apply_filters( 'woocommerce_coupons_enabled', 'yes' === get_option( 'woocommerce_enable_coupons' ) ); +} + +/** + * Get coupon code by ID. + * + * @since 3.0.0 + * @param int $id Coupon ID. + * @return string + */ +function wc_get_coupon_code_by_id( $id ) { + $data_store = WC_Data_Store::load( 'coupon' ); + return empty( $id ) ? '' : (string) $data_store->get_code_by_id( $id ); +} + +/** + * Get coupon ID by code. + * + * @since 3.0.0 + * @param string $code Coupon code. + * @param int $exclude Used to exclude an ID from the check if you're checking existence. + * @return int + */ +function wc_get_coupon_id_by_code( $code, $exclude = 0 ) { + + if ( empty( $code ) ) { + return 0; + } + + $data_store = WC_Data_Store::load( 'coupon' ); + $ids = wp_cache_get( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, 'coupons' ); + + if ( false === $ids ) { + $ids = $data_store->get_ids_by_code( $code ); + if ( $ids ) { + wp_cache_set( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $code, $ids, 'coupons' ); + } + } + + $ids = array_diff( array_filter( array_map( 'absint', (array) $ids ) ), array( $exclude ) ); + + return apply_filters( 'woocommerce_get_coupon_id_from_code', absint( current( $ids ) ), $code, $exclude ); +} diff --git a/includes/wc-deprecated-functions.php b/includes/wc-deprecated-functions.php new file mode 100644 index 0000000..ea5f57f --- /dev/null +++ b/includes/wc-deprecated-functions.php @@ -0,0 +1,1125 @@ +is_rest_api_request() ) { + do_action( 'deprecated_function_run', $function, $replacement, $version ); + $log_string = "The {$function} function is deprecated since version {$version}."; + $log_string .= $replacement ? " Replace with {$replacement}." : ''; + error_log( $log_string ); + } else { + _deprecated_function( $function, $version, $replacement ); + } + // @codingStandardsIgnoreEnd +} + +/** + * Wrapper for deprecated hook so we can apply some extra logic. + * + * @since 3.3.0 + * @param string $hook The hook that was used. + * @param string $version The version of WordPress that deprecated the hook. + * @param string $replacement The hook that should have been used. + * @param string $message A message regarding the change. + */ +function wc_deprecated_hook( $hook, $version, $replacement = null, $message = null ) { + // @codingStandardsIgnoreStart + if ( is_ajax() || WC()->is_rest_api_request() ) { + do_action( 'deprecated_hook_run', $hook, $replacement, $version, $message ); + + $message = empty( $message ) ? '' : ' ' . $message; + $log_string = "{$hook} is deprecated since version {$version}"; + $log_string .= $replacement ? "! Use {$replacement} instead." : ' with no alternative available.'; + + error_log( $log_string . $message ); + } else { + _deprecated_hook( $hook, $version, $replacement, $message ); + } + // @codingStandardsIgnoreEnd +} + +/** + * When catching an exception, this allows us to log it if unexpected. + * + * @since 3.3.0 + * @param Exception $exception_object The exception object. + * @param string $function The function which threw exception. + * @param array $args The args passed to the function. + */ +function wc_caught_exception( $exception_object, $function = '', $args = array() ) { + // @codingStandardsIgnoreStart + $message = $exception_object->getMessage(); + $message .= '. Args: ' . print_r( $args, true ) . '.'; + + do_action( 'woocommerce_caught_exception', $exception_object, $function, $args ); + error_log( "Exception caught in {$function}. {$message}." ); + // @codingStandardsIgnoreEnd +} + +/** + * Wrapper for _doing_it_wrong(). + * + * @since 3.0.0 + * @param string $function Function used. + * @param string $message Message to log. + * @param string $version Version the message was added in. + */ +function wc_doing_it_wrong( $function, $message, $version ) { + // @codingStandardsIgnoreStart + $message .= ' Backtrace: ' . wp_debug_backtrace_summary(); + + if ( is_ajax() || WC()->is_rest_api_request() ) { + do_action( 'doing_it_wrong_run', $function, $message, $version ); + error_log( "{$function} was called incorrectly. {$message}. This message was added in version {$version}." ); + } else { + _doing_it_wrong( $function, $message, $version ); + } + // @codingStandardsIgnoreEnd +} + +/** + * Wrapper for deprecated arguments so we can apply some extra logic. + * + * @since 3.0.0 + * @param string $argument + * @param string $version + * @param string $replacement + */ +function wc_deprecated_argument( $argument, $version, $message = null ) { + if ( is_ajax() || WC()->is_rest_api_request() ) { + do_action( 'deprecated_argument_run', $argument, $message, $version ); + error_log( "The {$argument} argument is deprecated since version {$version}. {$message}" ); + } else { + _deprecated_argument( $argument, $version, $message ); + } +} + +/** + * @deprecated 2.1 + */ +function woocommerce_show_messages() { + wc_deprecated_function( 'woocommerce_show_messages', '2.1', 'wc_print_notices' ); + wc_print_notices(); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_weekend_area_js() { + wc_deprecated_function( 'woocommerce_weekend_area_js', '2.1' ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_tooltip_js() { + wc_deprecated_function( 'woocommerce_tooltip_js', '2.1' ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_datepicker_js() { + wc_deprecated_function( 'woocommerce_datepicker_js', '2.1' ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_admin_scripts() { + wc_deprecated_function( 'woocommerce_admin_scripts', '2.1' ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_create_page( $slug, $option = '', $page_title = '', $page_content = '', $post_parent = 0 ) { + wc_deprecated_function( 'woocommerce_create_page', '2.1', 'wc_create_page' ); + return wc_create_page( $slug, $option, $page_title, $page_content, $post_parent ); +} + +/** + * @deprecated 2.1 + */ +function woocommerce_readfile_chunked( $file, $retbytes = true ) { + wc_deprecated_function( 'woocommerce_readfile_chunked', '2.1', 'WC_Download_Handler::readfile_chunked()' ); + return WC_Download_Handler::readfile_chunked( $file ); +} + +/** + * Formal total costs - format to the number of decimal places for the base currency. + * + * @access public + * @param mixed $number + * @deprecated 2.1 + * @return string + */ +function woocommerce_format_total( $number ) { + wc_deprecated_function( __FUNCTION__, '2.1', 'wc_format_decimal()' ); + return wc_format_decimal( $number, wc_get_price_decimals(), false ); +} + +/** + * Get product name with extra details such as SKU price and attributes. Used within admin. + * + * @access public + * @param WC_Product $product + * @deprecated 2.1 + * @return string + */ +function woocommerce_get_formatted_product_name( $product ) { + wc_deprecated_function( __FUNCTION__, '2.1', 'WC_Product::get_formatted_name()' ); + return $product->get_formatted_name(); +} + +/** + * Handle IPN requests for the legacy paypal gateway by calling gateways manually if needed. + * + * @access public + */ +function woocommerce_legacy_paypal_ipn() { + if ( ! empty( $_GET['paypalListener'] ) && 'paypal_standard_IPN' === $_GET['paypalListener'] ) { + WC()->payment_gateways(); + do_action( 'woocommerce_api_wc_gateway_paypal' ); + } +} +add_action( 'init', 'woocommerce_legacy_paypal_ipn' ); + +/** + * @deprecated 3.0 + */ +function get_product( $the_product = false, $args = array() ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product' ); + return wc_get_product( $the_product, $args ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_protected_product_add_to_cart( $passed, $product_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_protected_product_add_to_cart' ); + return wc_protected_product_add_to_cart( $passed, $product_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_empty_cart() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_empty_cart' ); + wc_empty_cart(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_load_persistent_cart( $user_login, $user = 0 ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_load_persistent_cart' ); + return wc_load_persistent_cart( $user_login, $user ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_add_to_cart_message( $product_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_to_cart_message' ); + wc_add_to_cart_message( $product_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_clear_cart_after_payment() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clear_cart_after_payment' ); + wc_clear_cart_after_payment(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_subtotal_html() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_subtotal_html' ); + wc_cart_totals_subtotal_html(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_shipping_html() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_html' ); + wc_cart_totals_shipping_html(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_coupon_html( $coupon ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_coupon_html' ); + wc_cart_totals_coupon_html( $coupon ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_order_total_html() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_order_total_html' ); + wc_cart_totals_order_total_html(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_fee_html( $fee ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_fee_html' ); + wc_cart_totals_fee_html( $fee ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cart_totals_shipping_method_label( $method ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cart_totals_shipping_method_label' ); + return wc_cart_totals_shipping_method_label( $method ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_template_part( $slug, $name = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template_part' ); + wc_get_template_part( $slug, $name ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_template' ); + wc_get_template( $template_name, $args, $template_path, $default_path ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_locate_template( $template_name, $template_path = '', $default_path = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_locate_template' ); + return wc_locate_template( $template_name, $template_path, $default_path ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_mail( $to, $subject, $message, $headers = "Content-Type: text/html\r\n", $attachments = "" ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_mail' ); + wc_mail( $to, $subject, $message, $headers, $attachments ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_disable_admin_bar( $show_admin_bar ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_disable_admin_bar' ); + return wc_disable_admin_bar( $show_admin_bar ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_create_new_customer( $email, $username = '', $password = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_create_new_customer' ); + return wc_create_new_customer( $email, $username, $password ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_set_customer_auth_cookie( $customer_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_customer_auth_cookie' ); + wc_set_customer_auth_cookie( $customer_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_update_new_customer_past_orders( $customer_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_new_customer_past_orders' ); + return wc_update_new_customer_past_orders( $customer_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_paying_customer( $order_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_paying_customer' ); + wc_paying_customer( $order_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_customer_bought_product( $customer_email, $user_id, $product_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_bought_product' ); + return wc_customer_bought_product( $customer_email, $user_id, $product_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_customer_has_capability( $allcaps, $caps, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_has_capability' ); + return wc_customer_has_capability( $allcaps, $caps, $args ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_sanitize_taxonomy_name( $taxonomy ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_sanitize_taxonomy_name' ); + return wc_sanitize_taxonomy_name( $taxonomy ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_filename_from_url( $file_url ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_filename_from_url' ); + return wc_get_filename_from_url( $file_url ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_dimension( $dim, $to_unit ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_dimension' ); + return wc_get_dimension( $dim, $to_unit ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_weight( $weight, $to_unit ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_weight' ); + return wc_get_weight( $weight, $to_unit ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_trim_zeros( $price ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_trim_zeros' ); + return wc_trim_zeros( $price ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_round_tax_total( $tax ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_round_tax_total' ); + return wc_round_tax_total( $tax ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_format_decimal( $number, $dp = false, $trim_zeros = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_decimal' ); + return wc_format_decimal( $number, $dp, $trim_zeros ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_clean( $var ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_clean' ); + return wc_clean( $var ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_array_overlay( $a1, $a2 ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_array_overlay' ); + return wc_array_overlay( $a1, $a2 ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_price( $price, $args = array() ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_price' ); + return wc_price( $price, $args ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_let_to_num( $size ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_let_to_num' ); + return wc_let_to_num( $size ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_date_format() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_date_format' ); + return wc_date_format(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_time_format() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_time_format' ); + return wc_time_format(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_timezone_string() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_timezone_string' ); + return wc_timezone_string(); +} + +if ( ! function_exists( 'woocommerce_rgb_from_hex' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_rgb_from_hex( $color ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_rgb_from_hex' ); + return wc_rgb_from_hex( $color ); + } +} + +if ( ! function_exists( 'woocommerce_hex_darker' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_hex_darker( $color, $factor = 30 ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_darker' ); + return wc_hex_darker( $color, $factor ); + } +} + +if ( ! function_exists( 'woocommerce_hex_lighter' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_hex_lighter( $color, $factor = 30 ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_hex_lighter' ); + return wc_hex_lighter( $color, $factor ); + } +} + +if ( ! function_exists( 'woocommerce_light_or_dark' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_light_or_dark' ); + return wc_light_or_dark( $color, $dark, $light ); + } +} + +if ( ! function_exists( 'woocommerce_format_hex' ) ) { + /** + * @deprecated 3.0 + */ + function woocommerce_format_hex( $hex ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_format_hex' ); + return wc_format_hex( $hex ); + } +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_order_id_by_order_key( $order_key ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_id_by_order_key' ); + return wc_get_order_id_by_order_key( $order_key ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_downloadable_file_permission( $download_id, $product_id, $order ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_file_permission' ); + return wc_downloadable_file_permission( $download_id, $product_id, $order ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_downloadable_product_permissions( $order_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_downloadable_product_permissions' ); + wc_downloadable_product_permissions( $order_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_add_order_item( $order_id, $item ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item' ); + return wc_add_order_item( $order_id, $item ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_delete_order_item( $item_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item' ); + return wc_delete_order_item( $item_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_update_order_item_meta' ); + return wc_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_add_order_item_meta' ); + return wc_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_delete_order_item_meta( $item_id, $meta_key, $meta_value = '', $delete_all = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_delete_order_item_meta' ); + return wc_delete_order_item_meta( $item_id, $meta_key, $meta_value, $delete_all ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_order_item_meta( $item_id, $key, $single = true ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_order_item_meta' ); + return wc_get_order_item_meta( $item_id, $key, $single ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_cancel_unpaid_orders() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_cancel_unpaid_orders' ); + wc_cancel_unpaid_orders(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_processing_order_count() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_processing_order_count' ); + return wc_processing_order_count(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_page_id( $page ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_page_id' ); + return wc_get_page_id( $page ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_endpoint_url' ); + return wc_get_endpoint_url( $endpoint, $value, $permalink ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_lostpassword_url( $url ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_lostpassword_url' ); + return wc_lostpassword_url( $url ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_customer_edit_account_url() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_customer_edit_account_url' ); + return wc_customer_edit_account_url(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_nav_menu_items( $items, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_items' ); + return wc_nav_menu_items( $items ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_nav_menu_item_classes( $menu_items, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_nav_menu_item_classes' ); + return wc_nav_menu_item_classes( $menu_items ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_list_pages( $pages ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_list_pages' ); + return wc_list_pages( $pages ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_product_dropdown_categories( $args = array(), $deprecated_hierarchical = 1, $deprecated_show_uncategorized = 1, $deprecated_orderby = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_dropdown_categories' ); + return wc_product_dropdown_categories( $args, $deprecated_hierarchical, $deprecated_show_uncategorized, $deprecated_orderby ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_walk_category_dropdown_tree( $a1 = '', $a2 = '', $a3 = '' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_walk_category_dropdown_tree' ); + return wc_walk_category_dropdown_tree( $a1, $a2, $a3 ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_taxonomy_metadata_wpdbfix() { + wc_deprecated_function( __FUNCTION__, '3.0' ); +} + +/** + * @deprecated 3.0 + */ +function wc_taxonomy_metadata_wpdbfix() { + wc_deprecated_function( __FUNCTION__, '3.0' ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_order_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_reorder_terms' ); + return wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $terms ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_set_term_order' ); + return wc_set_term_order( $term_id, $index, $taxonomy, $recursive ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_terms_clauses( $clauses, $taxonomies, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_terms_clauses' ); + return wc_terms_clauses( $clauses, $taxonomies, $args ); +} + +/** + * @deprecated 3.0 + */ +function _woocommerce_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ) { + wc_deprecated_function( __FUNCTION__, '3.0', '_wc_term_recount' ); + return _wc_term_recount( $terms, $taxonomy, $callback, $terms_are_term_taxonomy_ids ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_recount_after_stock_change( $product_id ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_recount_after_stock_change' ); + return wc_recount_after_stock_change( $product_id ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_change_term_counts( $terms, $taxonomies, $args ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_change_term_counts' ); + return wc_change_term_counts( $terms, $taxonomies ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_product_ids_on_sale() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_ids_on_sale' ); + return wc_get_product_ids_on_sale(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_featured_product_ids() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_featured_product_ids' ); + return wc_get_featured_product_ids(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_product_terms( $object_id, $taxonomy, $fields = 'all' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_product_terms' ); + return wc_get_product_terms( $object_id, $taxonomy, array( 'fields' => $fields ) ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_product_post_type_link( $permalink, $post ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_product_post_type_link' ); + return wc_product_post_type_link( $permalink, $post ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_placeholder_img_src() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img_src' ); + return wc_placeholder_img_src(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_placeholder_img( $size = 'woocommerce_thumbnail' ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_placeholder_img' ); + return wc_placeholder_img( $size ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_formatted_variation( $variation = '', $flat = false ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_formatted_variation' ); + return wc_get_formatted_variation( $variation, $flat ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_scheduled_sales() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_scheduled_sales' ); + return wc_scheduled_sales(); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_get_attachment_image_attributes( $attr ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_get_attachment_image_attributes' ); + return wc_get_attachment_image_attributes( $attr ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_prepare_attachment_for_js( $response ) { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_prepare_attachment_for_js' ); + return wc_prepare_attachment_for_js( $response ); +} + +/** + * @deprecated 3.0 + */ +function woocommerce_track_product_view() { + wc_deprecated_function( __FUNCTION__, '3.0', 'wc_track_product_view' ); + return wc_track_product_view(); +} + +/** + * @deprecated 2.3 has no replacement + */ +function woocommerce_compile_less_styles() { + wc_deprecated_function( 'woocommerce_compile_less_styles', '2.3' ); +} + +/** + * woocommerce_calc_shipping was an option used to determine if shipping was enabled prior to version 2.6.0. This has since been replaced with wc_shipping_enabled() function and + * the woocommerce_ship_to_countries setting. + * @deprecated 2.6.0 + * @return string + */ +function woocommerce_calc_shipping_backwards_compatibility( $value ) { + if ( Constants::is_defined( 'WC_UPDATING' ) ) { + return $value; + } + return 'disabled' === get_option( 'woocommerce_ship_to_countries' ) ? 'no' : 'yes'; +} +add_filter( 'pre_option_woocommerce_calc_shipping', 'woocommerce_calc_shipping_backwards_compatibility' ); + +/** + * @deprecated 3.0.0 + * @see WC_Structured_Data class + * + * @return string + */ +function woocommerce_get_product_schema() { + wc_deprecated_function( 'woocommerce_get_product_schema', '3.0' ); + + global $product; + + $schema = "Product"; + + // Downloadable product schema handling + if ( $product->is_downloadable() ) { + switch ( $product->download_type ) { + case 'application' : + $schema = "SoftwareApplication"; + break; + case 'music' : + $schema = "MusicAlbum"; + break; + default : + $schema = "Product"; + break; + } + } + + return 'http://schema.org/' . $schema; +} + +/** + * Save product price. + * + * This is a private function (internal use ONLY) used until a data manipulation api is built. + * + * @deprecated 3.0.0 + * @param int $product_id + * @param float $regular_price + * @param float $sale_price + * @param string $date_from + * @param string $date_to + */ +function _wc_save_product_price( $product_id, $regular_price, $sale_price = '', $date_from = '', $date_to = '' ) { + wc_doing_it_wrong( '_wc_save_product_price()', 'This function is not for developer use and is deprecated.', '3.0' ); + + $product_id = absint( $product_id ); + $regular_price = wc_format_decimal( $regular_price ); + $sale_price = '' === $sale_price ? '' : wc_format_decimal( $sale_price ); + $date_from = wc_clean( $date_from ); + $date_to = wc_clean( $date_to ); + + update_post_meta( $product_id, '_regular_price', $regular_price ); + update_post_meta( $product_id, '_sale_price', $sale_price ); + + // Save Dates + update_post_meta( $product_id, '_sale_price_dates_from', $date_from ? strtotime( $date_from ) : '' ); + update_post_meta( $product_id, '_sale_price_dates_to', $date_to ? strtotime( $date_to ) : '' ); + + if ( $date_to && ! $date_from ) { + $date_from = strtotime( 'NOW', current_time( 'timestamp' ) ); + update_post_meta( $product_id, '_sale_price_dates_from', $date_from ); + } + + // Update price if on sale + if ( '' !== $sale_price && '' === $date_to && '' === $date_from ) { + update_post_meta( $product_id, '_price', $sale_price ); + } else { + update_post_meta( $product_id, '_price', $regular_price ); + } + + if ( '' !== $sale_price && $date_from && strtotime( $date_from ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { + update_post_meta( $product_id, '_price', $sale_price ); + } + + if ( $date_to && strtotime( $date_to ) < strtotime( 'NOW', current_time( 'timestamp' ) ) ) { + update_post_meta( $product_id, '_price', $regular_price ); + update_post_meta( $product_id, '_sale_price_dates_from', '' ); + update_post_meta( $product_id, '_sale_price_dates_to', '' ); + } +} + +/** + * Return customer avatar URL. + * + * @deprecated 3.1.0 + * @since 2.6.0 + * @param string $email the customer's email. + * @return string the URL to the customer's avatar. + */ +function wc_get_customer_avatar_url( $email ) { + // Deprecated in favor of WordPress get_avatar_url() function. + wc_deprecated_function( 'wc_get_customer_avatar_url()', '3.1', 'get_avatar_url()' ); + + return get_avatar_url( $email ); +} + +/** + * WooCommerce Core Supported Themes. + * + * @deprecated 3.3.0 + * @since 2.2 + * @return string[] + */ +function wc_get_core_supported_themes() { + wc_deprecated_function( 'wc_get_core_supported_themes()', '3.3' ); + return array( 'twentyseventeen', 'twentysixteen', 'twentyfifteen', 'twentyfourteen', 'twentythirteen', 'twentyeleven', 'twentytwelve', 'twentyten' ); +} + +/** + * Get min/max price meta query args. + * + * @deprecated 3.6.0 + * @since 3.0.0 + * @param array $args Min price and max price arguments. + * @return array + */ +function wc_get_min_max_price_meta_query( $args ) { + wc_deprecated_function( 'wc_get_min_max_price_meta_query()', '3.6' ); + + $current_min_price = isset( $args['min_price'] ) ? floatval( $args['min_price'] ) : 0; + $current_max_price = isset( $args['max_price'] ) ? floatval( $args['max_price'] ) : PHP_INT_MAX; + + return apply_filters( + 'woocommerce_get_min_max_price_meta_query', + array( + 'key' => '_price', + 'value' => array( $current_min_price, $current_max_price ), + 'compare' => 'BETWEEN', + 'type' => 'DECIMAL(10,' . wc_get_price_decimals() . ')', + ), + $args + ); +} + +/** + * When a term is split, ensure meta data maintained. + * + * @deprecated 3.6.0 + * @param int $old_term_id Old term ID. + * @param int $new_term_id New term ID. + * @param string $term_taxonomy_id Term taxonomy ID. + * @param string $taxonomy Taxonomy. + */ +function wc_taxonomy_metadata_update_content_for_split_terms( $old_term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) { + wc_deprecated_function( 'wc_taxonomy_metadata_update_content_for_split_terms', '3.6' ); +} + +/** + * WooCommerce Term Meta API. + * + * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. + * This function serves as a wrapper, using the new table if present, or falling back to the WC table. + * + * @deprecated 3.6.0 + * @param int $term_id Term ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @param string $prev_value Previous value. (default: ''). + * @return bool + */ +function update_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $prev_value = '' ) { + wc_deprecated_function( 'update_woocommerce_term_meta', '3.6', 'update_term_meta' ); + return function_exists( 'update_term_meta' ) ? update_term_meta( $term_id, $meta_key, $meta_value, $prev_value ) : update_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $prev_value ); +} + +/** + * WooCommerce Term Meta API. + * + * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. + * This function serves as a wrapper, using the new table if present, or falling back to the WC table. + * + * @deprecated 3.6.0 + * @param int $term_id Term ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @param bool $unique Make meta key unique. (default: false). + * @return bool + */ +function add_woocommerce_term_meta( $term_id, $meta_key, $meta_value, $unique = false ) { + wc_deprecated_function( 'add_woocommerce_term_meta', '3.6', 'add_term_meta' ); + return function_exists( 'add_term_meta' ) ? add_term_meta( $term_id, $meta_key, $meta_value, $unique ) : add_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value, $unique ); +} + +/** + * WooCommerce Term Meta API + * + * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. + * This function serves as a wrapper, using the new table if present, or falling back to the WC table. + * + * @deprecated 3.6.0 + * @param int $term_id Term ID. + * @param string $meta_key Meta key. + * @param string $meta_value Meta value (default: ''). + * @param bool $deprecated Deprecated param (default: false). + * @return bool + */ +function delete_woocommerce_term_meta( $term_id, $meta_key, $meta_value = '', $deprecated = false ) { + wc_deprecated_function( 'delete_woocommerce_term_meta', '3.6', 'delete_term_meta' ); + return function_exists( 'delete_term_meta' ) ? delete_term_meta( $term_id, $meta_key, $meta_value ) : delete_metadata( 'woocommerce_term', $term_id, $meta_key, $meta_value ); +} + +/** + * WooCommerce Term Meta API + * + * WC tables for storing term meta are deprecated from WordPress 4.4 since 4.4 has its own table. + * This function serves as a wrapper, using the new table if present, or falling back to the WC table. + * + * @deprecated 3.6.0 + * @param int $term_id Term ID. + * @param string $key Meta key. + * @param bool $single Whether to return a single value. (default: true). + * @return mixed + */ +function get_woocommerce_term_meta( $term_id, $key, $single = true ) { + wc_deprecated_function( 'get_woocommerce_term_meta', '3.6', 'get_term_meta' ); + return function_exists( 'get_term_meta' ) ? get_term_meta( $term_id, $key, $single ) : get_metadata( 'woocommerce_term', $term_id, $key, $single ); +} diff --git a/includes/wc-formatting-functions.php b/includes/wc-formatting-functions.php new file mode 100644 index 0000000..2bb63a6 --- /dev/null +++ b/includes/wc-formatting-functions.php @@ -0,0 +1,1516 @@ +strip_invalid_text_for_column( $wpdb->options, 'option_value', $value ); + + if ( is_wp_error( $value ) ) { + $value = ''; + } + + $value = esc_url_raw( trim( $value ) ); + $value = str_replace( 'http://', '', $value ); + return untrailingslashit( $value ); +} + +/** + * Gets the filename part of a download URL. + * + * @param string $file_url File URL. + * @return string + */ +function wc_get_filename_from_url( $file_url ) { + $parts = wp_parse_url( $file_url ); + if ( isset( $parts['path'] ) ) { + return basename( $parts['path'] ); + } +} + +/** + * Normalise dimensions, unify to cm then convert to wanted unit value. + * + * Usage: + * wc_get_dimension( 55, 'in' ); + * wc_get_dimension( 55, 'in', 'm' ); + * + * @param int|float $dimension Dimension. + * @param string $to_unit Unit to convert to. + * Options: 'in', 'm', 'cm', 'm'. + * @param string $from_unit Unit to convert from. + * Defaults to ''. + * Options: 'in', 'm', 'cm', 'm'. + * @return float + */ +function wc_get_dimension( $dimension, $to_unit, $from_unit = '' ) { + $to_unit = strtolower( $to_unit ); + + if ( empty( $from_unit ) ) { + $from_unit = strtolower( get_option( 'woocommerce_dimension_unit' ) ); + } + + // Unify all units to cm first. + if ( $from_unit !== $to_unit ) { + switch ( $from_unit ) { + case 'in': + $dimension *= 2.54; + break; + case 'm': + $dimension *= 100; + break; + case 'mm': + $dimension *= 0.1; + break; + case 'yd': + $dimension *= 91.44; + break; + } + + // Output desired unit. + switch ( $to_unit ) { + case 'in': + $dimension *= 0.3937; + break; + case 'm': + $dimension *= 0.01; + break; + case 'mm': + $dimension *= 10; + break; + case 'yd': + $dimension *= 0.010936133; + break; + } + } + + return ( $dimension < 0 ) ? 0 : $dimension; +} + +/** + * Normalise weights, unify to kg then convert to wanted unit value. + * + * Usage: + * wc_get_weight(55, 'kg'); + * wc_get_weight(55, 'kg', 'lbs'); + * + * @param int|float $weight Weight. + * @param string $to_unit Unit to convert to. + * Options: 'g', 'kg', 'lbs', 'oz'. + * @param string $from_unit Unit to convert from. + * Defaults to ''. + * Options: 'g', 'kg', 'lbs', 'oz'. + * @return float + */ +function wc_get_weight( $weight, $to_unit, $from_unit = '' ) { + $weight = (float) $weight; + $to_unit = strtolower( $to_unit ); + + if ( empty( $from_unit ) ) { + $from_unit = strtolower( get_option( 'woocommerce_weight_unit' ) ); + } + + // Unify all units to kg first. + if ( $from_unit !== $to_unit ) { + switch ( $from_unit ) { + case 'g': + $weight *= 0.001; + break; + case 'lbs': + $weight *= 0.453592; + break; + case 'oz': + $weight *= 0.0283495; + break; + } + + // Output desired unit. + switch ( $to_unit ) { + case 'g': + $weight *= 1000; + break; + case 'lbs': + $weight *= 2.20462; + break; + case 'oz': + $weight *= 35.274; + break; + } + } + + return ( $weight < 0 ) ? 0 : $weight; +} + +/** + * Trim trailing zeros off prices. + * + * @param string|float|int $price Price. + * @return string + */ +function wc_trim_zeros( $price ) { + return preg_replace( '/' . preg_quote( wc_get_price_decimal_separator(), '/' ) . '0++$/', '', $price ); +} + +/** + * Round a tax amount. + * + * @param double $value Amount to round. + * @param int $precision DP to round. Defaults to wc_get_price_decimals. + * @return float + */ +function wc_round_tax_total( $value, $precision = null ) { + $precision = is_null( $precision ) ? wc_get_price_decimals() : intval( $precision ); + + if ( version_compare( PHP_VERSION, '5.3.0', '>=' ) ) { + $rounded_tax = NumberUtil::round( $value, $precision, wc_get_tax_rounding_mode() ); // phpcs:ignore PHPCompatibility.FunctionUse.NewFunctionParameters.round_modeFound + } elseif ( 2 === wc_get_tax_rounding_mode() ) { + $rounded_tax = wc_legacy_round_half_down( $value, $precision ); + } else { + $rounded_tax = NumberUtil::round( $value, $precision ); + } + + return apply_filters( 'wc_round_tax_total', $rounded_tax, $value, $precision, WC_TAX_ROUNDING_MODE ); +} + +/** + * Round half down in PHP 5.2. + * + * @since 3.2.6 + * @param float $value Value to round. + * @param int $precision Precision to round down to. + * @return float + */ +function wc_legacy_round_half_down( $value, $precision ) { + $value = wc_float_to_string( $value ); + + if ( false !== strstr( $value, '.' ) ) { + $value = explode( '.', $value ); + + if ( strlen( $value[1] ) > $precision && substr( $value[1], -1 ) === '5' ) { + $value[1] = substr( $value[1], 0, -1 ) . '4'; + } + + $value = implode( '.', $value ); + } + + return NumberUtil::round( floatval( $value ), $precision ); +} + +/** + * Make a refund total negative. + * + * @param float $amount Refunded amount. + * + * @return float + */ +function wc_format_refund_total( $amount ) { + return $amount * -1; +} + +/** + * Format decimal numbers ready for DB storage. + * + * Sanitize, optionally remove decimals, and optionally round + trim off zeros. + * + * This function does not remove thousands - this should be done before passing a value to the function. + * + * @param float|string $number Expects either a float or a string with a decimal separator only (no thousands). + * @param mixed $dp number Number of decimal points to use, blank to use woocommerce_price_num_decimals, or false to avoid all rounding. + * @param bool $trim_zeros From end of string. + * @return string + */ +function wc_format_decimal( $number, $dp = false, $trim_zeros = false ) { + $locale = localeconv(); + $decimals = array( wc_get_price_decimal_separator(), $locale['decimal_point'], $locale['mon_decimal_point'] ); + + // Remove locale from string. + if ( ! is_float( $number ) ) { + $number = str_replace( $decimals, '.', $number ); + + // Convert multiple dots to just one. + $number = preg_replace( '/\.(?![^.]+$)|[^0-9.-]/', '', wc_clean( $number ) ); + } + + if ( false !== $dp ) { + $dp = intval( '' === $dp ? wc_get_price_decimals() : $dp ); + $number = number_format( floatval( $number ), $dp, '.', '' ); + } elseif ( is_float( $number ) ) { + // DP is false - don't use number format, just return a string using whatever is given. Remove scientific notation using sprintf. + $number = str_replace( $decimals, '.', sprintf( '%.' . wc_get_rounding_precision() . 'f', $number ) ); + // We already had a float, so trailing zeros are not needed. + $trim_zeros = true; + } + + if ( $trim_zeros && strstr( $number, '.' ) ) { + $number = rtrim( rtrim( $number, '0' ), '.' ); + } + + return $number; +} + +/** + * Convert a float to a string without locale formatting which PHP adds when changing floats to strings. + * + * @param float $float Float value to format. + * @return string + */ +function wc_float_to_string( $float ) { + if ( ! is_float( $float ) ) { + return $float; + } + + $locale = localeconv(); + $string = strval( $float ); + $string = str_replace( $locale['decimal_point'], '.', $string ); + + return $string; +} + +/** + * Format a price with WC Currency Locale settings. + * + * @param string $value Price to localize. + * @return string + */ +function wc_format_localized_price( $value ) { + return apply_filters( 'woocommerce_format_localized_price', str_replace( '.', wc_get_price_decimal_separator(), strval( $value ) ), $value ); +} + +/** + * Format a decimal with PHP Locale settings. + * + * @param string $value Decimal to localize. + * @return string + */ +function wc_format_localized_decimal( $value ) { + $locale = localeconv(); + return apply_filters( 'woocommerce_format_localized_decimal', str_replace( '.', $locale['decimal_point'], strval( $value ) ), $value ); +} + +/** + * Format a coupon code. + * + * @since 3.0.0 + * @param string $value Coupon code to format. + * @return string + */ +function wc_format_coupon_code( $value ) { + return apply_filters( 'woocommerce_coupon_code', $value ); +} + +/** + * Sanitize a coupon code. + * + * Uses sanitize_post_field since coupon codes are stored as + * post_titles - the sanitization and escaping must match. + * + * @since 3.6.0 + * @param string $value Coupon code to format. + * @return string + */ +function wc_sanitize_coupon_code( $value ) { + return wp_filter_kses( sanitize_post_field( 'post_title', $value, 0, 'db' ) ); +} + +/** + * Clean variables using sanitize_text_field. Arrays are cleaned recursively. + * Non-scalar values are ignored. + * + * @param string|array $var Data to sanitize. + * @return string|array + */ +function wc_clean( $var ) { + if ( is_array( $var ) ) { + return array_map( 'wc_clean', $var ); + } else { + return is_scalar( $var ) ? sanitize_text_field( $var ) : $var; + } +} + +/** + * Function wp_check_invalid_utf8 with recursive array support. + * + * @param string|array $var Data to sanitize. + * @return string|array + */ +function wc_check_invalid_utf8( $var ) { + if ( is_array( $var ) ) { + return array_map( 'wc_check_invalid_utf8', $var ); + } else { + return wp_check_invalid_utf8( $var ); + } +} + +/** + * Run wc_clean over posted textarea but maintain line breaks. + * + * @since 3.0.0 + * @param string $var Data to sanitize. + * @return string + */ +function wc_sanitize_textarea( $var ) { + return implode( "\n", array_map( 'wc_clean', explode( "\n", $var ) ) ); +} + +/** + * Sanitize a string destined to be a tooltip. + * + * @since 2.3.10 Tooltips are encoded with htmlspecialchars to prevent XSS. Should not be used in conjunction with esc_attr() + * @param string $var Data to sanitize. + * @return string + */ +function wc_sanitize_tooltip( $var ) { + return htmlspecialchars( + wp_kses( + html_entity_decode( $var ), + array( + 'br' => array(), + 'em' => array(), + 'strong' => array(), + 'small' => array(), + 'span' => array(), + 'ul' => array(), + 'li' => array(), + 'ol' => array(), + 'p' => array(), + ) + ) + ); +} + +/** + * Merge two arrays. + * + * @param array $a1 First array to merge. + * @param array $a2 Second array to merge. + * @return array + */ +function wc_array_overlay( $a1, $a2 ) { + foreach ( $a1 as $k => $v ) { + if ( ! array_key_exists( $k, $a2 ) ) { + continue; + } + if ( is_array( $v ) && is_array( $a2[ $k ] ) ) { + $a1[ $k ] = wc_array_overlay( $v, $a2[ $k ] ); + } else { + $a1[ $k ] = $a2[ $k ]; + } + } + return $a1; +} + +/** + * Formats a stock amount by running it through a filter. + * + * @param int|float $amount Stock amount. + * @return int|float + */ +function wc_stock_amount( $amount ) { + return apply_filters( 'woocommerce_stock_amount', $amount ); +} + +/** + * Get the price format depending on the currency position. + * + * @return string + */ +function get_woocommerce_price_format() { + $currency_pos = get_option( 'woocommerce_currency_pos' ); + $format = '%1$s%2$s'; + + switch ( $currency_pos ) { + case 'left': + $format = '%1$s%2$s'; + break; + case 'right': + $format = '%2$s%1$s'; + break; + case 'left_space': + $format = '%1$s %2$s'; + break; + case 'right_space': + $format = '%2$s %1$s'; + break; + } + + return apply_filters( 'woocommerce_price_format', $format, $currency_pos ); +} + +/** + * Return the thousand separator for prices. + * + * @since 2.3 + * @return string + */ +function wc_get_price_thousand_separator() { + return stripslashes( apply_filters( 'wc_get_price_thousand_separator', get_option( 'woocommerce_price_thousand_sep' ) ) ); +} + +/** + * Return the decimal separator for prices. + * + * @since 2.3 + * @return string + */ +function wc_get_price_decimal_separator() { + $separator = apply_filters( 'wc_get_price_decimal_separator', get_option( 'woocommerce_price_decimal_sep' ) ); + return $separator ? stripslashes( $separator ) : '.'; +} + +/** + * Return the number of decimals after the decimal point. + * + * @since 2.3 + * @return int + */ +function wc_get_price_decimals() { + return absint( apply_filters( 'wc_get_price_decimals', get_option( 'woocommerce_price_num_decimals', 2 ) ) ); +} + +/** + * Format the price with a currency symbol. + * + * @param float $price Raw price. + * @param array $args Arguments to format a price { + * Array of arguments. + * Defaults to empty array. + * + * @type bool $ex_tax_label Adds exclude tax label. + * Defaults to false. + * @type string $currency Currency code. + * Defaults to empty string (Use the result from get_woocommerce_currency()). + * @type string $decimal_separator Decimal separator. + * Defaults the result of wc_get_price_decimal_separator(). + * @type string $thousand_separator Thousand separator. + * Defaults the result of wc_get_price_thousand_separator(). + * @type string $decimals Number of decimals. + * Defaults the result of wc_get_price_decimals(). + * @type string $price_format Price format depending on the currency position. + * Defaults the result of get_woocommerce_price_format(). + * } + * @return string + */ +function wc_price( $price, $args = array() ) { + $args = apply_filters( + 'wc_price_args', + wp_parse_args( + $args, + array( + 'ex_tax_label' => false, + 'currency' => '', + 'decimal_separator' => wc_get_price_decimal_separator(), + 'thousand_separator' => wc_get_price_thousand_separator(), + 'decimals' => wc_get_price_decimals(), + 'price_format' => get_woocommerce_price_format(), + ) + ) + ); + + $original_price = $price; + + // Convert to float to avoid issues on PHP 8. + $price = (float) $price; + + $unformatted_price = $price; + $negative = $price < 0; + + /** + * Filter raw price. + * + * @param float $raw_price Raw price. + * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. + */ + $price = apply_filters( 'raw_woocommerce_price', $negative ? $price * -1 : $price, $original_price ); + + /** + * Filter formatted price. + * + * @param float $formatted_price Formatted price. + * @param float $price Unformatted price. + * @param int $decimals Number of decimals. + * @param string $decimal_separator Decimal separator. + * @param string $thousand_separator Thousand separator. + * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. + */ + $price = apply_filters( 'formatted_woocommerce_price', number_format( $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'] ), $price, $args['decimals'], $args['decimal_separator'], $args['thousand_separator'], $original_price ); + + if ( apply_filters( 'woocommerce_price_trim_zeros', false ) && $args['decimals'] > 0 ) { + $price = wc_trim_zeros( $price ); + } + + $formatted_price = ( $negative ? '-' : '' ) . sprintf( $args['price_format'], '' . get_woocommerce_currency_symbol( $args['currency'] ) . '', $price ); + $return = '' . $formatted_price . ''; + + if ( $args['ex_tax_label'] && wc_tax_enabled() ) { + $return .= ' ' . WC()->countries->ex_tax_or_vat() . ''; + } + + /** + * Filters the string of price markup. + * + * @param string $return Price HTML markup. + * @param string $price Formatted price. + * @param array $args Pass on the args. + * @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0. + * @param float|string $original_price Original price as float, or empty string. Since 5.0.0. + */ + return apply_filters( 'wc_price', $return, $price, $args, $unformatted_price, $original_price ); +} + +/** + * Notation to numbers. + * + * This function transforms the php.ini notation for numbers (like '2M') to an integer. + * + * @param string $size Size value. + * @return int + */ +function wc_let_to_num( $size ) { + $l = substr( $size, -1 ); + $ret = (int) substr( $size, 0, -1 ); + switch ( strtoupper( $l ) ) { + case 'P': + $ret *= 1024; + // No break. + case 'T': + $ret *= 1024; + // No break. + case 'G': + $ret *= 1024; + // No break. + case 'M': + $ret *= 1024; + // No break. + case 'K': + $ret *= 1024; + // No break. + } + return $ret; +} + +/** + * WooCommerce Date Format - Allows to change date format for everything WooCommerce. + * + * @return string + */ +function wc_date_format() { + $date_format = get_option( 'date_format' ); + if ( empty( $date_format ) ) { + // Return default date format if the option is empty. + $date_format = 'F j, Y'; + } + return apply_filters( 'woocommerce_date_format', $date_format ); +} + +/** + * WooCommerce Time Format - Allows to change time format for everything WooCommerce. + * + * @return string + */ +function wc_time_format() { + $time_format = get_option( 'time_format' ); + if ( empty( $time_format ) ) { + // Return default time format if the option is empty. + $time_format = 'g:i a'; + } + return apply_filters( 'woocommerce_time_format', $time_format ); +} + +/** + * Convert mysql datetime to PHP timestamp, forcing UTC. Wrapper for strtotime. + * + * Based on wcs_strtotime_dark_knight() from WC Subscriptions by Prospress. + * + * @since 3.0.0 + * @param string $time_string Time string. + * @param int|null $from_timestamp Timestamp to convert from. + * @return int + */ +function wc_string_to_timestamp( $time_string, $from_timestamp = null ) { + $original_timezone = date_default_timezone_get(); + + // @codingStandardsIgnoreStart + date_default_timezone_set( 'UTC' ); + + if ( null === $from_timestamp ) { + $next_timestamp = strtotime( $time_string ); + } else { + $next_timestamp = strtotime( $time_string, $from_timestamp ); + } + + date_default_timezone_set( $original_timezone ); + // @codingStandardsIgnoreEnd + + return $next_timestamp; +} + +/** + * Convert a date string to a WC_DateTime. + * + * @since 3.1.0 + * @param string $time_string Time string. + * @return WC_DateTime + */ +function wc_string_to_datetime( $time_string ) { + // Strings are defined in local WP timezone. Convert to UTC. + if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $time_string, $date_bits ) ) { + $offset = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset(); + $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset; + } else { + $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $time_string ) ) ) ); + } + $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); + + // Set local timezone or offset. + if ( get_option( 'timezone_string' ) ) { + $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); + } else { + $datetime->set_utc_offset( wc_timezone_offset() ); + } + + return $datetime; +} + +/** + * WooCommerce Timezone - helper to retrieve the timezone string for a site until. + * a WP core method exists (see https://core.trac.wordpress.org/ticket/24730). + * + * Adapted from https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. + * + * @since 2.1 + * @return string PHP timezone string for the site + */ +function wc_timezone_string() { + // Added in WordPress 5.3 Ref https://developer.wordpress.org/reference/functions/wp_timezone_string/. + if ( function_exists( 'wp_timezone_string' ) ) { + return wp_timezone_string(); + } + + // If site timezone string exists, return it. + $timezone = get_option( 'timezone_string' ); + if ( $timezone ) { + return $timezone; + } + + // Get UTC offset, if it isn't set then return UTC. + $utc_offset = floatval( get_option( 'gmt_offset', 0 ) ); + if ( ! is_numeric( $utc_offset ) || 0.0 === $utc_offset ) { + return 'UTC'; + } + + // Adjust UTC offset from hours to seconds. + $utc_offset = (int) ( $utc_offset * 3600 ); + + // Attempt to guess the timezone string from the UTC offset. + $timezone = timezone_name_from_abbr( '', $utc_offset ); + if ( $timezone ) { + return $timezone; + } + + // Last try, guess timezone string manually. + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + // WordPress restrict the use of date(), since it's affected by timezone settings, but in this case is just what we need to guess the correct timezone. + if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date + return $city['timezone_id']; + } + } + } + + // Fallback to UTC. + return 'UTC'; +} + +/** + * Get timezone offset in seconds. + * + * @since 3.0.0 + * @return float + */ +function wc_timezone_offset() { + $timezone = get_option( 'timezone_string' ); + + if ( $timezone ) { + $timezone_object = new DateTimeZone( $timezone ); + return $timezone_object->getOffset( new DateTime( 'now' ) ); + } else { + return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; + } +} + +/** + * Callback which can flatten post meta (gets the first value if it's an array). + * + * @since 3.0.0 + * @param array $value Value to flatten. + * @return mixed + */ +function wc_flatten_meta_callback( $value ) { + return is_array( $value ) ? current( $value ) : $value; +} + +if ( ! function_exists( 'wc_rgb_from_hex' ) ) { + + /** + * Convert RGB to HEX. + * + * @param mixed $color Color. + * + * @return array + */ + function wc_rgb_from_hex( $color ) { + $color = str_replace( '#', '', $color ); + // Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF". + $color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color ); + + $rgb = array(); + $rgb['R'] = hexdec( $color[0] . $color[1] ); + $rgb['G'] = hexdec( $color[2] . $color[3] ); + $rgb['B'] = hexdec( $color[4] . $color[5] ); + + return $rgb; + } +} + +if ( ! function_exists( 'wc_hex_darker' ) ) { + + /** + * Make HEX color darker. + * + * @param mixed $color Color. + * @param int $factor Darker factor. + * Defaults to 30. + * @return string + */ + function wc_hex_darker( $color, $factor = 30 ) { + $base = wc_rgb_from_hex( $color ); + $color = '#'; + + foreach ( $base as $k => $v ) { + $amount = $v / 100; + $amount = NumberUtil::round( $amount * $factor ); + $new_decimal = $v - $amount; + + $new_hex_component = dechex( $new_decimal ); + if ( strlen( $new_hex_component ) < 2 ) { + $new_hex_component = '0' . $new_hex_component; + } + $color .= $new_hex_component; + } + + return $color; + } +} + +if ( ! function_exists( 'wc_hex_lighter' ) ) { + + /** + * Make HEX color lighter. + * + * @param mixed $color Color. + * @param int $factor Lighter factor. + * Defaults to 30. + * @return string + */ + function wc_hex_lighter( $color, $factor = 30 ) { + $base = wc_rgb_from_hex( $color ); + $color = '#'; + + foreach ( $base as $k => $v ) { + $amount = 255 - $v; + $amount = $amount / 100; + $amount = NumberUtil::round( $amount * $factor ); + $new_decimal = $v + $amount; + + $new_hex_component = dechex( $new_decimal ); + if ( strlen( $new_hex_component ) < 2 ) { + $new_hex_component = '0' . $new_hex_component; + } + $color .= $new_hex_component; + } + + return $color; + } +} + +if ( ! function_exists( 'wc_hex_is_light' ) ) { + + /** + * Determine whether a hex color is light. + * + * @param mixed $color Color. + * @return bool True if a light color. + */ + function wc_hex_is_light( $color ) { + $hex = str_replace( '#', '', $color ); + + $c_r = hexdec( substr( $hex, 0, 2 ) ); + $c_g = hexdec( substr( $hex, 2, 2 ) ); + $c_b = hexdec( substr( $hex, 4, 2 ) ); + + $brightness = ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000; + + return $brightness > 155; + } +} + +if ( ! function_exists( 'wc_light_or_dark' ) ) { + + /** + * Detect if we should use a light or dark color on a background color. + * + * @param mixed $color Color. + * @param string $dark Darkest reference. + * Defaults to '#000000'. + * @param string $light Lightest reference. + * Defaults to '#FFFFFF'. + * @return string + */ + function wc_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) { + return wc_hex_is_light( $color ) ? $dark : $light; + } +} + +if ( ! function_exists( 'wc_format_hex' ) ) { + + /** + * Format string as hex. + * + * @param string $hex HEX color. + * @return string|null + */ + function wc_format_hex( $hex ) { + $hex = trim( str_replace( '#', '', $hex ) ); + + if ( strlen( $hex ) === 3 ) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + + return $hex ? '#' . $hex : null; + } +} + +/** + * Format the postcode according to the country and length of the postcode. + * + * @param string $postcode Unformatted postcode. + * @param string $country Base country. + * @return string + */ +function wc_format_postcode( $postcode, $country ) { + $postcode = wc_normalize_postcode( $postcode ); + + switch ( $country ) { + case 'CA': + case 'GB': + $postcode = substr_replace( $postcode, ' ', -3, 0 ); + break; + case 'IE': + $postcode = substr_replace( $postcode, ' ', 3, 0 ); + break; + case 'BR': + case 'PL': + $postcode = substr_replace( $postcode, '-', -3, 0 ); + break; + case 'JP': + $postcode = substr_replace( $postcode, '-', 3, 0 ); + break; + case 'PT': + $postcode = substr_replace( $postcode, '-', 4, 0 ); + break; + case 'PR': + case 'US': + $postcode = rtrim( substr_replace( $postcode, '-', 5, 0 ), '-' ); + break; + case 'NL': + $postcode = substr_replace( $postcode, ' ', 4, 0 ); + break; + } + + return apply_filters( 'woocommerce_format_postcode', trim( $postcode ), $country ); +} + +/** + * Normalize postcodes. + * + * Remove spaces and convert characters to uppercase. + * + * @since 2.6.0 + * @param string $postcode Postcode. + * @return string + */ +function wc_normalize_postcode( $postcode ) { + return preg_replace( '/[\s\-]/', '', trim( wc_strtoupper( $postcode ) ) ); +} + +/** + * Format phone numbers. + * + * @param string $phone Phone number. + * @return string + */ +function wc_format_phone_number( $phone ) { + if ( ! WC_Validation::is_phone( $phone ) ) { + return ''; + } + return preg_replace( '/[^0-9\+\-\(\)\s]/', '-', preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $phone ) ); +} + +/** + * Sanitize phone number. + * Allows only numbers and "+" (plus sign). + * + * @since 3.6.0 + * @param string $phone Phone number. + * @return string + */ +function wc_sanitize_phone_number( $phone ) { + return preg_replace( '/[^\d+]/', '', $phone ); +} + +/** + * Wrapper for mb_strtoupper which see's if supported first. + * + * @since 3.1.0 + * @param string $string String to format. + * @return string + */ +function wc_strtoupper( $string ) { + return function_exists( 'mb_strtoupper' ) ? mb_strtoupper( $string ) : strtoupper( $string ); +} + +/** + * Make a string lowercase. + * Try to use mb_strtolower() when available. + * + * @since 2.3 + * @param string $string String to format. + * @return string + */ +function wc_strtolower( $string ) { + return function_exists( 'mb_strtolower' ) ? mb_strtolower( $string ) : strtolower( $string ); +} + +/** + * Trim a string and append a suffix. + * + * @param string $string String to trim. + * @param integer $chars Amount of characters. + * Defaults to 200. + * @param string $suffix Suffix. + * Defaults to '...'. + * @return string + */ +function wc_trim_string( $string, $chars = 200, $suffix = '...' ) { + if ( strlen( $string ) > $chars ) { + if ( function_exists( 'mb_substr' ) ) { + $string = mb_substr( $string, 0, ( $chars - mb_strlen( $suffix ) ) ) . $suffix; + } else { + $string = substr( $string, 0, ( $chars - strlen( $suffix ) ) ) . $suffix; + } + } + return $string; +} + +/** + * Format content to display shortcodes. + * + * @since 2.3.0 + * @param string $raw_string Raw string. + * @return string + */ +function wc_format_content( $raw_string ) { + return apply_filters( 'woocommerce_format_content', apply_filters( 'woocommerce_short_description', $raw_string ), $raw_string ); +} + +/** + * Format product short description. + * Adds support for Jetpack Markdown. + * + * @codeCoverageIgnore + * @since 2.4.0 + * @param string $content Product short description. + * @return string + */ +function wc_format_product_short_description( $content ) { + // Add support for Jetpack Markdown. + if ( class_exists( 'WPCom_Markdown' ) ) { + $markdown = WPCom_Markdown::get_instance(); + + return wpautop( + $markdown->transform( + $content, + array( + 'unslash' => false, + ) + ) + ); + } + + return $content; +} + +/** + * Formats curency symbols when saved in settings. + * + * @codeCoverageIgnore + * @param string $value Option value. + * @param array $option Option name. + * @param string $raw_value Raw value. + * @return string + */ +function wc_format_option_price_separators( $value, $option, $raw_value ) { + return wp_kses_post( $raw_value ); +} +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_decimal_sep', 'wc_format_option_price_separators', 10, 3 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_thousand_sep', 'wc_format_option_price_separators', 10, 3 ); + +/** + * Formats decimals when saved in settings. + * + * @codeCoverageIgnore + * @param string $value Option value. + * @param array $option Option name. + * @param string $raw_value Raw value. + * @return string + */ +function wc_format_option_price_num_decimals( $value, $option, $raw_value ) { + return is_null( $raw_value ) ? 2 : absint( $raw_value ); +} +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_price_num_decimals', 'wc_format_option_price_num_decimals', 10, 3 ); + +/** + * Formats hold stock option and sets cron event up. + * + * @codeCoverageIgnore + * @param string $value Option value. + * @param array $option Option name. + * @param string $raw_value Raw value. + * @return string + */ +function wc_format_option_hold_stock_minutes( $value, $option, $raw_value ) { + $value = ! empty( $raw_value ) ? absint( $raw_value ) : ''; // Allow > 0 or set to ''. + + wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); + + if ( '' !== $value ) { + $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $value ) ); + wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); + } + + return $value; +} +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_hold_stock_minutes', 'wc_format_option_hold_stock_minutes', 10, 3 ); + +/** + * Sanitize terms from an attribute text based. + * + * @since 2.4.5 + * @param string $term Term value. + * @return string + */ +function wc_sanitize_term_text_based( $term ) { + return trim( wp_strip_all_tags( wp_unslash( $term ) ) ); +} + +if ( ! function_exists( 'wc_make_numeric_postcode' ) ) { + /** + * Make numeric postcode. + * + * Converts letters to numbers so we can do a simple range check on postcodes. + * E.g. PE30 becomes 16050300 (P = 16, E = 05, 3 = 03, 0 = 00) + * + * @since 2.6.0 + * @param string $postcode Regular postcode. + * @return string + */ + function wc_make_numeric_postcode( $postcode ) { + $postcode = str_replace( array( ' ', '-' ), '', $postcode ); + $postcode_length = strlen( $postcode ); + $letters_to_numbers = array_merge( array( 0 ), range( 'A', 'Z' ) ); + $letters_to_numbers = array_flip( $letters_to_numbers ); + $numeric_postcode = ''; + + for ( $i = 0; $i < $postcode_length; $i ++ ) { + if ( is_numeric( $postcode[ $i ] ) ) { + $numeric_postcode .= str_pad( $postcode[ $i ], 2, '0', STR_PAD_LEFT ); + } elseif ( isset( $letters_to_numbers[ $postcode[ $i ] ] ) ) { + $numeric_postcode .= str_pad( $letters_to_numbers[ $postcode[ $i ] ], 2, '0', STR_PAD_LEFT ); + } else { + $numeric_postcode .= '00'; + } + } + + return $numeric_postcode; + } +} + +/** + * Format the stock amount ready for display based on settings. + * + * @since 3.0.0 + * @param WC_Product $product Product object for which the stock you need to format. + * @return string + */ +function wc_format_stock_for_display( $product ) { + $display = __( 'In stock', 'woocommerce' ); + $stock_amount = $product->get_stock_quantity(); + + switch ( get_option( 'woocommerce_stock_format' ) ) { + case 'low_amount': + if ( $stock_amount <= wc_get_low_stock_amount( $product ) ) { + /* translators: %s: stock amount */ + $display = sprintf( __( 'Only %s left in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) ); + } + break; + case '': + /* translators: %s: stock amount */ + $display = sprintf( __( '%s in stock', 'woocommerce' ), wc_format_stock_quantity_for_display( $stock_amount, $product ) ); + break; + } + + if ( $product->backorders_allowed() && $product->backorders_require_notification() ) { + $display .= ' ' . __( '(can be backordered)', 'woocommerce' ); + } + + return $display; +} + +/** + * Format the stock quantity ready for display. + * + * @since 3.0.0 + * @param int $stock_quantity Stock quantity. + * @param WC_Product $product Product instance so that we can pass through the filters. + * @return string + */ +function wc_format_stock_quantity_for_display( $stock_quantity, $product ) { + return apply_filters( 'woocommerce_format_stock_quantity', $stock_quantity, $product ); +} + +/** + * Format a sale price for display. + * + * @since 3.0.0 + * @param string $regular_price Regular price. + * @param string $sale_price Sale price. + * @return string + */ +function wc_format_sale_price( $regular_price, $sale_price ) { + $price = ' ' . ( is_numeric( $sale_price ) ? wc_price( $sale_price ) : $sale_price ) . ''; + return apply_filters( 'woocommerce_format_sale_price', $price, $regular_price, $sale_price ); +} + +/** + * Format a price range for display. + * + * @param string $from Price from. + * @param string $to Price to. + * @return string + */ +function wc_format_price_range( $from, $to ) { + /* translators: 1: price from 2: price to */ + $price = sprintf( _x( '%1$s – %2$s', 'Price range: from-to', 'woocommerce' ), is_numeric( $from ) ? wc_price( $from ) : $from, is_numeric( $to ) ? wc_price( $to ) : $to ); + return apply_filters( 'woocommerce_format_price_range', $price, $from, $to ); +} + +/** + * Format a weight for display. + * + * @since 3.0.0 + * @param float $weight Weight. + * @return string + */ +function wc_format_weight( $weight ) { + $weight_string = wc_format_localized_decimal( $weight ); + + if ( ! empty( $weight_string ) ) { + $weight_string .= ' ' . get_option( 'woocommerce_weight_unit' ); + } else { + $weight_string = __( 'N/A', 'woocommerce' ); + } + + return apply_filters( 'woocommerce_format_weight', $weight_string, $weight ); +} + +/** + * Format dimensions for display. + * + * @since 3.0.0 + * @param array $dimensions Array of dimensions. + * @return string + */ +function wc_format_dimensions( $dimensions ) { + $dimension_string = implode( ' × ', array_filter( array_map( 'wc_format_localized_decimal', $dimensions ) ) ); + + if ( ! empty( $dimension_string ) ) { + $dimension_string .= ' ' . get_option( 'woocommerce_dimension_unit' ); + } else { + $dimension_string = __( 'N/A', 'woocommerce' ); + } + + return apply_filters( 'woocommerce_format_dimensions', $dimension_string, $dimensions ); +} + +/** + * Format a date for output. + * + * @since 3.0.0 + * @param WC_DateTime $date Instance of WC_DateTime. + * @param string $format Data format. + * Defaults to the wc_date_format function if not set. + * @return string + */ +function wc_format_datetime( $date, $format = '' ) { + if ( ! $format ) { + $format = wc_date_format(); + } + if ( ! is_a( $date, 'WC_DateTime' ) ) { + return ''; + } + return $date->date_i18n( $format ); +} + +/** + * Process oEmbeds. + * + * @since 3.1.0 + * @param string $content Content. + * @return string + */ +function wc_do_oembeds( $content ) { + global $wp_embed; + + $content = $wp_embed->autoembed( $content ); + + return $content; +} + +/** + * Get part of a string before :. + * + * Used for example in shipping methods ids where they take the format + * method_id:instance_id + * + * @since 3.2.0 + * @param string $string String to extract. + * @return string + */ +function wc_get_string_before_colon( $string ) { + return trim( current( explode( ':', (string) $string ) ) ); +} + +/** + * Array merge and sum function. + * + * Source: https://gist.github.com/Nickology/f700e319cbafab5eaedc + * + * @since 3.2.0 + * @return array + */ +function wc_array_merge_recursive_numeric() { + $arrays = func_get_args(); + + // If there's only one array, it's already merged. + if ( 1 === count( $arrays ) ) { + return $arrays[0]; + } + + // Remove any items in $arrays that are NOT arrays. + foreach ( $arrays as $key => $array ) { + if ( ! is_array( $array ) ) { + unset( $arrays[ $key ] ); + } + } + + // We start by setting the first array as our final array. + // We will merge all other arrays with this one. + $final = array_shift( $arrays ); + + foreach ( $arrays as $b ) { + foreach ( $final as $key => $value ) { + // If $key does not exist in $b, then it is unique and can be safely merged. + if ( ! isset( $b[ $key ] ) ) { + $final[ $key ] = $value; + } else { + // If $key is present in $b, then we need to merge and sum numeric values in both. + if ( is_numeric( $value ) && is_numeric( $b[ $key ] ) ) { + // If both values for these keys are numeric, we sum them. + $final[ $key ] = $value + $b[ $key ]; + } elseif ( is_array( $value ) && is_array( $b[ $key ] ) ) { + // If both values are arrays, we recursively call ourself. + $final[ $key ] = wc_array_merge_recursive_numeric( $value, $b[ $key ] ); + } else { + // If both keys exist but differ in type, then we cannot merge them. + // In this scenario, we will $b's value for $key is used. + $final[ $key ] = $b[ $key ]; + } + } + } + + // Finally, we need to merge any keys that exist only in $b. + foreach ( $b as $key => $value ) { + if ( ! isset( $final[ $key ] ) ) { + $final[ $key ] = $value; + } + } + } + + return $final; +} + +/** + * Implode and escape HTML attributes for output. + * + * @since 3.3.0 + * @param array $raw_attributes Attribute name value pairs. + * @return string + */ +function wc_implode_html_attributes( $raw_attributes ) { + $attributes = array(); + foreach ( $raw_attributes as $name => $value ) { + $attributes[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; + } + return implode( ' ', $attributes ); +} + +/** + * Escape JSON for use on HTML or attribute text nodes. + * + * @since 3.5.5 + * @param string $json JSON to escape. + * @param bool $html True if escaping for HTML text node, false for attributes. Determines how quotes are handled. + * @return string Escaped JSON. + */ +function wc_esc_json( $json, $html = false ) { + return _wp_specialchars( + $json, + $html ? ENT_NOQUOTES : ENT_QUOTES, // Escape quotes in attribute nodes only. + 'UTF-8', // json_encode() outputs UTF-8 (really just ASCII), not the blog's charset. + true // Double escape entities: `&` -> `&amp;`. + ); +} + +/** + * Parse a relative date option from the settings API into a standard format. + * + * @since 3.4.0 + * @param mixed $raw_value Value stored in DB. + * @return array Nicely formatted array with number and unit values. + */ +function wc_parse_relative_date_option( $raw_value ) { + $periods = array( + 'days' => __( 'Day(s)', 'woocommerce' ), + 'weeks' => __( 'Week(s)', 'woocommerce' ), + 'months' => __( 'Month(s)', 'woocommerce' ), + 'years' => __( 'Year(s)', 'woocommerce' ), + ); + + $value = wp_parse_args( + (array) $raw_value, + array( + 'number' => '', + 'unit' => 'days', + ) + ); + + $value['number'] = ! empty( $value['number'] ) ? absint( $value['number'] ) : ''; + + if ( ! in_array( $value['unit'], array_keys( $periods ), true ) ) { + $value['unit'] = 'days'; + } + + return $value; +} + +/** + * Format the endpoint slug, strip out anything not allowed in a url. + * + * @since 3.5.0 + * @param string $raw_value The raw value. + * @return string + */ +function wc_sanitize_endpoint_slug( $raw_value ) { + return sanitize_title( $raw_value ); +} +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_pay_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_checkout_order_received_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_add_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_delete_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_set_default_payment_method_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_orders_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_view_order_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_downloads_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_account_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_edit_address_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_payment_methods_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_myaccount_lost_password_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); +add_filter( 'woocommerce_admin_settings_sanitize_option_woocommerce_logout_endpoint', 'wc_sanitize_endpoint_slug', 10, 1 ); diff --git a/includes/wc-notice-functions.php b/includes/wc-notice-functions.php new file mode 100644 index 0000000..e119e58 --- /dev/null +++ b/includes/wc-notice-functions.php @@ -0,0 +1,294 @@ +session->get( 'wc_notices', array() ); + + if ( isset( $all_notices[ $notice_type ] ) ) { + + $notice_count = count( $all_notices[ $notice_type ] ); + + } elseif ( empty( $notice_type ) ) { + + foreach ( $all_notices as $notices ) { + $notice_count += count( $notices ); + } + } + + return $notice_count; +} + +/** + * Check if a notice has already been added. + * + * @since 2.1 + * @param string $message The text to display in the notice. + * @param string $notice_type Optional. The name of the notice type - either error, success or notice. + * @return bool + */ +function wc_has_notice( $message, $notice_type = 'success' ) { + if ( ! did_action( 'woocommerce_init' ) ) { + wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); + return false; + } + + $notices = WC()->session->get( 'wc_notices', array() ); + $notices = isset( $notices[ $notice_type ] ) ? $notices[ $notice_type ] : array(); + return array_search( $message, wp_list_pluck( $notices, 'notice' ), true ) !== false; +} + +/** + * Add and store a notice. + * + * @since 2.1 + * @version 3.9.0 + * @param string $message The text to display in the notice. + * @param string $notice_type Optional. The name of the notice type - either error, success or notice. + * @param array $data Optional notice data. + */ +function wc_add_notice( $message, $notice_type = 'success', $data = array() ) { + if ( ! did_action( 'woocommerce_init' ) ) { + wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); + return; + } + + $notices = WC()->session->get( 'wc_notices', array() ); + + // Backward compatibility. + if ( 'success' === $notice_type ) { + $message = apply_filters( 'woocommerce_add_message', $message ); + } + + $message = apply_filters( 'woocommerce_add_' . $notice_type, $message ); + + if ( ! empty( $message ) ) { + $notices[ $notice_type ][] = array( + 'notice' => $message, + 'data' => $data, + ); + } + + WC()->session->set( 'wc_notices', $notices ); +} + +/** + * Set all notices at once. + * + * @since 2.6.0 + * @param array[] $notices Array of notices. + */ +function wc_set_notices( $notices ) { + if ( ! did_action( 'woocommerce_init' ) ) { + wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.6' ); + return; + } + + WC()->session->set( 'wc_notices', $notices ); +} + +/** + * Unset all notices. + * + * @since 2.1 + */ +function wc_clear_notices() { + if ( ! did_action( 'woocommerce_init' ) ) { + wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); + return; + } + WC()->session->set( 'wc_notices', null ); +} + +/** + * Prints messages and errors which are stored in the session, then clears them. + * + * @since 2.1 + * @param bool $return true to return rather than echo. @since 3.5.0. + * @return string|null + */ +function wc_print_notices( $return = false ) { + if ( ! did_action( 'woocommerce_init' ) ) { + wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); + return; + } + + $all_notices = WC()->session->get( 'wc_notices', array() ); + $notice_types = apply_filters( 'woocommerce_notice_types', array( 'error', 'success', 'notice' ) ); + + // Buffer output. + ob_start(); + + foreach ( $notice_types as $notice_type ) { + if ( wc_notice_count( $notice_type ) > 0 ) { + $messages = array(); + + foreach ( $all_notices[ $notice_type ] as $notice ) { + $messages[] = isset( $notice['notice'] ) ? $notice['notice'] : $notice; + } + + wc_get_template( + "notices/{$notice_type}.php", + array( + 'messages' => array_filter( $messages ), // @deprecated 3.9.0 + 'notices' => array_filter( $all_notices[ $notice_type ] ), + ) + ); + } + } + + wc_clear_notices(); + + $notices = wc_kses_notice( ob_get_clean() ); + + if ( $return ) { + return $notices; + } + + echo $notices; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Print a single notice immediately. + * + * @since 2.1 + * @version 3.9.0 + * @param string $message The text to display in the notice. + * @param string $notice_type Optional. The singular name of the notice type - either error, success or notice. + * @param array $data Optional notice data. @since 3.9.0. + */ +function wc_print_notice( $message, $notice_type = 'success', $data = array() ) { + if ( 'success' === $notice_type ) { + $message = apply_filters( 'woocommerce_add_message', $message ); + } + + $message = apply_filters( 'woocommerce_add_' . $notice_type, $message ); + + wc_get_template( + "notices/{$notice_type}.php", + array( + 'messages' => array( $message ), // @deprecated 3.9.0 + 'notices' => array( + array( + 'notice' => $message, + 'data' => $data, + ), + ), + ) + ); +} + +/** + * Returns all queued notices, optionally filtered by a notice type. + * + * @since 2.1 + * @version 3.9.0 + * @param string $notice_type Optional. The singular name of the notice type - either error, success or notice. + * @return array[] + */ +function wc_get_notices( $notice_type = '' ) { + if ( ! did_action( 'woocommerce_init' ) ) { + wc_doing_it_wrong( __FUNCTION__, __( 'This function should not be called before woocommerce_init.', 'woocommerce' ), '2.3' ); + return; + } + + $all_notices = WC()->session->get( 'wc_notices', array() ); + + if ( empty( $notice_type ) ) { + $notices = $all_notices; + } elseif ( isset( $all_notices[ $notice_type ] ) ) { + $notices = $all_notices[ $notice_type ]; + } else { + $notices = array(); + } + + return $notices; +} + +/** + * Add notices for WP Errors. + * + * @param WP_Error $errors Errors. + */ +function wc_add_wp_error_notices( $errors ) { + if ( is_wp_error( $errors ) && $errors->get_error_messages() ) { + foreach ( $errors->get_error_messages() as $error ) { + wc_add_notice( $error, 'error' ); + } + } +} + +/** + * Filters out the same tags as wp_kses_post, but allows tabindex for element. + * + * @since 3.5.0 + * @param string $message Content to filter through kses. + * @return string + */ +function wc_kses_notice( $message ) { + $allowed_tags = array_replace_recursive( + wp_kses_allowed_html( 'post' ), + array( + 'a' => array( + 'tabindex' => true, + ), + ) + ); + + /** + * Kses notice allowed tags. + * + * @since 3.9.0 + * @param array[]|string $allowed_tags An array of allowed HTML elements and attributes, or a context name such as 'post'. + */ + return wp_kses( $message, apply_filters( 'woocommerce_kses_notice_allowed_tags', $allowed_tags ) ); +} + +/** + * Get notice data attribute. + * + * @since 3.9.0 + * @param array $notice Notice data. + * @return string + */ +function wc_get_notice_data_attr( $notice ) { + if ( empty( $notice['data'] ) ) { + return; + } + + $attr = ''; + + foreach ( $notice['data'] as $key => $value ) { + $attr .= sprintf( + ' data-%1$s="%2$s"', + sanitize_title( $key ), + esc_attr( $value ) + ); + } + + return $attr; +} diff --git a/includes/wc-order-functions.php b/includes/wc-order-functions.php new file mode 100644 index 0000000..eca35d8 --- /dev/null +++ b/includes/wc-order-functions.php @@ -0,0 +1,1102 @@ + 'limit', + 'post_type' => 'type', + 'post_status' => 'status', + 'post_parent' => 'parent', + 'author' => 'customer', + 'email' => 'billing_email', + 'posts_per_page' => 'limit', + 'paged' => 'page', + ); + + foreach ( $map_legacy as $from => $to ) { + if ( isset( $args[ $from ] ) ) { + $args[ $to ] = $args[ $from ]; + } + } + + // Map legacy date args to modern date args. + $date_before = false; + $date_after = false; + + if ( ! empty( $args['date_before'] ) ) { + $datetime = wc_string_to_datetime( $args['date_before'] ); + $date_before = strpos( $args['date_before'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); + } + if ( ! empty( $args['date_after'] ) ) { + $datetime = wc_string_to_datetime( $args['date_after'] ); + $date_after = strpos( $args['date_after'], ':' ) ? $datetime->getOffsetTimestamp() : $datetime->date( 'Y-m-d' ); + } + + if ( $date_before && $date_after ) { + $args['date_created'] = $date_after . '...' . $date_before; + } elseif ( $date_before ) { + $args['date_created'] = '<' . $date_before; + } elseif ( $date_after ) { + $args['date_created'] = '>' . $date_after; + } + + $query = new WC_Order_Query( $args ); + return $query->get_orders(); +} + +/** + * Main function for returning orders, uses the WC_Order_Factory class. + * + * @since 2.2 + * + * @param mixed $the_order Post object or post ID of the order. + * + * @return bool|WC_Order|WC_Order_Refund + */ +function wc_get_order( $the_order = false ) { + if ( ! did_action( 'woocommerce_after_register_post_type' ) ) { + wc_doing_it_wrong( __FUNCTION__, 'wc_get_order should not be called before post types are registered (woocommerce_after_register_post_type action)', '2.5' ); + return false; + } + return WC()->order_factory->get_order( $the_order ); +} + +/** + * Get all order statuses. + * + * @since 2.2 + * @used-by WC_Order::set_status + * @return array + */ +function wc_get_order_statuses() { + $order_statuses = array( + 'wc-pending' => _x( 'Pending payment', 'Order status', 'woocommerce' ), + 'wc-processing' => _x( 'Processing', 'Order status', 'woocommerce' ), + 'wc-on-hold' => _x( 'On hold', 'Order status', 'woocommerce' ), + 'wc-completed' => _x( 'Completed', 'Order status', 'woocommerce' ), + 'wc-cancelled' => _x( 'Cancelled', 'Order status', 'woocommerce' ), + 'wc-refunded' => _x( 'Refunded', 'Order status', 'woocommerce' ), + 'wc-failed' => _x( 'Failed', 'Order status', 'woocommerce' ), + ); + return apply_filters( 'wc_order_statuses', $order_statuses ); +} + +/** + * See if a string is an order status. + * + * @param string $maybe_status Status, including any wc- prefix. + * @return bool + */ +function wc_is_order_status( $maybe_status ) { + $order_statuses = wc_get_order_statuses(); + return isset( $order_statuses[ $maybe_status ] ); +} + +/** + * Get list of statuses which are consider 'paid'. + * + * @since 3.0.0 + * @return array + */ +function wc_get_is_paid_statuses() { + return apply_filters( 'woocommerce_order_is_paid_statuses', array( 'processing', 'completed' ) ); +} + +/** + * Get list of statuses which are consider 'pending payment'. + * + * @since 3.6.0 + * @return array + */ +function wc_get_is_pending_statuses() { + return apply_filters( 'woocommerce_order_is_pending_statuses', array( 'pending' ) ); +} + +/** + * Get the nice name for an order status. + * + * @since 2.2 + * @param string $status Status. + * @return string + */ +function wc_get_order_status_name( $status ) { + $statuses = wc_get_order_statuses(); + $status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status; + $status = isset( $statuses[ 'wc-' . $status ] ) ? $statuses[ 'wc-' . $status ] : $status; + return $status; +} + +/** + * Generate an order key with prefix. + * + * @since 3.5.4 + * @param string $key Order key without a prefix. By default generates a 13 digit secret. + * @return string The order key. + */ +function wc_generate_order_key( $key = '' ) { + if ( '' === $key ) { + $key = wp_generate_password( 13, false ); + } + + return 'wc_' . apply_filters( 'woocommerce_generate_order_key', 'order_' . $key ); +} + +/** + * Finds an Order ID based on an order key. + * + * @param string $order_key An order key has generated by. + * @return int The ID of an order, or 0 if the order could not be found. + */ +function wc_get_order_id_by_order_key( $order_key ) { + $data_store = WC_Data_Store::load( 'order' ); + return $data_store->get_order_id_by_order_key( $order_key ); +} + +/** + * Get all registered order types. + * + * @since 2.2 + * @param string $for Optionally define what you are getting order types for so + * only relevant types are returned. + * e.g. for 'order-meta-boxes', 'order-count'. + * @return array + */ +function wc_get_order_types( $for = '' ) { + global $wc_order_types; + + if ( ! is_array( $wc_order_types ) ) { + $wc_order_types = array(); + } + + $order_types = array(); + + switch ( $for ) { + case 'order-count': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_count'] ) { + $order_types[] = $type; + } + } + break; + case 'order-meta-boxes': + foreach ( $wc_order_types as $type => $args ) { + if ( $args['add_order_meta_boxes'] ) { + $order_types[] = $type; + } + } + break; + case 'view-orders': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_views'] ) { + $order_types[] = $type; + } + } + break; + case 'reports': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_reports'] ) { + $order_types[] = $type; + } + } + break; + case 'sales-reports': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_sales_reports'] ) { + $order_types[] = $type; + } + } + break; + case 'order-webhooks': + foreach ( $wc_order_types as $type => $args ) { + if ( ! $args['exclude_from_order_webhooks'] ) { + $order_types[] = $type; + } + } + break; + default: + $order_types = array_keys( $wc_order_types ); + break; + } + + return apply_filters( 'wc_order_types', $order_types, $for ); +} + +/** + * Get an order type by post type name. + * + * @param string $type Post type name. + * @return bool|array Details about the order type. + */ +function wc_get_order_type( $type ) { + global $wc_order_types; + + if ( isset( $wc_order_types[ $type ] ) ) { + return $wc_order_types[ $type ]; + } + + return false; +} + +/** + * Register order type. Do not use before init. + * + * Wrapper for register post type, as well as a method of telling WC which. + * post types are types of orders, and having them treated as such. + * + * $args are passed to register_post_type, but there are a few specific to this function: + * - exclude_from_orders_screen (bool) Whether or not this order type also get shown in the main. + * orders screen. + * - add_order_meta_boxes (bool) Whether or not the order type gets shop_order meta boxes. + * - exclude_from_order_count (bool) Whether or not this order type is excluded from counts. + * - exclude_from_order_views (bool) Whether or not this order type is visible by customers when. + * viewing orders e.g. on the my account page. + * - exclude_from_order_reports (bool) Whether or not to exclude this type from core reports. + * - exclude_from_order_sales_reports (bool) Whether or not to exclude this type from core sales reports. + * + * @since 2.2 + * @see register_post_type for $args used in that function + * @param string $type Post type. (max. 20 characters, can not contain capital letters or spaces). + * @param array $args An array of arguments. + * @return bool Success or failure + */ +function wc_register_order_type( $type, $args = array() ) { + if ( post_type_exists( $type ) ) { + return false; + } + + global $wc_order_types; + + if ( ! is_array( $wc_order_types ) ) { + $wc_order_types = array(); + } + + // Register as a post type. + if ( is_wp_error( register_post_type( $type, $args ) ) ) { + return false; + } + + // Register for WC usage. + $order_type_args = array( + 'exclude_from_orders_screen' => false, + 'add_order_meta_boxes' => true, + 'exclude_from_order_count' => false, + 'exclude_from_order_views' => false, + 'exclude_from_order_webhooks' => false, + 'exclude_from_order_reports' => false, + 'exclude_from_order_sales_reports' => false, + 'class_name' => 'WC_Order', + ); + + $args = array_intersect_key( $args, $order_type_args ); + $args = wp_parse_args( $args, $order_type_args ); + $wc_order_types[ $type ] = $args; + + return true; +} + +/** + * Return the count of processing orders. + * + * @return int + */ +function wc_processing_order_count() { + return wc_orders_count( 'processing' ); +} + +/** + * Return the orders count of a specific order status. + * + * @param string $status Status. + * @return int + */ +function wc_orders_count( $status ) { + $count = 0; + $status = 'wc-' . $status; + $order_statuses = array_keys( wc_get_order_statuses() ); + + if ( ! in_array( $status, $order_statuses, true ) ) { + return 0; + } + + $cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . $status; + $cached_count = wp_cache_get( $cache_key, 'counts' ); + + if ( false !== $cached_count ) { + return $cached_count; + } + + foreach ( wc_get_order_types( 'order-count' ) as $type ) { + $data_store = WC_Data_Store::load( 'shop_order' === $type ? 'order' : $type ); + if ( $data_store ) { + $count += $data_store->get_order_count( $status ); + } + } + + wp_cache_set( $cache_key, $count, 'counts' ); + + return $count; +} + +/** + * Grant downloadable product access to the file identified by $download_id. + * + * @param string $download_id File identifier. + * @param int|WC_Product $product Product instance or ID. + * @param WC_Order $order Order data. + * @param int $qty Quantity purchased. + * @param WC_Order_Item $item Item of the order. + * @return int|bool insert id or false on failure. + */ +function wc_downloadable_file_permission( $download_id, $product, $order, $qty = 1, $item = null ) { + if ( is_numeric( $product ) ) { + $product = wc_get_product( $product ); + } + $download = new WC_Customer_Download(); + $download->set_download_id( $download_id ); + $download->set_product_id( $product->get_id() ); + $download->set_user_id( $order->get_customer_id() ); + $download->set_order_id( $order->get_id() ); + $download->set_user_email( $order->get_billing_email() ); + $download->set_order_key( $order->get_order_key() ); + $download->set_downloads_remaining( 0 > $product->get_download_limit() ? '' : $product->get_download_limit() * $qty ); + $download->set_access_granted( time() ); + $download->set_download_count( 0 ); + + $expiry = $product->get_download_expiry(); + + if ( $expiry > 0 ) { + $from_date = $order->get_date_completed() ? $order->get_date_completed()->format( 'Y-m-d' ) : current_time( 'mysql', true ); + $download->set_access_expires( strtotime( $from_date . ' + ' . $expiry . ' DAY' ) ); + } + + $download = apply_filters( 'woocommerce_downloadable_file_permission', $download, $product, $order, $qty, $item ); + + return $download->save(); +} + +/** + * Order Status completed - give downloadable product access to customer. + * + * @param int $order_id Order ID. + * @param bool $force Force downloadable permissions. + */ +function wc_downloadable_product_permissions( $order_id, $force = false ) { + $order = wc_get_order( $order_id ); + + if ( ! $order || ( $order->get_data_store()->get_download_permissions_granted( $order ) && ! $force ) ) { + return; + } + + if ( $order->has_status( 'processing' ) && 'no' === get_option( 'woocommerce_downloads_grant_access_after_payment' ) ) { + return; + } + + if ( count( $order->get_items() ) > 0 ) { + foreach ( $order->get_items() as $item ) { + $product = $item->get_product(); + + if ( $product && $product->exists() && $product->is_downloadable() ) { + $downloads = $product->get_downloads(); + + foreach ( array_keys( $downloads ) as $download_id ) { + wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity(), $item ); + } + } + } + } + + $order->get_data_store()->set_download_permissions_granted( $order, true ); + do_action( 'woocommerce_grant_product_download_permissions', $order_id ); +} +add_action( 'woocommerce_order_status_completed', 'wc_downloadable_product_permissions' ); +add_action( 'woocommerce_order_status_processing', 'wc_downloadable_product_permissions' ); + +/** + * Clear all transients cache for order data. + * + * @param int|WC_Order $order Order instance or ID. + */ +function wc_delete_shop_order_transients( $order = 0 ) { + if ( is_numeric( $order ) ) { + $order = wc_get_order( $order ); + } + $reports = WC_Admin_Reports::get_reports(); + $transients_to_clear = array( + 'wc_admin_report', + ); + + foreach ( $reports as $report_group ) { + foreach ( $report_group['reports'] as $report_key => $report ) { + $transients_to_clear[] = 'wc_report_' . $report_key; + } + } + + foreach ( $transients_to_clear as $transient ) { + delete_transient( $transient ); + } + + // Clear customer's order related caches. + if ( is_a( $order, 'WC_Order' ) ) { + $order_id = $order->get_id(); + delete_user_meta( $order->get_customer_id(), '_money_spent' ); + delete_user_meta( $order->get_customer_id(), '_order_count' ); + delete_user_meta( $order->get_customer_id(), '_last_order' ); + } else { + $order_id = 0; + } + + // Increments the transient version to invalidate cache. + WC_Cache_Helper::get_transient_version( 'orders', true ); + + // Do the same for regular cache. + WC_Cache_Helper::invalidate_cache_group( 'orders' ); + + do_action( 'woocommerce_delete_shop_order_transients', $order_id ); +} + +/** + * See if we only ship to billing addresses. + * + * @return bool + */ +function wc_ship_to_billing_address_only() { + return 'billing_only' === get_option( 'woocommerce_ship_to_destination' ); +} + +/** + * Create a new order refund programmatically. + * + * Returns a new refund object on success which can then be used to add additional data. + * + * @since 2.2 + * @throws Exception Throws exceptions when fail to create, but returns WP_Error instead. + * @param array $args New refund arguments. + * @return WC_Order_Refund|WP_Error + */ +function wc_create_refund( $args = array() ) { + $default_args = array( + 'amount' => 0, + 'reason' => null, + 'order_id' => 0, + 'refund_id' => 0, + 'line_items' => array(), + 'refund_payment' => false, + 'restock_items' => false, + ); + + try { + $args = wp_parse_args( $args, $default_args ); + $order = wc_get_order( $args['order_id'] ); + + if ( ! $order ) { + throw new Exception( __( 'Invalid order ID.', 'woocommerce' ) ); + } + + $remaining_refund_amount = $order->get_remaining_refund_amount(); + $remaining_refund_items = $order->get_remaining_refund_items(); + $refund_item_count = 0; + $refund = new WC_Order_Refund( $args['refund_id'] ); + + if ( 0 > $args['amount'] || $args['amount'] > $remaining_refund_amount ) { + throw new Exception( __( 'Invalid refund amount.', 'woocommerce' ) ); + } + + $refund->set_currency( $order->get_currency() ); + $refund->set_amount( $args['amount'] ); + $refund->set_parent_id( absint( $args['order_id'] ) ); + $refund->set_refunded_by( get_current_user_id() ? get_current_user_id() : 1 ); + $refund->set_prices_include_tax( $order->get_prices_include_tax() ); + + if ( ! is_null( $args['reason'] ) ) { + $refund->set_reason( $args['reason'] ); + } + + // Negative line items. + if ( count( $args['line_items'] ) > 0 ) { + $items = $order->get_items( array( 'line_item', 'fee', 'shipping' ) ); + + foreach ( $items as $item_id => $item ) { + if ( ! isset( $args['line_items'][ $item_id ] ) ) { + continue; + } + + $qty = isset( $args['line_items'][ $item_id ]['qty'] ) ? $args['line_items'][ $item_id ]['qty'] : 0; + $refund_total = $args['line_items'][ $item_id ]['refund_total']; + $refund_tax = isset( $args['line_items'][ $item_id ]['refund_tax'] ) ? array_filter( (array) $args['line_items'][ $item_id ]['refund_tax'] ) : array(); + + if ( empty( $qty ) && empty( $refund_total ) && empty( $args['line_items'][ $item_id ]['refund_tax'] ) ) { + continue; + } + + $class = get_class( $item ); + $refunded_item = new $class( $item ); + $refunded_item->set_id( 0 ); + $refunded_item->add_meta_data( '_refunded_item_id', $item_id, true ); + $refunded_item->set_total( wc_format_refund_total( $refund_total ) ); + $refunded_item->set_taxes( + array( + 'total' => array_map( 'wc_format_refund_total', $refund_tax ), + 'subtotal' => array_map( 'wc_format_refund_total', $refund_tax ), + ) + ); + + if ( is_callable( array( $refunded_item, 'set_subtotal' ) ) ) { + $refunded_item->set_subtotal( wc_format_refund_total( $refund_total ) ); + } + + if ( is_callable( array( $refunded_item, 'set_quantity' ) ) ) { + $refunded_item->set_quantity( $qty * -1 ); + } + + $refund->add_item( $refunded_item ); + $refund_item_count += $qty; + } + } + + $refund->update_taxes(); + $refund->calculate_totals( false ); + $refund->set_total( $args['amount'] * -1 ); + + // this should remain after update_taxes(), as this will save the order, and write the current date to the db + // so we must wait until the order is persisted to set the date. + if ( isset( $args['date_created'] ) ) { + $refund->set_date_created( $args['date_created'] ); + } + + /** + * Action hook to adjust refund before save. + * + * @since 3.0.0 + */ + do_action( 'woocommerce_create_refund', $refund, $args ); + + if ( $refund->save() ) { + if ( $args['refund_payment'] ) { + $result = wc_refund_payment( $order, $refund->get_amount(), $refund->get_reason() ); + + if ( is_wp_error( $result ) ) { + $refund->delete(); + return $result; + } + + $refund->set_refunded_payment( true ); + $refund->save(); + } + + if ( $args['restock_items'] ) { + wc_restock_refunded_items( $order, $args['line_items'] ); + } + + // Trigger notification emails. + if ( ( $remaining_refund_amount - $args['amount'] ) > 0 || ( $order->has_free_item() && ( $remaining_refund_items - $refund_item_count ) > 0 ) ) { + do_action( 'woocommerce_order_partially_refunded', $order->get_id(), $refund->get_id() ); + } else { + do_action( 'woocommerce_order_fully_refunded', $order->get_id(), $refund->get_id() ); + + $parent_status = apply_filters( 'woocommerce_order_fully_refunded_status', 'refunded', $order->get_id(), $refund->get_id() ); + + if ( $parent_status ) { + $order->update_status( $parent_status ); + } + } + } + + do_action( 'woocommerce_refund_created', $refund->get_id(), $args ); + do_action( 'woocommerce_order_refunded', $order->get_id(), $refund->get_id() ); + + } catch ( Exception $e ) { + if ( isset( $refund ) && is_a( $refund, 'WC_Order_Refund' ) ) { + wp_delete_post( $refund->get_id(), true ); + } + return new WP_Error( 'error', $e->getMessage() ); + } + + return $refund; +} + +/** + * Try to refund the payment for an order via the gateway. + * + * @since 3.0.0 + * @throws Exception Throws exceptions when fail to refund, but returns WP_Error instead. + * @param WC_Order $order Order instance. + * @param string $amount Amount to refund. + * @param string $reason Refund reason. + * @return bool|WP_Error + */ +function wc_refund_payment( $order, $amount, $reason = '' ) { + try { + if ( ! is_a( $order, 'WC_Order' ) ) { + throw new Exception( __( 'Invalid order.', 'woocommerce' ) ); + } + + $gateway_controller = WC_Payment_Gateways::instance(); + $all_gateways = $gateway_controller->payment_gateways(); + $payment_method = $order->get_payment_method(); + $gateway = isset( $all_gateways[ $payment_method ] ) ? $all_gateways[ $payment_method ] : false; + + if ( ! $gateway ) { + throw new Exception( __( 'The payment gateway for this order does not exist.', 'woocommerce' ) ); + } + + if ( ! $gateway->supports( 'refunds' ) ) { + throw new Exception( __( 'The payment gateway for this order does not support automatic refunds.', 'woocommerce' ) ); + } + + $result = $gateway->process_refund( $order->get_id(), $amount, $reason ); + + if ( ! $result ) { + throw new Exception( __( 'An error occurred while attempting to create the refund using the payment gateway API.', 'woocommerce' ) ); + } + + if ( is_wp_error( $result ) ) { + throw new Exception( $result->get_error_message() ); + } + + return true; + + } catch ( Exception $e ) { + return new WP_Error( 'error', $e->getMessage() ); + } +} + +/** + * Restock items during refund. + * + * @since 3.0.0 + * @param WC_Order $order Order instance. + * @param array $refunded_line_items Refunded items list. + */ +function wc_restock_refunded_items( $order, $refunded_line_items ) { + if ( ! apply_filters( 'woocommerce_can_restock_refunded_items', true, $order, $refunded_line_items ) ) { + return; + } + + $line_items = $order->get_items(); + + foreach ( $line_items as $item_id => $item ) { + if ( ! isset( $refunded_line_items[ $item_id ], $refunded_line_items[ $item_id ]['qty'] ) ) { + continue; + } + $product = $item->get_product(); + $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); + $restock_refunded_items = (int) $item->get_meta( '_restock_refunded_items', true ); + $qty_to_refund = $refunded_line_items[ $item_id ]['qty']; + + if ( ! $item_stock_reduced || ! $qty_to_refund || ! $product || ! $product->managing_stock() ) { + continue; + } + + $old_stock = $product->get_stock_quantity(); + $new_stock = wc_update_product_stock( $product, $qty_to_refund, 'increase' ); + + // Update _reduced_stock meta to track changes. + $item_stock_reduced = $item_stock_reduced - $qty_to_refund; + + if ( 0 < $item_stock_reduced ) { + // Keeps track of total running tally of reduced stock. + $item->update_meta_data( '_reduced_stock', $item_stock_reduced ); + + // Keeps track of only refunded items that needs restock. + $item->update_meta_data( '_restock_refunded_items', $qty_to_refund + $restock_refunded_items ); + } else { + $item->delete_meta_data( '_reduced_stock' ); + $item->delete_meta_data( '_restock_refunded_items' ); + } + + /* translators: 1: product ID 2: old stock level 3: new stock level */ + $order->add_order_note( sprintf( __( 'Item #%1$s stock increased from %2$s to %3$s.', 'woocommerce' ), $product->get_id(), $old_stock, $new_stock ) ); + + $item->save(); + + do_action( 'woocommerce_restock_refunded_item', $product->get_id(), $old_stock, $new_stock, $order, $product ); + } +} + +/** + * Get tax class by tax id. + * + * @since 2.2 + * @param int $tax_id Tax ID. + * @return string + */ +function wc_get_tax_class_by_tax_id( $tax_id ) { + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT tax_rate_class FROM {$wpdb->prefix}woocommerce_tax_rates WHERE tax_rate_id = %d", $tax_id ) ); +} + +/** + * Get payment gateway class by order data. + * + * @since 2.2 + * @param int|WC_Order $order Order instance. + * @return WC_Payment_Gateway|bool + */ +function wc_get_payment_gateway_by_order( $order ) { + if ( WC()->payment_gateways() ) { + $payment_gateways = WC()->payment_gateways()->payment_gateways(); + } else { + $payment_gateways = array(); + } + + if ( ! is_object( $order ) ) { + $order_id = absint( $order ); + $order = wc_get_order( $order_id ); + } + + return is_a( $order, 'WC_Order' ) && isset( $payment_gateways[ $order->get_payment_method() ] ) ? $payment_gateways[ $order->get_payment_method() ] : false; +} + +/** + * When refunding an order, create a refund line item if the partial refunds do not match order total. + * + * This is manual; no gateway refund will be performed. + * + * @since 2.4 + * @param int $order_id Order ID. + */ +function wc_order_fully_refunded( $order_id ) { + $order = wc_get_order( $order_id ); + $max_refund = wc_format_decimal( $order->get_total() - $order->get_total_refunded() ); + + if ( ! $max_refund ) { + return; + } + + // Create the refund object. + wc_switch_to_site_locale(); + wc_create_refund( + array( + 'amount' => $max_refund, + 'reason' => __( 'Order fully refunded.', 'woocommerce' ), + 'order_id' => $order_id, + 'line_items' => array(), + ) + ); + wc_restore_locale(); + + $order->add_order_note( __( 'Order status set to refunded. To return funds to the customer you will need to issue a refund through your payment gateway.', 'woocommerce' ) ); +} +add_action( 'woocommerce_order_status_refunded', 'wc_order_fully_refunded' ); + +/** + * Search orders. + * + * @since 2.6.0 + * @param string $term Term to search. + * @return array List of orders ID. + */ +function wc_order_search( $term ) { + $data_store = WC_Data_Store::load( 'order' ); + return $data_store->search_orders( str_replace( 'Order #', '', wc_clean( $term ) ) ); +} + +/** + * Update total sales amount for each product within a paid order. + * + * @since 3.0.0 + * @param int $order_id Order ID. + */ +function wc_update_total_sales_counts( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order || $order->get_data_store()->get_recorded_sales( $order ) ) { + return; + } + + if ( count( $order->get_items() ) > 0 ) { + foreach ( $order->get_items() as $item ) { + $product_id = $item->get_product_id(); + + if ( $product_id ) { + $data_store = WC_Data_Store::load( 'product' ); + $data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), 'increase' ); + } + } + } + + $order->get_data_store()->set_recorded_sales( $order, true ); + + /** + * Called when sales for an order are recorded + * + * @param int $order_id order id + */ + do_action( 'woocommerce_recorded_sales', $order_id ); +} +add_action( 'woocommerce_order_status_completed', 'wc_update_total_sales_counts' ); +add_action( 'woocommerce_order_status_processing', 'wc_update_total_sales_counts' ); +add_action( 'woocommerce_order_status_on-hold', 'wc_update_total_sales_counts' ); + +/** + * Update used coupon amount for each coupon within an order. + * + * @since 3.0.0 + * @param int $order_id Order ID. + */ +function wc_update_coupon_usage_counts( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return; + } + + $has_recorded = $order->get_data_store()->get_recorded_coupon_usage_counts( $order ); + + if ( $order->has_status( 'cancelled' ) && $has_recorded ) { + $action = 'reduce'; + $order->get_data_store()->set_recorded_coupon_usage_counts( $order, false ); + } elseif ( ! $order->has_status( 'cancelled' ) && ! $has_recorded ) { + $action = 'increase'; + $order->get_data_store()->set_recorded_coupon_usage_counts( $order, true ); + } elseif ( $order->has_status( 'cancelled' ) ) { + $order->get_data_store()->release_held_coupons( $order, true ); + return; + } else { + return; + } + + if ( count( $order->get_coupon_codes() ) > 0 ) { + foreach ( $order->get_coupon_codes() as $code ) { + if ( ! $code ) { + continue; + } + + $coupon = new WC_Coupon( $code ); + $used_by = $order->get_user_id(); + + if ( ! $used_by ) { + $used_by = $order->get_billing_email(); + } + + switch ( $action ) { + case 'reduce': + $coupon->decrease_usage_count( $used_by ); + break; + case 'increase': + $coupon->increase_usage_count( $used_by, $order ); + break; + } + } + $order->get_data_store()->release_held_coupons( $order, true ); + } +} +add_action( 'woocommerce_order_status_pending', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_completed', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_processing', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_on-hold', 'wc_update_coupon_usage_counts' ); +add_action( 'woocommerce_order_status_cancelled', 'wc_update_coupon_usage_counts' ); + +/** + * Cancel all unpaid orders after held duration to prevent stock lock for those products. + */ +function wc_cancel_unpaid_orders() { + $held_duration = get_option( 'woocommerce_hold_stock_minutes' ); + + // Re-schedule the event before cancelling orders + // this way in case of a DB timeout or (plugin) crash the event is always scheduled for retry. + wp_clear_scheduled_hook( 'woocommerce_cancel_unpaid_orders' ); + $cancel_unpaid_interval = apply_filters( 'woocommerce_cancel_unpaid_orders_interval_minutes', absint( $held_duration ) ); + wp_schedule_single_event( time() + ( absint( $cancel_unpaid_interval ) * 60 ), 'woocommerce_cancel_unpaid_orders' ); + + if ( $held_duration < 1 || 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { + return; + } + + $data_store = WC_Data_Store::load( 'order' ); + $unpaid_orders = $data_store->get_unpaid_orders( strtotime( '-' . absint( $held_duration ) . ' MINUTES', current_time( 'timestamp' ) ) ); + + if ( $unpaid_orders ) { + foreach ( $unpaid_orders as $unpaid_order ) { + $order = wc_get_order( $unpaid_order ); + + if ( apply_filters( 'woocommerce_cancel_unpaid_order', 'checkout' === $order->get_created_via(), $order ) ) { + $order->update_status( 'cancelled', __( 'Unpaid order cancelled - time limit reached.', 'woocommerce' ) ); + } + } + } +} +add_action( 'woocommerce_cancel_unpaid_orders', 'wc_cancel_unpaid_orders' ); + +/** + * Sanitize order id removing unwanted characters. + * + * E.g Users can sometimes try to track an order id using # with no success. + * This function will fix this. + * + * @since 3.1.0 + * @param int $order_id Order ID. + */ +function wc_sanitize_order_id( $order_id ) { + return (int) filter_var( $order_id, FILTER_SANITIZE_NUMBER_INT ); +} +add_filter( 'woocommerce_shortcode_order_tracking_order_id', 'wc_sanitize_order_id' ); + +/** + * Get an order note. + * + * @since 3.2.0 + * @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only). + * @return stdClass|null Object with order note details or null when does not exists. + */ +function wc_get_order_note( $data ) { + if ( is_numeric( $data ) ) { + $data = get_comment( $data ); + } + + if ( ! is_a( $data, 'WP_Comment' ) ) { + return null; + } + + return (object) apply_filters( + 'woocommerce_get_order_note', + array( + 'id' => (int) $data->comment_ID, + 'date_created' => wc_string_to_datetime( $data->comment_date ), + 'content' => $data->comment_content, + 'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ), + 'added_by' => __( 'WooCommerce', 'woocommerce' ) === $data->comment_author ? 'system' : $data->comment_author, + ), + $data + ); +} + +/** + * Get order notes. + * + * @since 3.2.0 + * @param array $args Query arguments { + * Array of query parameters. + * + * @type string $limit Maximum number of notes to retrieve. + * Default empty (no limit). + * @type int $order_id Limit results to those affiliated with a given order ID. + * Default 0. + * @type array $order__in Array of order IDs to include affiliated notes for. + * Default empty. + * @type array $order__not_in Array of order IDs to exclude affiliated notes for. + * Default empty. + * @type string $orderby Define how should sort notes. + * Accepts 'date_created', 'date_created_gmt' or 'id'. + * Default: 'id'. + * @type string $order How to order retrieved notes. + * Accepts 'ASC' or 'DESC'. + * Default: 'DESC'. + * @type string $type Define what type of note should retrieve. + * Accepts 'customer', 'internal' or empty for both. + * Default empty. + * } + * @return stdClass[] Array of stdClass objects with order notes details. + */ +function wc_get_order_notes( $args ) { + $key_mapping = array( + 'limit' => 'number', + 'order_id' => 'post_id', + 'order__in' => 'post__in', + 'order__not_in' => 'post__not_in', + ); + + foreach ( $key_mapping as $query_key => $db_key ) { + if ( isset( $args[ $query_key ] ) ) { + $args[ $db_key ] = $args[ $query_key ]; + unset( $args[ $query_key ] ); + } + } + + // Define orderby. + $orderby_mapping = array( + 'date_created' => 'comment_date', + 'date_created_gmt' => 'comment_date_gmt', + 'id' => 'comment_ID', + ); + + $args['orderby'] = ! empty( $args['orderby'] ) && in_array( $args['orderby'], array( 'date_created', 'date_created_gmt', 'id' ), true ) ? $orderby_mapping[ $args['orderby'] ] : 'comment_ID'; + + // Set WooCommerce order type. + if ( isset( $args['type'] ) && 'customer' === $args['type'] ) { + $args['meta_query'] = array( // WPCS: slow query ok. + array( + 'key' => 'is_customer_note', + 'value' => 1, + 'compare' => '=', + ), + ); + } elseif ( isset( $args['type'] ) && 'internal' === $args['type'] ) { + $args['meta_query'] = array( // WPCS: slow query ok. + array( + 'key' => 'is_customer_note', + 'compare' => 'NOT EXISTS', + ), + ); + } + + // Set correct comment type. + $args['type'] = 'order_note'; + + // Always approved. + $args['status'] = 'approve'; + + // Does not support 'count' or 'fields'. + unset( $args['count'], $args['fields'] ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + return array_filter( array_map( 'wc_get_order_note', $notes ) ); +} + +/** + * Create an order note. + * + * @since 3.2.0 + * @param int $order_id Order ID. + * @param string $note Note to add. + * @param bool $is_customer_note If is a costumer note. + * @param bool $added_by_user If note is create by an user. + * @return int|WP_Error Integer when created or WP_Error when found an error. + */ +function wc_create_order_note( $order_id, $note, $is_customer_note = false, $added_by_user = false ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return new WP_Error( 'invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + return $order->add_order_note( $note, (int) $is_customer_note, $added_by_user ); +} + +/** + * Delete an order note. + * + * @since 3.2.0 + * @param int $note_id Order note. + * @return bool True on success, false on failure. + */ +function wc_delete_order_note( $note_id ) { + return wp_delete_comment( $note_id, true ); +} diff --git a/includes/wc-order-item-functions.php b/includes/wc-order-item-functions.php new file mode 100644 index 0000000..5f7c6a1 --- /dev/null +++ b/includes/wc-order-item-functions.php @@ -0,0 +1,181 @@ + '', + 'order_item_type' => 'line_item', + ); + + $item_array = wp_parse_args( $item_array, $defaults ); + $data_store = WC_Data_Store::load( 'order-item' ); + $item_id = $data_store->add_order_item( $order_id, $item_array ); + $item = WC_Order_Factory::get_order_item( $item_id ); + + do_action( 'woocommerce_new_order_item', $item_id, $item, $order_id ); + + return $item_id; +} + +/** + * Update an item for an order. + * + * @since 2.2 + * @param int $item_id Item ID. + * @param array $args Either `order_item_type` or `order_item_name`. + * + * @throws Exception When `WC_Data_Store::load` validation fails. + * @return bool True if successfully updated, false otherwise. + */ +function wc_update_order_item( $item_id, $args ) { + $data_store = WC_Data_Store::load( 'order-item' ); + $update = $data_store->update_order_item( $item_id, $args ); + + if ( false === $update ) { + return false; + } + + do_action( 'woocommerce_update_order_item', $item_id, $args ); + + return true; +} + +/** + * Delete an item from the order it belongs to based on item id. + * + * @param int $item_id Item ID. + * + * @throws Exception When `WC_Data_Store::load` validation fails. + * @return bool + */ +function wc_delete_order_item( $item_id ) { + $item_id = absint( $item_id ); + + if ( ! $item_id ) { + return false; + } + + $data_store = WC_Data_Store::load( 'order-item' ); + + do_action( 'woocommerce_before_delete_order_item', $item_id ); + + $data_store->delete_order_item( $item_id ); + + do_action( 'woocommerce_delete_order_item', $item_id ); + + return true; +} + +/** + * WooCommerce Order Item Meta API - Update term meta. + * + * @param int $item_id Item ID. + * @param string $meta_key Meta key. + * @param string $meta_value Meta value. + * @param string $prev_value Previous value (default: ''). + * + * @throws Exception When `WC_Data_Store::load` validation fails. + * @return bool + */ +function wc_update_order_item_meta( $item_id, $meta_key, $meta_value, $prev_value = '' ) { + $data_store = WC_Data_Store::load( 'order-item' ); + if ( $data_store->update_metadata( $item_id, $meta_key, $meta_value, $prev_value ) ) { + WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache. + return true; + } + return false; +} + +/** + * WooCommerce Order Item Meta API - Add term meta. + * + * @param int $item_id Item ID. + * @param string $meta_key Meta key. + * @param string $meta_value Meta value. + * @param bool $unique If meta data should be unique (default: false). + * + * @throws Exception When `WC_Data_Store::load` validation fails. + * @return int New row ID or 0. + */ +function wc_add_order_item_meta( $item_id, $meta_key, $meta_value, $unique = false ) { + $data_store = WC_Data_Store::load( 'order-item' ); + $meta_id = $data_store->add_metadata( $item_id, $meta_key, $meta_value, $unique ); + + if ( $meta_id ) { + WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache. + return $meta_id; + } + return 0; +} + +/** + * WooCommerce Order Item Meta API - Delete term meta. + * + * @param int $item_id Item ID. + * @param string $meta_key Meta key. + * @param string $meta_value Meta value (default: ''). + * @param bool $delete_all Delete all meta data, defaults to `false`. + * + * @throws Exception When `WC_Data_Store::load` validation fails. + * @return bool + */ +function wc_delete_order_item_meta( $item_id, $meta_key, $meta_value = '', $delete_all = false ) { + $data_store = WC_Data_Store::load( 'order-item' ); + if ( $data_store->delete_metadata( $item_id, $meta_key, $meta_value, $delete_all ) ) { + WC_Cache_Helper::invalidate_cache_group( 'object_' . $item_id ); // Invalidate cache. + return true; + } + return false; +} + +/** + * WooCommerce Order Item Meta API - Get term meta. + * + * @param int $item_id Item ID. + * @param string $key Meta key. + * @param bool $single Whether to return a single value. (default: true). + * + * @throws Exception When `WC_Data_Store::load` validation fails. + * @return mixed + */ +function wc_get_order_item_meta( $item_id, $key, $single = true ) { + $data_store = WC_Data_Store::load( 'order-item' ); + return $data_store->get_metadata( $item_id, $key, $single ); +} + +/** + * Get order ID by order item ID. + * + * @param int $item_id Item ID. + * + * @throws Exception When `WC_Data_Store::load` validation fails. + * @return int + */ +function wc_get_order_id_by_order_item_id( $item_id ) { + $data_store = WC_Data_Store::load( 'order-item' ); + return $data_store->get_order_id_by_order_item_id( $item_id ); +} diff --git a/includes/wc-page-functions.php b/includes/wc-page-functions.php new file mode 100644 index 0000000..7519db9 --- /dev/null +++ b/includes/wc-page-functions.php @@ -0,0 +1,227 @@ +query->get_current_endpoint(); + $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; + $endpoint_title = WC()->query->get_endpoint_title( $endpoint, $action ); + $title = $endpoint_title ? $endpoint_title : $title; + + remove_filter( 'the_title', 'wc_page_endpoint_title' ); + } + + return $title; +} + +add_filter( 'the_title', 'wc_page_endpoint_title' ); + +/** + * Retrieve page ids - used for myaccount, edit_address, shop, cart, checkout, pay, view_order, terms. returns -1 if no page is found. + * + * @param string $page Page slug. + * @return int + */ +function wc_get_page_id( $page ) { + if ( 'pay' === $page || 'thanks' === $page ) { + wc_deprecated_argument( __FUNCTION__, '2.1', 'The "pay" and "thanks" pages are no-longer used - an endpoint is added to the checkout instead. To get a valid link use the WC_Order::get_checkout_payment_url() or WC_Order::get_checkout_order_received_url() methods instead.' ); + + $page = 'checkout'; + } + if ( 'change_password' === $page || 'edit_address' === $page || 'lost_password' === $page ) { + wc_deprecated_argument( __FUNCTION__, '2.1', 'The "change_password", "edit_address" and "lost_password" pages are no-longer used - an endpoint is added to the my-account instead. To get a valid link use the wc_customer_edit_account_url() function instead.' ); + + $page = 'myaccount'; + } + + $page = apply_filters( 'woocommerce_get_' . $page . '_page_id', get_option( 'woocommerce_' . $page . '_page_id' ) ); + + return $page ? absint( $page ) : -1; +} + +/** + * Retrieve page permalink. + * + * @param string $page page slug. + * @param string|bool $fallback Fallback URL if page is not set. Defaults to home URL. @since 3.4.0. + * @return string + */ +function wc_get_page_permalink( $page, $fallback = null ) { + $page_id = wc_get_page_id( $page ); + $permalink = 0 < $page_id ? get_permalink( $page_id ) : ''; + + if ( ! $permalink ) { + $permalink = is_null( $fallback ) ? get_home_url() : $fallback; + } + + return apply_filters( 'woocommerce_get_' . $page . '_page_permalink', $permalink ); +} + +/** + * Get endpoint URL. + * + * Gets the URL for an endpoint, which varies depending on permalink settings. + * + * @param string $endpoint Endpoint slug. + * @param string $value Query param value. + * @param string $permalink Permalink. + * + * @return string + */ +function wc_get_endpoint_url( $endpoint, $value = '', $permalink = '' ) { + if ( ! $permalink ) { + $permalink = get_permalink(); + } + + // Map endpoint to options. + $query_vars = WC()->query->get_query_vars(); + $endpoint = ! empty( $query_vars[ $endpoint ] ) ? $query_vars[ $endpoint ] : $endpoint; + $value = ( get_option( 'woocommerce_myaccount_edit_address_endpoint', 'edit-address' ) === $endpoint ) ? wc_edit_address_i18n( $value ) : $value; + + if ( get_option( 'permalink_structure' ) ) { + if ( strstr( $permalink, '?' ) ) { + $query_string = '?' . wp_parse_url( $permalink, PHP_URL_QUERY ); + $permalink = current( explode( '?', $permalink ) ); + } else { + $query_string = ''; + } + $url = trailingslashit( $permalink ); + + if ( $value ) { + $url .= trailingslashit( $endpoint ) . user_trailingslashit( $value ); + } else { + $url .= user_trailingslashit( $endpoint ); + } + + $url .= $query_string; + } else { + $url = add_query_arg( $endpoint, $value, $permalink ); + } + + return apply_filters( 'woocommerce_get_endpoint_url', $url, $endpoint, $value, $permalink ); +} + +/** + * Hide menu items conditionally. + * + * @param array $items Navigation items. + * @return array + */ +function wc_nav_menu_items( $items ) { + if ( ! is_user_logged_in() ) { + $customer_logout = get_option( 'woocommerce_logout_endpoint', 'customer-logout' ); + + if ( ! empty( $customer_logout ) && ! empty( $items ) && is_array( $items ) ) { + foreach ( $items as $key => $item ) { + if ( empty( $item->url ) ) { + continue; + } + $path = wp_parse_url( $item->url, PHP_URL_PATH ); + $query = wp_parse_url( $item->url, PHP_URL_QUERY ); + + if ( strstr( $path, $customer_logout ) || strstr( $query, $customer_logout ) ) { + unset( $items[ $key ] ); + } + } + } + } + + return $items; +} +add_filter( 'wp_nav_menu_objects', 'wc_nav_menu_items', 10 ); + + +/** + * Fix active class in nav for shop page. + * + * @param array $menu_items Menu items. + * @return array + */ +function wc_nav_menu_item_classes( $menu_items ) { + if ( ! is_woocommerce() ) { + return $menu_items; + } + + $shop_page = wc_get_page_id( 'shop' ); + $page_for_posts = (int) get_option( 'page_for_posts' ); + + if ( ! empty( $menu_items ) && is_array( $menu_items ) ) { + foreach ( $menu_items as $key => $menu_item ) { + $classes = (array) $menu_item->classes; + $menu_id = (int) $menu_item->object_id; + + // Unset active class for blog page. + if ( $page_for_posts === $menu_id ) { + $menu_items[ $key ]->current = false; + + if ( in_array( 'current_page_parent', $classes, true ) ) { + unset( $classes[ array_search( 'current_page_parent', $classes, true ) ] ); + } + + if ( in_array( 'current-menu-item', $classes, true ) ) { + unset( $classes[ array_search( 'current-menu-item', $classes, true ) ] ); + } + } elseif ( is_shop() && $shop_page === $menu_id && 'page' === $menu_item->object ) { + // Set active state if this is the shop page link. + $menu_items[ $key ]->current = true; + $classes[] = 'current-menu-item'; + $classes[] = 'current_page_item'; + + } elseif ( is_singular( 'product' ) && $shop_page === $menu_id ) { + // Set parent state if this is a product page. + $classes[] = 'current_page_parent'; + } + + $menu_items[ $key ]->classes = array_unique( $classes ); + } + } + + return $menu_items; +} +add_filter( 'wp_nav_menu_objects', 'wc_nav_menu_item_classes', 2 ); + + +/** + * Fix active class in wp_list_pages for shop page. + * + * See details in https://github.com/woocommerce/woocommerce/issues/177. + * + * @param string $pages Pages list. + * @return string + */ +function wc_list_pages( $pages ) { + if ( ! is_woocommerce() ) { + return $pages; + } + + // Remove current_page_parent class from any item. + $pages = str_replace( 'current_page_parent', '', $pages ); + // Find shop_page_id through woocommerce options. + $shop_page = 'page-item-' . wc_get_page_id( 'shop' ); + + if ( is_shop() ) { + // Add current_page_item class to shop page. + return str_replace( $shop_page, $shop_page . ' current_page_item', $pages ); + } + + // Add current_page_parent class to shop page. + return str_replace( $shop_page, $shop_page . ' current_page_parent', $pages ); +} +add_filter( 'wp_list_pages', 'wc_list_pages' ); diff --git a/includes/wc-product-functions.php b/includes/wc-product-functions.php new file mode 100644 index 0000000..49bd021 --- /dev/null +++ b/includes/wc-product-functions.php @@ -0,0 +1,1645 @@ + 'limit', + 'post_status' => 'status', + 'post_parent' => 'parent', + 'posts_per_page' => 'limit', + 'paged' => 'page', + ); + + foreach ( $map_legacy as $from => $to ) { + if ( isset( $args[ $from ] ) ) { + $args[ $to ] = $args[ $from ]; + } + } + + $query = new WC_Product_Query( $args ); + return $query->get_products(); +} + +/** + * Main function for returning products, uses the WC_Product_Factory class. + * + * This function should only be called after 'init' action is finished, as there might be taxonomies that are getting + * registered during the init action. + * + * @since 2.2.0 + * + * @param mixed $the_product Post object or post ID of the product. + * @param array $deprecated Previously used to pass arguments to the factory, e.g. to force a type. + * @return WC_Product|null|false + */ +function wc_get_product( $the_product = false, $deprecated = array() ) { + if ( ! did_action( 'woocommerce_init' ) || ! did_action( 'woocommerce_after_register_taxonomy' ) || ! did_action( 'woocommerce_after_register_post_type' ) ) { + /* translators: 1: wc_get_product 2: woocommerce_init 3: woocommerce_after_register_taxonomy 4: woocommerce_after_register_post_type */ + wc_doing_it_wrong( __FUNCTION__, sprintf( __( '%1$s should not be called before the %2$s, %3$s and %4$s actions have finished.', 'woocommerce' ), 'wc_get_product', 'woocommerce_init', 'woocommerce_after_register_taxonomy', 'woocommerce_after_register_post_type' ), '3.9' ); + return false; + } + if ( ! empty( $deprecated ) ) { + wc_deprecated_argument( 'args', '3.0', 'Passing args to wc_get_product is deprecated. If you need to force a type, construct the product class directly.' ); + } + return WC()->product_factory->get_product( $the_product, $deprecated ); +} + +/** + * Get a product object. + * + * @see WC_Product_Factory::get_product_classname + * @since 3.9.0 + * @param string $product_type Product type. If used an invalid type a WC_Product_Simple instance will be returned. + * @param int $product_id Product ID. + * @return WC_Product + */ +function wc_get_product_object( $product_type, $product_id = 0 ) { + $classname = WC_Product_Factory::get_product_classname( $product_id, $product_type ); + + return new $classname( $product_id ); +} + +/** + * Returns whether or not SKUS are enabled. + * + * @return bool + */ +function wc_product_sku_enabled() { + return apply_filters( 'wc_product_sku_enabled', true ); +} + +/** + * Returns whether or not product weights are enabled. + * + * @return bool + */ +function wc_product_weight_enabled() { + return apply_filters( 'wc_product_weight_enabled', true ); +} + +/** + * Returns whether or not product dimensions (HxWxD) are enabled. + * + * @return bool + */ +function wc_product_dimensions_enabled() { + return apply_filters( 'wc_product_dimensions_enabled', true ); +} + +/** + * Clear transient cache for product data. + * + * @param int $post_id (default: 0) The product ID. + */ +function wc_delete_product_transients( $post_id = 0 ) { + // Transient data to clear with a fixed name which may be stale after product updates. + $transients_to_clear = array( + 'wc_products_onsale', + 'wc_featured_products', + 'wc_outofstock_count', + 'wc_low_stock_count', + ); + + foreach ( $transients_to_clear as $transient ) { + delete_transient( $transient ); + } + + if ( $post_id > 0 ) { + // Transient names that include an ID - since they are dynamic they cannot be cleaned in bulk without the ID. + $post_transient_names = array( + 'wc_product_children_', + 'wc_var_prices_', + 'wc_related_', + 'wc_child_has_weight_', + 'wc_child_has_dimensions_', + ); + + foreach ( $post_transient_names as $transient ) { + delete_transient( $transient . $post_id ); + } + } + + // Increments the transient version to invalidate cache. + WC_Cache_Helper::get_transient_version( 'product', true ); + + do_action( 'woocommerce_delete_product_transients', $post_id ); +} + +/** + * Function that returns an array containing the IDs of the products that are on sale. + * + * @since 2.0 + * @return array + */ +function wc_get_product_ids_on_sale() { + // Load from cache. + $product_ids_on_sale = get_transient( 'wc_products_onsale' ); + + // Valid cache found. + if ( false !== $product_ids_on_sale ) { + return $product_ids_on_sale; + } + + $data_store = WC_Data_Store::load( 'product' ); + $on_sale_products = $data_store->get_on_sale_products(); + $product_ids_on_sale = wp_parse_id_list( array_merge( wp_list_pluck( $on_sale_products, 'id' ), array_diff( wp_list_pluck( $on_sale_products, 'parent_id' ), array( 0 ) ) ) ); + + set_transient( 'wc_products_onsale', $product_ids_on_sale, DAY_IN_SECONDS * 30 ); + + return $product_ids_on_sale; +} + +/** + * Function that returns an array containing the IDs of the featured products. + * + * @since 2.1 + * @return array + */ +function wc_get_featured_product_ids() { + // Load from cache. + $featured_product_ids = get_transient( 'wc_featured_products' ); + + // Valid cache found. + if ( false !== $featured_product_ids ) { + return $featured_product_ids; + } + + $data_store = WC_Data_Store::load( 'product' ); + $featured = $data_store->get_featured_product_ids(); + $product_ids = array_keys( $featured ); + $parent_ids = array_values( array_filter( $featured ) ); + $featured_product_ids = array_unique( array_merge( $product_ids, $parent_ids ) ); + + set_transient( 'wc_featured_products', $featured_product_ids, DAY_IN_SECONDS * 30 ); + + return $featured_product_ids; +} + +/** + * Filter to allow product_cat in the permalinks for products. + * + * @param string $permalink The existing permalink URL. + * @param WP_Post $post WP_Post object. + * @return string + */ +function wc_product_post_type_link( $permalink, $post ) { + // Abort if post is not a product. + if ( 'product' !== $post->post_type ) { + return $permalink; + } + + // Abort early if the placeholder rewrite tag isn't in the generated URL. + if ( false === strpos( $permalink, '%' ) ) { + return $permalink; + } + + // Get the custom taxonomy terms in use by this post. + $terms = get_the_terms( $post->ID, 'product_cat' ); + + if ( ! empty( $terms ) ) { + $terms = wp_list_sort( + $terms, + array( + 'parent' => 'DESC', + 'term_id' => 'ASC', + ) + ); + $category_object = apply_filters( 'wc_product_post_type_link_product_cat', $terms[0], $terms, $post ); + $product_cat = $category_object->slug; + + if ( $category_object->parent ) { + $ancestors = get_ancestors( $category_object->term_id, 'product_cat' ); + foreach ( $ancestors as $ancestor ) { + $ancestor_object = get_term( $ancestor, 'product_cat' ); + if ( apply_filters( 'woocommerce_product_post_type_link_parent_category_only', false ) ) { + $product_cat = $ancestor_object->slug; + } else { + $product_cat = $ancestor_object->slug . '/' . $product_cat; + } + } + } + } else { + // If no terms are assigned to this post, use a string instead (can't leave the placeholder there). + $product_cat = _x( 'uncategorized', 'slug', 'woocommerce' ); + } + + $find = array( + '%year%', + '%monthnum%', + '%day%', + '%hour%', + '%minute%', + '%second%', + '%post_id%', + '%category%', + '%product_cat%', + ); + + $replace = array( + date_i18n( 'Y', strtotime( $post->post_date ) ), + date_i18n( 'm', strtotime( $post->post_date ) ), + date_i18n( 'd', strtotime( $post->post_date ) ), + date_i18n( 'H', strtotime( $post->post_date ) ), + date_i18n( 'i', strtotime( $post->post_date ) ), + date_i18n( 's', strtotime( $post->post_date ) ), + $post->ID, + $product_cat, + $product_cat, + ); + + $permalink = str_replace( $find, $replace, $permalink ); + + return $permalink; +} +add_filter( 'post_type_link', 'wc_product_post_type_link', 10, 2 ); + +/** + * Get the placeholder image URL either from media, or use the fallback image. + * + * @param string $size Thumbnail size to use. + * @return string + */ +function wc_placeholder_img_src( $size = 'woocommerce_thumbnail' ) { + $src = WC()->plugin_url() . '/assets/images/placeholder.png'; + $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); + + if ( ! empty( $placeholder_image ) ) { + if ( is_numeric( $placeholder_image ) ) { + $image = wp_get_attachment_image_src( $placeholder_image, $size ); + + if ( ! empty( $image[0] ) ) { + $src = $image[0]; + } + } else { + $src = $placeholder_image; + } + } + + return apply_filters( 'woocommerce_placeholder_img_src', $src ); +} + +/** + * Get the placeholder image. + * + * Uses wp_get_attachment_image if using an attachment ID @since 3.6.0 to handle responsiveness. + * + * @param string $size Image size. + * @param string|array $attr Optional. Attributes for the image markup. Default empty. + * @return string + */ +function wc_placeholder_img( $size = 'woocommerce_thumbnail', $attr = '' ) { + $dimensions = wc_get_image_size( $size ); + $placeholder_image = get_option( 'woocommerce_placeholder_image', 0 ); + + $default_attr = array( + 'class' => 'woocommerce-placeholder wp-post-image', + 'alt' => __( 'Placeholder', 'woocommerce' ), + ); + + $attr = wp_parse_args( $attr, $default_attr ); + + if ( wp_attachment_is_image( $placeholder_image ) ) { + $image_html = wp_get_attachment_image( + $placeholder_image, + $size, + false, + $attr + ); + } else { + $image = wc_placeholder_img_src( $size ); + $hwstring = image_hwstring( $dimensions['width'], $dimensions['height'] ); + $attributes = array(); + + foreach ( $attr as $name => $value ) { + $attribute[] = esc_attr( $name ) . '="' . esc_attr( $value ) . '"'; + } + + $image_html = ''; + } + + return apply_filters( 'woocommerce_placeholder_img', $image_html, $size, $dimensions ); +} + +/** + * Variation Formatting. + * + * Gets a formatted version of variation data or item meta. + * + * @param array|WC_Product_Variation $variation Variation object. + * @param bool $flat Should this be a flat list or HTML list? (default: false). + * @param bool $include_names include attribute names/labels in the list. + * @param bool $skip_attributes_in_name Do not list attributes already part of the variation name. + * @return string + */ +function wc_get_formatted_variation( $variation, $flat = false, $include_names = true, $skip_attributes_in_name = false ) { + $return = ''; + + if ( is_a( $variation, 'WC_Product_Variation' ) ) { + $variation_attributes = $variation->get_attributes(); + $product = $variation; + $variation_name = $variation->get_name(); + } else { + $product = false; + $variation_name = ''; + // Remove attribute_ prefix from names. + $variation_attributes = array(); + if ( is_array( $variation ) ) { + foreach ( $variation as $key => $value ) { + $variation_attributes[ str_replace( 'attribute_', '', $key ) ] = $value; + } + } + } + + $list_type = $include_names ? 'dl' : 'ul'; + + if ( is_array( $variation_attributes ) ) { + + if ( ! $flat ) { + $return = '<' . $list_type . ' class="variation">'; + } + + $variation_list = array(); + + foreach ( $variation_attributes as $name => $value ) { + // If this is a term slug, get the term's nice name. + if ( taxonomy_exists( $name ) ) { + $term = get_term_by( 'slug', $value, $name ); + if ( ! is_wp_error( $term ) && ! empty( $term->name ) ) { + $value = $term->name; + } + } + + // Do not list attributes already part of the variation name. + if ( '' === $value || ( $skip_attributes_in_name && wc_is_attribute_in_product_name( $value, $variation_name ) ) ) { + continue; + } + + if ( $include_names ) { + if ( $flat ) { + $variation_list[] = wc_attribute_label( $name, $product ) . ': ' . rawurldecode( $value ); + } else { + $variation_list[] = '
    ' . wc_attribute_label( $name, $product ) . ':
    ' . rawurldecode( $value ) . '
    '; + } + } else { + if ( $flat ) { + $variation_list[] = rawurldecode( $value ); + } else { + $variation_list[] = '
  • ' . rawurldecode( $value ) . '
  • '; + } + } + } + + if ( $flat ) { + $return .= implode( ', ', $variation_list ); + } else { + $return .= implode( '', $variation_list ); + } + + if ( ! $flat ) { + $return .= ''; + } + } + return $return; +} + +/** + * Function which handles the start and end of scheduled sales via cron. + */ +function wc_scheduled_sales() { + $data_store = WC_Data_Store::load( 'product' ); + + // Sales which are due to start. + $product_ids = $data_store->get_starting_sales(); + if ( $product_ids ) { + do_action( 'wc_before_products_starting_sales', $product_ids ); + foreach ( $product_ids as $product_id ) { + $product = wc_get_product( $product_id ); + + if ( $product ) { + $sale_price = $product->get_sale_price(); + + if ( $sale_price ) { + $product->set_price( $sale_price ); + $product->set_date_on_sale_from( '' ); + } else { + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + } + + $product->save(); + } + } + do_action( 'wc_after_products_starting_sales', $product_ids ); + + WC_Cache_Helper::get_transient_version( 'product', true ); + delete_transient( 'wc_products_onsale' ); + } + + // Sales which are due to end. + $product_ids = $data_store->get_ending_sales(); + if ( $product_ids ) { + do_action( 'wc_before_products_ending_sales', $product_ids ); + foreach ( $product_ids as $product_id ) { + $product = wc_get_product( $product_id ); + + if ( $product ) { + $regular_price = $product->get_regular_price(); + $product->set_price( $regular_price ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->save(); + } + } + do_action( 'wc_after_products_ending_sales', $product_ids ); + + WC_Cache_Helper::get_transient_version( 'product', true ); + delete_transient( 'wc_products_onsale' ); + } +} +add_action( 'woocommerce_scheduled_sales', 'wc_scheduled_sales' ); + +/** + * Get attachment image attributes. + * + * @param array $attr Image attributes. + * @return array + */ +function wc_get_attachment_image_attributes( $attr ) { + /* + * If the user can manage woocommerce, allow them to + * see the image content. + */ + if ( current_user_can( 'manage_woocommerce' ) ) { + return $attr; + } + + /* + * If the user does not have the right capabilities, + * filter out the image source and replace with placeholder + * image. + */ + if ( isset( $attr['src'] ) && strstr( $attr['src'], 'woocommerce_uploads/' ) ) { + $attr['src'] = wc_placeholder_img_src(); + + if ( isset( $attr['srcset'] ) ) { + $attr['srcset'] = ''; + } + } + return $attr; +} +add_filter( 'wp_get_attachment_image_attributes', 'wc_get_attachment_image_attributes' ); + + +/** + * Prepare attachment for JavaScript. + * + * @param array $response JS version of a attachment post object. + * @return array + */ +function wc_prepare_attachment_for_js( $response ) { + /* + * If the user can manage woocommerce, allow them to + * see the image content. + */ + if ( current_user_can( 'manage_woocommerce' ) ) { + return $response; + } + + /* + * If the user does not have the right capabilities, + * filter out the image source and replace with placeholder + * image. + */ + if ( isset( $response['url'] ) && strstr( $response['url'], 'woocommerce_uploads/' ) ) { + $response['full']['url'] = wc_placeholder_img_src(); + if ( isset( $response['sizes'] ) ) { + foreach ( $response['sizes'] as $size => $value ) { + $response['sizes'][ $size ]['url'] = wc_placeholder_img_src(); + } + } + } + + return $response; +} +add_filter( 'wp_prepare_attachment_for_js', 'wc_prepare_attachment_for_js' ); + +/** + * Track product views. + */ +function wc_track_product_view() { + if ( ! is_singular( 'product' ) || ! is_active_widget( false, false, 'woocommerce_recently_viewed_products', true ) ) { + return; + } + + global $post; + + if ( empty( $_COOKIE['woocommerce_recently_viewed'] ) ) { // @codingStandardsIgnoreLine. + $viewed_products = array(); + } else { + $viewed_products = wp_parse_id_list( (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) ); // @codingStandardsIgnoreLine. + } + + // Unset if already in viewed products list. + $keys = array_flip( $viewed_products ); + + if ( isset( $keys[ $post->ID ] ) ) { + unset( $viewed_products[ $keys[ $post->ID ] ] ); + } + + $viewed_products[] = $post->ID; + + if ( count( $viewed_products ) > 15 ) { + array_shift( $viewed_products ); + } + + // Store for session only. + wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $viewed_products ) ); +} + +add_action( 'template_redirect', 'wc_track_product_view', 20 ); + +/** + * Get product types. + * + * @since 2.2 + * @return array + */ +function wc_get_product_types() { + return (array) apply_filters( + 'product_type_selector', + array( + 'simple' => __( 'Simple product', 'woocommerce' ), + 'grouped' => __( 'Grouped product', 'woocommerce' ), + 'external' => __( 'External/Affiliate product', 'woocommerce' ), + 'variable' => __( 'Variable product', 'woocommerce' ), + ) + ); +} + +/** + * Check if product sku is unique. + * + * @since 2.2 + * @param int $product_id Product ID. + * @param string $sku Product SKU. + * @return bool + */ +function wc_product_has_unique_sku( $product_id, $sku ) { + $data_store = WC_Data_Store::load( 'product' ); + $sku_found = $data_store->is_existing_sku( $product_id, $sku ); + + if ( apply_filters( 'wc_product_has_unique_sku', $sku_found, $product_id, $sku ) ) { + return false; + } + + return true; +} + +/** + * Force a unique SKU. + * + * @since 3.0.0 + * @param integer $product_id Product ID. + */ +function wc_product_force_unique_sku( $product_id ) { + $product = wc_get_product( $product_id ); + $current_sku = $product ? $product->get_sku( 'edit' ) : ''; + + if ( $current_sku ) { + try { + $new_sku = wc_product_generate_unique_sku( $product_id, $current_sku ); + + if ( $current_sku !== $new_sku ) { + $product->set_sku( $new_sku ); + $product->save(); + } + } catch ( Exception $e ) {} // @codingStandardsIgnoreLine. + } +} + +/** + * Recursively appends a suffix until a unique SKU is found. + * + * @since 3.0.0 + * @param integer $product_id Product ID. + * @param string $sku Product SKU. + * @param integer $index An optional index that can be added to the product SKU. + * @return string + */ +function wc_product_generate_unique_sku( $product_id, $sku, $index = 0 ) { + $generated_sku = 0 < $index ? $sku . '-' . $index : $sku; + + if ( ! wc_product_has_unique_sku( $product_id, $generated_sku ) ) { + $generated_sku = wc_product_generate_unique_sku( $product_id, $sku, ( $index + 1 ) ); + } + + return $generated_sku; +} + +/** + * Get product ID by SKU. + * + * @since 2.3.0 + * @param string $sku Product SKU. + * @return int + */ +function wc_get_product_id_by_sku( $sku ) { + $data_store = WC_Data_Store::load( 'product' ); + return $data_store->get_product_id_by_sku( $sku ); +} + +/** + * Get attributes/data for an individual variation from the database and maintain it's integrity. + * + * @since 2.4.0 + * @param int $variation_id Variation ID. + * @return array + */ +function wc_get_product_variation_attributes( $variation_id ) { + // Build variation data from meta. + $all_meta = get_post_meta( $variation_id ); + $parent_id = wp_get_post_parent_id( $variation_id ); + $parent_attributes = array_filter( (array) get_post_meta( $parent_id, '_product_attributes', true ) ); + $found_parent_attributes = array(); + $variation_attributes = array(); + + // Compare to parent variable product attributes and ensure they match. + foreach ( $parent_attributes as $attribute_name => $options ) { + if ( ! empty( $options['is_variation'] ) ) { + $attribute = 'attribute_' . sanitize_title( $attribute_name ); + $found_parent_attributes[] = $attribute; + if ( ! array_key_exists( $attribute, $variation_attributes ) ) { + $variation_attributes[ $attribute ] = ''; // Add it - 'any' will be asumed. + } + } + } + + // Get the variation attributes from meta. + foreach ( $all_meta as $name => $value ) { + // Only look at valid attribute meta, and also compare variation level attributes and remove any which do not exist at parent level. + if ( 0 !== strpos( $name, 'attribute_' ) || ! in_array( $name, $found_parent_attributes, true ) ) { + unset( $variation_attributes[ $name ] ); + continue; + } + /** + * Pre 2.4 handling where 'slugs' were saved instead of the full text attribute. + * Attempt to get full version of the text attribute from the parent. + */ + if ( sanitize_title( $value[0] ) === $value[0] && version_compare( get_post_meta( $parent_id, '_product_version', true ), '2.4.0', '<' ) ) { + foreach ( $parent_attributes as $attribute ) { + if ( 'attribute_' . sanitize_title( $attribute['name'] ) !== $name ) { + continue; + } + $text_attributes = wc_get_text_attributes( $attribute['value'] ); + + foreach ( $text_attributes as $text_attribute ) { + if ( sanitize_title( $text_attribute ) === $value[0] ) { + $value[0] = $text_attribute; + break; + } + } + } + } + + $variation_attributes[ $name ] = $value[0]; + } + + return $variation_attributes; +} + +/** + * Get all product cats for a product by ID, including hierarchy + * + * @since 2.5.0 + * @param int $product_id Product ID. + * @return array + */ +function wc_get_product_cat_ids( $product_id ) { + $product_cats = wc_get_product_term_ids( $product_id, 'product_cat' ); + + foreach ( $product_cats as $product_cat ) { + $product_cats = array_merge( $product_cats, get_ancestors( $product_cat, 'product_cat' ) ); + } + + return $product_cats; +} + +/** + * Gets data about an attachment, such as alt text and captions. + * + * @since 2.6.0 + * + * @param int|null $attachment_id Attachment ID. + * @param WC_Product|bool $product WC_Product object. + * + * @return array + */ +function wc_get_product_attachment_props( $attachment_id = null, $product = false ) { + $props = array( + 'title' => '', + 'caption' => '', + 'url' => '', + 'alt' => '', + 'src' => '', + 'srcset' => false, + 'sizes' => false, + ); + $attachment = get_post( $attachment_id ); + + if ( $attachment && 'attachment' === $attachment->post_type ) { + $props['title'] = wp_strip_all_tags( $attachment->post_title ); + $props['caption'] = wp_strip_all_tags( $attachment->post_excerpt ); + $props['url'] = wp_get_attachment_url( $attachment_id ); + + // Alt text. + $alt_text = array( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ), $props['caption'], wp_strip_all_tags( $attachment->post_title ) ); + + if ( $product && $product instanceof WC_Product ) { + $alt_text[] = wp_strip_all_tags( get_the_title( $product->get_id() ) ); + } + + $alt_text = array_filter( $alt_text ); + $props['alt'] = isset( $alt_text[0] ) ? $alt_text[0] : ''; + + // Large version. + $full_size = apply_filters( 'woocommerce_gallery_full_size', apply_filters( 'woocommerce_product_thumbnails_large_size', 'full' ) ); + $src = wp_get_attachment_image_src( $attachment_id, $full_size ); + $props['full_src'] = $src[0]; + $props['full_src_w'] = $src[1]; + $props['full_src_h'] = $src[2]; + + // Gallery thumbnail. + $gallery_thumbnail = wc_get_image_size( 'gallery_thumbnail' ); + $gallery_thumbnail_size = apply_filters( 'woocommerce_gallery_thumbnail_size', array( $gallery_thumbnail['width'], $gallery_thumbnail['height'] ) ); + $src = wp_get_attachment_image_src( $attachment_id, $gallery_thumbnail_size ); + $props['gallery_thumbnail_src'] = $src[0]; + $props['gallery_thumbnail_src_w'] = $src[1]; + $props['gallery_thumbnail_src_h'] = $src[2]; + + // Thumbnail version. + $thumbnail_size = apply_filters( 'woocommerce_thumbnail_size', 'woocommerce_thumbnail' ); + $src = wp_get_attachment_image_src( $attachment_id, $thumbnail_size ); + $props['thumb_src'] = $src[0]; + $props['thumb_src_w'] = $src[1]; + $props['thumb_src_h'] = $src[2]; + + // Image source. + $image_size = apply_filters( 'woocommerce_gallery_image_size', 'woocommerce_single' ); + $src = wp_get_attachment_image_src( $attachment_id, $image_size ); + $props['src'] = $src[0]; + $props['src_w'] = $src[1]; + $props['src_h'] = $src[2]; + $props['srcset'] = function_exists( 'wp_get_attachment_image_srcset' ) ? wp_get_attachment_image_srcset( $attachment_id, $image_size ) : false; + $props['sizes'] = function_exists( 'wp_get_attachment_image_sizes' ) ? wp_get_attachment_image_sizes( $attachment_id, $image_size ) : false; + } + return $props; +} + +/** + * Get product visibility options. + * + * @since 3.0.0 + * @return array + */ +function wc_get_product_visibility_options() { + return apply_filters( + 'woocommerce_product_visibility_options', + array( + 'visible' => __( 'Shop and search results', 'woocommerce' ), + 'catalog' => __( 'Shop only', 'woocommerce' ), + 'search' => __( 'Search results only', 'woocommerce' ), + 'hidden' => __( 'Hidden', 'woocommerce' ), + ) + ); +} + +/** + * Get product tax class options. + * + * @since 3.0.0 + * @return array + */ +function wc_get_product_tax_class_options() { + $tax_classes = WC_Tax::get_tax_classes(); + $tax_class_options = array(); + $tax_class_options[''] = __( 'Standard', 'woocommerce' ); + + if ( ! empty( $tax_classes ) ) { + foreach ( $tax_classes as $class ) { + $tax_class_options[ sanitize_title( $class ) ] = $class; + } + } + return $tax_class_options; +} + +/** + * Get stock status options. + * + * @since 3.0.0 + * @return array + */ +function wc_get_product_stock_status_options() { + return apply_filters( + 'woocommerce_product_stock_status_options', + array( + 'instock' => __( 'In stock', 'woocommerce' ), + 'outofstock' => __( 'Out of stock', 'woocommerce' ), + 'onbackorder' => __( 'On backorder', 'woocommerce' ), + ) + ); +} + +/** + * Get backorder options. + * + * @since 3.0.0 + * @return array + */ +function wc_get_product_backorder_options() { + return array( + 'no' => __( 'Do not allow', 'woocommerce' ), + 'notify' => __( 'Allow, but notify customer', 'woocommerce' ), + 'yes' => __( 'Allow', 'woocommerce' ), + ); +} + +/** + * Get related products based on product category and tags. + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @param int $limit Limit of results. + * @param array $exclude_ids Exclude IDs from the results. + * @return array + */ +function wc_get_related_products( $product_id, $limit = 5, $exclude_ids = array() ) { + + $product_id = absint( $product_id ); + $limit = $limit >= -1 ? $limit : 5; + $exclude_ids = array_merge( array( 0, $product_id ), $exclude_ids ); + $transient_name = 'wc_related_' . $product_id; + $query_args = http_build_query( + array( + 'limit' => $limit, + 'exclude_ids' => $exclude_ids, + ) + ); + + $transient = get_transient( $transient_name ); + $related_posts = $transient && isset( $transient[ $query_args ] ) ? $transient[ $query_args ] : false; + + // We want to query related posts if they are not cached, or we don't have enough. + if ( false === $related_posts || count( $related_posts ) < $limit ) { + + $cats_array = apply_filters( 'woocommerce_product_related_posts_relate_by_category', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_cat_terms', wc_get_product_term_ids( $product_id, 'product_cat' ), $product_id ) : array(); + $tags_array = apply_filters( 'woocommerce_product_related_posts_relate_by_tag', true, $product_id ) ? apply_filters( 'woocommerce_get_related_product_tag_terms', wc_get_product_term_ids( $product_id, 'product_tag' ), $product_id ) : array(); + + // Don't bother if none are set, unless woocommerce_product_related_posts_force_display is set to true in which case all products are related. + if ( empty( $cats_array ) && empty( $tags_array ) && ! apply_filters( 'woocommerce_product_related_posts_force_display', false, $product_id ) ) { + $related_posts = array(); + } else { + $data_store = WC_Data_Store::load( 'product' ); + $related_posts = $data_store->get_related_products( $cats_array, $tags_array, $exclude_ids, $limit + 10, $product_id ); + } + + if ( $transient ) { + $transient[ $query_args ] = $related_posts; + } else { + $transient = array( $query_args => $related_posts ); + } + + set_transient( $transient_name, $transient, DAY_IN_SECONDS ); + } + + $related_posts = apply_filters( + 'woocommerce_related_products', + $related_posts, + $product_id, + array( + 'limit' => $limit, + 'excluded_ids' => $exclude_ids, + ) + ); + + if ( apply_filters( 'woocommerce_product_related_posts_shuffle', true ) ) { + shuffle( $related_posts ); + } + + return array_slice( $related_posts, 0, $limit ); +} + +/** + * Retrieves product term ids for a taxonomy. + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @param string $taxonomy Taxonomy slug. + * @return array + */ +function wc_get_product_term_ids( $product_id, $taxonomy ) { + $terms = get_the_terms( $product_id, $taxonomy ); + return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' ); +} + +/** + * For a given product, and optionally price/qty, work out the price with tax included, based on store settings. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @param array $args Optional arguments to pass product quantity and price. + * @return float|string Price with tax included, or an empty string if price calculation failed. + */ +function wc_get_price_including_tax( $product, $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'qty' => '', + 'price' => '', + ) + ); + + $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); + $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; + + if ( '' === $price ) { + return ''; + } elseif ( empty( $qty ) ) { + return 0.0; + } + + $line_price = $price * $qty; + $return_price = $line_price; + + if ( $product->is_taxable() ) { + if ( ! wc_prices_include_tax() ) { + $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); + $taxes = WC_Tax::calc_tax( $line_price, $tax_rates, false ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $taxes_total = array_sum( $taxes ); + } else { + $taxes_total = array_sum( array_map( 'wc_round_tax_total', $taxes ) ); + } + + $return_price = NumberUtil::round( $line_price + $taxes_total, wc_get_price_decimals() ); + } else { + $tax_rates = WC_Tax::get_rates( $product->get_tax_class() ); + $base_tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); + + /** + * If the customer is excempt from VAT, remove the taxes here. + * Either remove the base or the user taxes depending on woocommerce_adjust_non_base_location_prices setting. + */ + if ( ! empty( WC()->customer ) && WC()->customer->get_is_vat_exempt() ) { // @codingStandardsIgnoreLine. + $remove_taxes = apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ? WC_Tax::calc_tax( $line_price, $base_tax_rates, true ) : WC_Tax::calc_tax( $line_price, $tax_rates, true ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $remove_taxes_total = array_sum( $remove_taxes ); + } else { + $remove_taxes_total = array_sum( array_map( 'wc_round_tax_total', $remove_taxes ) ); + } + + $return_price = NumberUtil::round( $line_price - $remove_taxes_total, wc_get_price_decimals() ); + + /** + * The woocommerce_adjust_non_base_location_prices filter can stop base taxes being taken off when dealing with out of base locations. + * e.g. If a product costs 10 including tax, all users will pay 10 regardless of location and taxes. + * This feature is experimental @since 2.4.7 and may change in the future. Use at your risk. + */ + } elseif ( $tax_rates !== $base_tax_rates && apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) ) { + $base_taxes = WC_Tax::calc_tax( $line_price, $base_tax_rates, true ); + $modded_taxes = WC_Tax::calc_tax( $line_price - array_sum( $base_taxes ), $tax_rates, false ); + + if ( 'yes' === get_option( 'woocommerce_tax_round_at_subtotal' ) ) { + $base_taxes_total = array_sum( $base_taxes ); + $modded_taxes_total = array_sum( $modded_taxes ); + } else { + $base_taxes_total = array_sum( array_map( 'wc_round_tax_total', $base_taxes ) ); + $modded_taxes_total = array_sum( array_map( 'wc_round_tax_total', $modded_taxes ) ); + } + + $return_price = NumberUtil::round( $line_price - $base_taxes_total + $modded_taxes_total, wc_get_price_decimals() ); + } + } + } + return apply_filters( 'woocommerce_get_price_including_tax', $return_price, $qty, $product ); +} + +/** + * For a given product, and optionally price/qty, work out the price with tax excluded, based on store settings. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @param array $args Optional arguments to pass product quantity and price. + * @return float|string Price with tax excluded, or an empty string if price calculation failed. + */ +function wc_get_price_excluding_tax( $product, $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'qty' => '', + 'price' => '', + ) + ); + + $price = '' !== $args['price'] ? max( 0.0, (float) $args['price'] ) : $product->get_price(); + $qty = '' !== $args['qty'] ? max( 0.0, (float) $args['qty'] ) : 1; + + if ( '' === $price ) { + return ''; + } elseif ( empty( $qty ) ) { + return 0.0; + } + + $line_price = $price * $qty; + + if ( $product->is_taxable() && wc_prices_include_tax() ) { + $order = ArrayUtil::get_value_or_default( $args, 'order' ); + $customer_id = $order ? $order->get_customer_id() : 0; + if ( apply_filters( 'woocommerce_adjust_non_base_location_prices', true ) || ! $customer_id ) { + $tax_rates = WC_Tax::get_base_tax_rates( $product->get_tax_class( 'unfiltered' ) ); + } else { + $customer = wc_get_container()->get( LegacyProxy::class )->get_instance_of( WC_Customer::class, $customer_id ); + $tax_rates = WC_Tax::get_rates( $product->get_tax_class(), $customer ); + } + $remove_taxes = WC_Tax::calc_tax( $line_price, $tax_rates, true ); + $return_price = $line_price - array_sum( $remove_taxes ); // Unrounded since we're dealing with tax inclusive prices. Matches logic in cart-totals class. @see adjust_non_base_location_price. + } else { + $return_price = $line_price; + } + + return apply_filters( 'woocommerce_get_price_excluding_tax', $return_price, $qty, $product ); +} + +/** + * Returns the price including or excluding tax, based on the 'woocommerce_tax_display_shop' setting. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @param array $args Optional arguments to pass product quantity and price. + * @return float + */ +function wc_get_price_to_display( $product, $args = array() ) { + $args = wp_parse_args( + $args, + array( + 'qty' => 1, + 'price' => $product->get_price(), + ) + ); + + $price = $args['price']; + $qty = $args['qty']; + + return 'incl' === get_option( 'woocommerce_tax_display_shop' ) ? + wc_get_price_including_tax( + $product, + array( + 'qty' => $qty, + 'price' => $price, + ) + ) : + wc_get_price_excluding_tax( + $product, + array( + 'qty' => $qty, + 'price' => $price, + ) + ); +} + +/** + * Returns the product categories in a list. + * + * @param int $product_id Product ID. + * @param string $sep (default: ', '). + * @param string $before (default: ''). + * @param string $after (default: ''). + * @return string + */ +function wc_get_product_category_list( $product_id, $sep = ', ', $before = '', $after = '' ) { + return get_the_term_list( $product_id, 'product_cat', $before, $sep, $after ); +} + +/** + * Returns the product tags in a list. + * + * @param int $product_id Product ID. + * @param string $sep (default: ', '). + * @param string $before (default: ''). + * @param string $after (default: ''). + * @return string + */ +function wc_get_product_tag_list( $product_id, $sep = ', ', $before = '', $after = '' ) { + return get_the_term_list( $product_id, 'product_tag', $before, $sep, $after ); +} + +/** + * Callback for array filter to get visible only. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @return bool + */ +function wc_products_array_filter_visible( $product ) { + return $product && is_a( $product, 'WC_Product' ) && $product->is_visible(); +} + +/** + * Callback for array filter to get visible grouped products only. + * + * @since 3.1.0 + * @param WC_Product $product WC_Product object. + * @return bool + */ +function wc_products_array_filter_visible_grouped( $product ) { + return $product && is_a( $product, 'WC_Product' ) && ( 'publish' === $product->get_status() || current_user_can( 'edit_product', $product->get_id() ) ); +} + +/** + * Callback for array filter to get products the user can edit only. + * + * @since 3.0.0 + * @param WC_Product $product WC_Product object. + * @return bool + */ +function wc_products_array_filter_editable( $product ) { + return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'edit_product', $product->get_id() ); +} + +/** + * Callback for array filter to get products the user can view only. + * + * @since 3.4.0 + * @param WC_Product $product WC_Product object. + * @return bool + */ +function wc_products_array_filter_readable( $product ) { + return $product && is_a( $product, 'WC_Product' ) && current_user_can( 'read_product', $product->get_id() ); +} + +/** + * Sort an array of products by a value. + * + * @since 3.0.0 + * + * @param array $products List of products to be ordered. + * @param string $orderby Optional order criteria. + * @param string $order Ascending or descending order. + * + * @return array + */ +function wc_products_array_orderby( $products, $orderby = 'date', $order = 'desc' ) { + $orderby = strtolower( $orderby ); + $order = strtolower( $order ); + switch ( $orderby ) { + case 'title': + case 'id': + case 'date': + case 'modified': + case 'menu_order': + case 'price': + usort( $products, 'wc_products_array_orderby_' . $orderby ); + break; + case 'none': + break; + default: + shuffle( $products ); + break; + } + if ( 'desc' === $order ) { + $products = array_reverse( $products ); + } + return $products; +} + +/** + * Sort by title. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_title( $a, $b ) { + return strcasecmp( $a->get_name(), $b->get_name() ); +} + +/** + * Sort by id. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_id( $a, $b ) { + if ( $a->get_id() === $b->get_id() ) { + return 0; + } + return ( $a->get_id() < $b->get_id() ) ? -1 : 1; +} + +/** + * Sort by date. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_date( $a, $b ) { + if ( $a->get_date_created() === $b->get_date_created() ) { + return 0; + } + return ( $a->get_date_created() < $b->get_date_created() ) ? -1 : 1; +} + +/** + * Sort by modified. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_modified( $a, $b ) { + if ( $a->get_date_modified() === $b->get_date_modified() ) { + return 0; + } + return ( $a->get_date_modified() < $b->get_date_modified() ) ? -1 : 1; +} + +/** + * Sort by menu order. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_menu_order( $a, $b ) { + if ( $a->get_menu_order() === $b->get_menu_order() ) { + return 0; + } + return ( $a->get_menu_order() < $b->get_menu_order() ) ? -1 : 1; +} + +/** + * Sort by price low to high. + * + * @since 3.0.0 + * @param WC_Product $a First WC_Product object. + * @param WC_Product $b Second WC_Product object. + * @return int + */ +function wc_products_array_orderby_price( $a, $b ) { + if ( $a->get_price() === $b->get_price() ) { + return 0; + } + return ( $a->get_price() < $b->get_price() ) ? -1 : 1; +} + +/** + * Queue a product for syncing at the end of the request. + * + * @param int $product_id Product ID. + */ +function wc_deferred_product_sync( $product_id ) { + global $wc_deferred_product_sync; + + if ( empty( $wc_deferred_product_sync ) ) { + $wc_deferred_product_sync = array(); + } + + $wc_deferred_product_sync[] = $product_id; +} + +/** + * See if the lookup table is being generated already. + * + * @since 3.6.0 + * @return bool + */ +function wc_update_product_lookup_tables_is_running() { + $table_updates_pending = WC()->queue()->search( + array( + 'status' => 'pending', + 'group' => 'wc_update_product_lookup_tables', + 'per_page' => 1, + ) + ); + + return (bool) count( $table_updates_pending ); +} + +/** + * Populate lookup table data for products. + * + * @since 3.6.0 + */ +function wc_update_product_lookup_tables() { + global $wpdb; + + $is_cli = Constants::is_true( 'WP_CLI' ); + + if ( ! $is_cli ) { + WC_Admin_Notices::add_notice( 'regenerating_lookup_table' ); + } + + // Note that the table is not yet generated. + update_option( 'woocommerce_product_lookup_table_is_generating', true ); + + // Make a row per product in lookup table. + $wpdb->query( + " + INSERT IGNORE INTO {$wpdb->wc_product_meta_lookup} (`product_id`) + SELECT + posts.ID + FROM {$wpdb->posts} posts + WHERE + posts.post_type IN ('product', 'product_variation') + " + ); + + // List of column names in the lookup table we need to populate. + $columns = array( + 'min_max_price', + 'stock_quantity', + 'sku', + 'stock_status', + 'average_rating', + 'total_sales', + 'downloadable', + 'virtual', + 'onsale', + 'tax_class', + 'tax_status', // When last column is updated, woocommerce_product_lookup_table_is_generating is updated. + ); + + foreach ( $columns as $index => $column ) { + if ( $is_cli ) { + wc_update_product_lookup_tables_column( $column ); + } else { + WC()->queue()->schedule_single( + time() + $index, + 'wc_update_product_lookup_tables_column', + array( + 'column' => $column, + ), + 'wc_update_product_lookup_tables' + ); + } + } + + // Rating counts are serialised so they have to be unserialised before populating the lookup table. + if ( $is_cli ) { + $rating_count_rows = $wpdb->get_results( + " + SELECT post_id, meta_value FROM {$wpdb->postmeta} + WHERE meta_key = '_wc_rating_count' + AND meta_value != '' + AND meta_value != 'a:0:{}' + ", + ARRAY_A + ); + wc_update_product_lookup_tables_rating_count( $rating_count_rows ); + } else { + WC()->queue()->schedule_single( + time() + 10, + 'wc_update_product_lookup_tables_rating_count_batch', + array( + 'offset' => 0, + 'limit' => 50, + ), + 'wc_update_product_lookup_tables' + ); + } +} + +/** + * Populate lookup table column data. + * + * @since 3.6.0 + * @param string $column Column name to set. + */ +function wc_update_product_lookup_tables_column( $column ) { + if ( empty( $column ) ) { + return; + } + global $wpdb; + switch ( $column ) { + case 'min_max_price': + $wpdb->query( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + INNER JOIN ( + SELECT lookup_table.product_id, MIN( meta_value+0 ) as min_price, MAX( meta_value+0 ) as max_price + FROM {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' + WHERE + meta1.meta_value <> '' + GROUP BY lookup_table.product_id + ) as source on source.product_id = lookup_table.product_id + SET + lookup_table.min_price = source.min_price, + lookup_table.max_price = source.max_price + " + ); + break; + case 'stock_quantity': + $wpdb->query( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_manage_stock' + LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_stock' + SET + lookup_table.stock_quantity = meta2.meta_value + WHERE + meta1.meta_value = 'yes' + " + ); + break; + case 'sku': + case 'stock_status': + case 'average_rating': + case 'total_sales': + case 'tax_class': + case 'tax_status': + if ( 'total_sales' === $column ) { + $meta_key = 'total_sales'; + } elseif ( 'average_rating' === $column ) { + $meta_key = '_wc_average_rating'; + } else { + $meta_key = '_' . $column; + } + $column = esc_sql( $column ); + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( + $wpdb->prepare( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta ON lookup_table.product_id = meta.post_id AND meta.meta_key = %s + SET + lookup_table.`{$column}` = meta.meta_value + ", + $meta_key + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + break; + case 'downloadable': + case 'virtual': + $column = esc_sql( $column ); + $meta_key = '_' . $column; + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( + $wpdb->prepare( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = %s + SET + lookup_table.`{$column}` = IF ( meta1.meta_value = 'yes', 1, 0 ) + ", + $meta_key + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + break; + case 'onsale': + $column = esc_sql( $column ); + $decimals = absint( wc_get_price_decimals() ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $wpdb->query( + $wpdb->prepare( + " + UPDATE + {$wpdb->wc_product_meta_lookup} lookup_table + LEFT JOIN {$wpdb->postmeta} meta1 ON lookup_table.product_id = meta1.post_id AND meta1.meta_key = '_price' + LEFT JOIN {$wpdb->postmeta} meta2 ON lookup_table.product_id = meta2.post_id AND meta2.meta_key = '_sale_price' + SET + lookup_table.`{$column}` = IF ( + CAST( meta1.meta_value AS DECIMAL ) >= 0 + AND CAST( meta2.meta_value AS CHAR ) != '' + AND CAST( meta1.meta_value AS DECIMAL( 10, %d ) ) = CAST( meta2.meta_value AS DECIMAL( 10, %d ) ) + , 1, 0 ) + ", + $decimals, + $decimals + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + break; + } + + // Final column - mark complete. + if ( 'tax_status' === $column ) { + delete_option( 'woocommerce_product_lookup_table_is_generating' ); + } +} +add_action( 'wc_update_product_lookup_tables_column', 'wc_update_product_lookup_tables_column' ); + +/** + * Populate rating count lookup table data for products. + * + * @since 3.6.0 + * @param array $rows Rows of rating counts to update in lookup table. + */ +function wc_update_product_lookup_tables_rating_count( $rows ) { + if ( ! $rows || ! is_array( $rows ) ) { + return; + } + global $wpdb; + + foreach ( $rows as $row ) { + $count = array_sum( (array) maybe_unserialize( $row['meta_value'] ) ); + $wpdb->update( + $wpdb->wc_product_meta_lookup, + array( + 'rating_count' => absint( $count ), + ), + array( + 'product_id' => absint( $row['post_id'] ), + ) + ); + } +} + +/** + * Populate a batch of rating count lookup table data for products. + * + * @since 3.6.2 + * @param array $offset Offset to query. + * @param array $limit Limit to query. + */ +function wc_update_product_lookup_tables_rating_count_batch( $offset = 0, $limit = 0 ) { + global $wpdb; + + if ( ! $limit ) { + return; + } + + $rating_count_rows = $wpdb->get_results( + $wpdb->prepare( + " + SELECT post_id, meta_value FROM {$wpdb->postmeta} + WHERE meta_key = '_wc_rating_count' + AND meta_value != '' + AND meta_value != 'a:0:{}' + ORDER BY post_id ASC + LIMIT %d, %d + ", + $offset, + $limit + ), + ARRAY_A + ); + + if ( $rating_count_rows ) { + wc_update_product_lookup_tables_rating_count( $rating_count_rows ); + WC()->queue()->schedule_single( + time() + 1, + 'wc_update_product_lookup_tables_rating_count_batch', + array( + 'offset' => $offset + $limit, + 'limit' => $limit, + ), + 'wc_update_product_lookup_tables' + ); + } +} +add_action( 'wc_update_product_lookup_tables_rating_count_batch', 'wc_update_product_lookup_tables_rating_count_batch', 10, 2 ); diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php new file mode 100644 index 0000000..54029b4 --- /dev/null +++ b/includes/wc-rest-functions.php @@ -0,0 +1,356 @@ +setTimezone( new DateTimeZone( wc_timezone_string() ) ); + } elseif ( is_string( $date ) ) { + $date = new WC_DateTime( $date, new DateTimeZone( 'UTC' ) ); + $date->setTimezone( new DateTimeZone( wc_timezone_string() ) ); + } + + if ( ! is_a( $date, 'WC_DateTime' ) ) { + return null; + } + + // Get timestamp before changing timezone to UTC. + return gmdate( 'Y-m-d\TH:i:s', $utc ? $date->getTimestamp() : $date->getOffsetTimestamp() ); +} + +/** + * Returns image mime types users are allowed to upload via the API. + * + * @since 2.6.4 + * @return array + */ +function wc_rest_allowed_image_mime_types() { + return apply_filters( + 'woocommerce_rest_allowed_image_mime_types', + array( + 'jpg|jpeg|jpe' => 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + 'bmp' => 'image/bmp', + 'tiff|tif' => 'image/tiff', + 'ico' => 'image/x-icon', + ) + ); +} + +/** + * Upload image from URL. + * + * @since 2.6.0 + * @param string $image_url Image URL. + * @return array|WP_Error Attachment data or error message. + */ +function wc_rest_upload_image_from_url( $image_url ) { + $parsed_url = wp_parse_url( $image_url ); + + // Check parsed URL. + if ( ! $parsed_url || ! is_array( $parsed_url ) ) { + /* translators: %s: image URL */ + return new WP_Error( 'woocommerce_rest_invalid_image_url', sprintf( __( 'Invalid URL %s.', 'woocommerce' ), $image_url ), array( 'status' => 400 ) ); + } + + // Ensure url is valid. + $image_url = esc_url_raw( $image_url ); + + // download_url function is part of wp-admin. + if ( ! function_exists( 'download_url' ) ) { + include_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $file_array = array(); + $file_array['name'] = basename( current( explode( '?', $image_url ) ) ); + + // Download file to temp location. + $file_array['tmp_name'] = download_url( $image_url ); + + // If error storing temporarily, return the error. + if ( is_wp_error( $file_array['tmp_name'] ) ) { + return new WP_Error( + 'woocommerce_rest_invalid_remote_image_url', + /* translators: %s: image URL */ + sprintf( __( 'Error getting remote image %s.', 'woocommerce' ), $image_url ) . ' ' + /* translators: %s: error message */ + . sprintf( __( 'Error: %s', 'woocommerce' ), $file_array['tmp_name']->get_error_message() ), + array( 'status' => 400 ) + ); + } + + // Do the validation and storage stuff. + $file = wp_handle_sideload( + $file_array, + array( + 'test_form' => false, + 'mimes' => wc_rest_allowed_image_mime_types(), + ), + current_time( 'Y/m' ) + ); + + if ( isset( $file['error'] ) ) { + @unlink( $file_array['tmp_name'] ); // @codingStandardsIgnoreLine. + + /* translators: %s: error message */ + return new WP_Error( 'woocommerce_rest_invalid_image', sprintf( __( 'Invalid image: %s', 'woocommerce' ), $file['error'] ), array( 'status' => 400 ) ); + } + + do_action( 'woocommerce_rest_api_uploaded_image_from_url', $file, $image_url ); + + return $file; +} + +/** + * Set uploaded image as attachment. + * + * @since 2.6.0 + * @param array $upload Upload information from wp_upload_bits. + * @param int $id Post ID. Default to 0. + * @return int Attachment ID + */ +function wc_rest_set_uploaded_image_as_attachment( $upload, $id = 0 ) { + $info = wp_check_filetype( $upload['file'] ); + $title = ''; + $content = ''; + + if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) { + include_once ABSPATH . 'wp-admin/includes/image.php'; + } + + $image_meta = wp_read_image_metadata( $upload['file'] ); + if ( $image_meta ) { + if ( trim( $image_meta['title'] ) && ! is_numeric( sanitize_title( $image_meta['title'] ) ) ) { + $title = wc_clean( $image_meta['title'] ); + } + if ( trim( $image_meta['caption'] ) ) { + $content = wc_clean( $image_meta['caption'] ); + } + } + + $attachment = array( + 'post_mime_type' => $info['type'], + 'guid' => $upload['url'], + 'post_parent' => $id, + 'post_title' => $title ? $title : basename( $upload['file'] ), + 'post_content' => $content, + ); + + $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $id ); + if ( ! is_wp_error( $attachment_id ) ) { + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) ); + } + + return $attachment_id; +} + +/** + * Validate reports request arguments. + * + * @since 2.6.0 + * @param mixed $value Value to valdate. + * @param WP_REST_Request $request Request instance. + * @param string $param Param to validate. + * @return WP_Error|boolean + */ +function wc_rest_validate_reports_request_arg( $value, $request, $param ) { + + $attributes = $request->get_attributes(); + if ( ! isset( $attributes['args'][ $param ] ) || ! is_array( $attributes['args'][ $param ] ) ) { + return true; + } + $args = $attributes['args'][ $param ]; + + if ( 'string' === $args['type'] && ! is_string( $value ) ) { + /* translators: 1: param 2: type */ + return new WP_Error( 'woocommerce_rest_invalid_param', sprintf( __( '%1$s is not of type %2$s', 'woocommerce' ), $param, 'string' ) ); + } + + if ( 'date' === $args['format'] ) { + $regex = '#^\d{4}-\d{2}-\d{2}$#'; + + if ( ! preg_match( $regex, $value, $matches ) ) { + return new WP_Error( 'woocommerce_rest_invalid_date', __( 'The date you provided is invalid.', 'woocommerce' ) ); + } + } + + return true; +} + +/** + * Encodes a value according to RFC 3986. + * Supports multidimensional arrays. + * + * @since 2.6.0 + * @param string|array $value The value to encode. + * @return string|array Encoded values. + */ +function wc_rest_urlencode_rfc3986( $value ) { + if ( is_array( $value ) ) { + return array_map( 'wc_rest_urlencode_rfc3986', $value ); + } + + return str_replace( array( '+', '%7E' ), array( ' ', '~' ), rawurlencode( $value ) ); +} + +/** + * Check permissions of posts on REST API. + * + * @since 2.6.0 + * @param string $post_type Post type. + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_post_permissions( $post_type, $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'read_private_posts', + 'create' => 'publish_posts', + 'edit' => 'edit_post', + 'delete' => 'delete_post', + 'batch' => 'edit_others_posts', + ); + + if ( 'revision' === $post_type ) { + $permission = false; + } else { + $cap = $contexts[ $context ]; + $post_type_object = get_post_type_object( $post_type ); + $permission = current_user_can( $post_type_object->cap->$cap, $object_id ); + } + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $post_type ); +} + +/** + * Check permissions of users on REST API. + * + * @since 2.6.0 + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_user_permissions( $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'list_users', + 'create' => 'promote_users', // Check if current user can create users, shop managers are not allowed to create users. + 'edit' => 'edit_users', + 'delete' => 'delete_users', + 'batch' => 'promote_users', + ); + + // Check to allow shop_managers to manage only customers. + if ( in_array( $context, array( 'edit', 'delete' ), true ) && wc_current_user_has_role( 'shop_manager' ) ) { + $permission = false; + $user_data = get_userdata( $object_id ); + $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); + + if ( isset( $user_data->roles ) ) { + $can_manage_users = array_intersect( $user_data->roles, array_unique( $shop_manager_editable_roles ) ); + + // Check if Shop Manager can edit customer or with the is same shop manager. + if ( 0 < count( $can_manage_users ) || intval( $object_id ) === intval( get_current_user_id() ) ) { + $permission = current_user_can( $contexts[ $context ], $object_id ); + } + } + } else { + $permission = current_user_can( $contexts[ $context ], $object_id ); + } + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'user' ); +} + +/** + * Check permissions of product terms on REST API. + * + * @since 2.6.0 + * @param string $taxonomy Taxonomy. + * @param string $context Request context. + * @param int $object_id Post ID. + * @return bool + */ +function wc_rest_check_product_term_permissions( $taxonomy, $context = 'read', $object_id = 0 ) { + $contexts = array( + 'read' => 'manage_terms', + 'create' => 'edit_terms', + 'edit' => 'edit_terms', + 'delete' => 'delete_terms', + 'batch' => 'edit_terms', + ); + + $cap = $contexts[ $context ]; + $taxonomy_object = get_taxonomy( $taxonomy ); + $permission = current_user_can( $taxonomy_object->cap->$cap, $object_id ); + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, $taxonomy ); +} + +/** + * Check manager permissions on REST API. + * + * @since 2.6.0 + * @param string $object Object. + * @param string $context Request context. + * @return bool + */ +function wc_rest_check_manager_permissions( $object, $context = 'read' ) { + $objects = array( + 'reports' => 'view_woocommerce_reports', + 'settings' => 'manage_woocommerce', + 'system_status' => 'manage_woocommerce', + 'attributes' => 'manage_product_terms', + 'shipping_methods' => 'manage_woocommerce', + 'payment_gateways' => 'manage_woocommerce', + 'webhooks' => 'manage_woocommerce', + ); + + $permission = current_user_can( $objects[ $object ] ); + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, 0, $object ); +} + +/** + * Check product reviews permissions on REST API. + * + * @since 3.5.0 + * @param string $context Request context. + * @param string $object_id Object ID. + * @return bool + */ +function wc_rest_check_product_reviews_permissions( $context = 'read', $object_id = 0 ) { + $permission = false; + $contexts = array( + 'read' => 'moderate_comments', + 'create' => 'moderate_comments', + 'edit' => 'moderate_comments', + 'delete' => 'moderate_comments', + 'batch' => 'moderate_comments', + ); + + if ( isset( $contexts[ $context ] ) ) { + $permission = current_user_can( $contexts[ $context ] ); + } + + return apply_filters( 'woocommerce_rest_check_permissions', $permission, $context, $object_id, 'product_review' ); +} diff --git a/includes/wc-stock-functions.php b/includes/wc-stock-functions.php new file mode 100644 index 0000000..f8fc3d5 --- /dev/null +++ b/includes/wc-stock-functions.php @@ -0,0 +1,415 @@ +managing_stock() ) { + // Some products (variations) can have their stock managed by their parent. Get the correct object to be updated here. + $product_id_with_stock = $product->get_stock_managed_by_id(); + $product_with_stock = $product_id_with_stock !== $product->get_id() ? wc_get_product( $product_id_with_stock ) : $product; + $data_store = WC_Data_Store::load( 'product' ); + + // Fire actions to let 3rd parties know the stock is about to be changed. + if ( $product_with_stock->is_type( 'variation' ) ) { + do_action( 'woocommerce_variation_before_set_stock', $product_with_stock ); + } else { + do_action( 'woocommerce_product_before_set_stock', $product_with_stock ); + } + + // Update the database. + $new_stock = $data_store->update_product_stock( $product_id_with_stock, $stock_quantity, $operation ); + + // Update the product object. + $data_store->read_stock_quantity( $product_with_stock, $new_stock ); + + // If this is not being called during an update routine, save the product so stock status etc is in sync, and caches are cleared. + if ( ! $updating ) { + $product_with_stock->save(); + } + + // Fire actions to let 3rd parties know the stock changed. + if ( $product_with_stock->is_type( 'variation' ) ) { + do_action( 'woocommerce_variation_set_stock', $product_with_stock ); + } else { + do_action( 'woocommerce_product_set_stock', $product_with_stock ); + } + + return $product_with_stock->get_stock_quantity(); + } + return $product->get_stock_quantity(); +} + +/** + * Update a product's stock status. + * + * @param int $product_id Product ID. + * @param string $status Status. + */ +function wc_update_product_stock_status( $product_id, $status ) { + $product = wc_get_product( $product_id ); + + if ( $product ) { + $product->set_stock_status( $status ); + $product->save(); + } +} + +/** + * When a payment is complete, we can reduce stock levels for items within an order. + * + * @since 3.0.0 + * @param int $order_id Order ID. + */ +function wc_maybe_reduce_stock_levels( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return; + } + + $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); + $trigger_reduce = apply_filters( 'woocommerce_payment_complete_reduce_order_stock', ! $stock_reduced, $order_id ); + + // Only continue if we're reducing stock. + if ( ! $trigger_reduce ) { + return; + } + + wc_reduce_stock_levels( $order ); + + // Ensure stock is marked as "reduced" in case payment complete or other stock actions are called. + $order->get_data_store()->set_stock_reduced( $order_id, true ); +} +add_action( 'woocommerce_payment_complete', 'wc_maybe_reduce_stock_levels' ); +add_action( 'woocommerce_order_status_completed', 'wc_maybe_reduce_stock_levels' ); +add_action( 'woocommerce_order_status_processing', 'wc_maybe_reduce_stock_levels' ); +add_action( 'woocommerce_order_status_on-hold', 'wc_maybe_reduce_stock_levels' ); + +/** + * When a payment is cancelled, restore stock. + * + * @since 3.0.0 + * @param int $order_id Order ID. + */ +function wc_maybe_increase_stock_levels( $order_id ) { + $order = wc_get_order( $order_id ); + + if ( ! $order ) { + return; + } + + $stock_reduced = $order->get_data_store()->get_stock_reduced( $order_id ); + $trigger_increase = (bool) $stock_reduced; + + // Only continue if we're increasing stock. + if ( ! $trigger_increase ) { + return; + } + + wc_increase_stock_levels( $order ); + + // Ensure stock is not marked as "reduced" anymore. + $order->get_data_store()->set_stock_reduced( $order_id, false ); +} +add_action( 'woocommerce_order_status_cancelled', 'wc_maybe_increase_stock_levels' ); +add_action( 'woocommerce_order_status_pending', 'wc_maybe_increase_stock_levels' ); + +/** + * Reduce stock levels for items within an order, if stock has not already been reduced for the items. + * + * @since 3.0.0 + * @param int|WC_Order $order_id Order ID or order instance. + */ +function wc_reduce_stock_levels( $order_id ) { + if ( is_a( $order_id, 'WC_Order' ) ) { + $order = $order_id; + $order_id = $order->get_id(); + } else { + $order = wc_get_order( $order_id ); + } + // We need an order, and a store with stock management to continue. + if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_reduce_order_stock', true, $order ) ) { + return; + } + + $changes = array(); + + // Loop over all items. + foreach ( $order->get_items() as $item ) { + if ( ! $item->is_type( 'line_item' ) ) { + continue; + } + + // Only reduce stock once for each item. + $product = $item->get_product(); + $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); + + if ( $item_stock_reduced || ! $product || ! $product->managing_stock() ) { + continue; + } + + /** + * Filter order item quantity. + * + * @param int|float $quantity Quantity. + * @param WC_Order $order Order data. + * @param WC_Order_Item_Product $item Order item data. + */ + $qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); + $item_name = $product->get_formatted_name(); + $new_stock = wc_update_product_stock( $product, $qty, 'decrease' ); + + if ( is_wp_error( $new_stock ) ) { + /* translators: %s item name. */ + $order->add_order_note( sprintf( __( 'Unable to reduce stock for item %s.', 'woocommerce' ), $item_name ) ); + continue; + } + + $item->add_meta_data( '_reduced_stock', $qty, true ); + $item->save(); + + $changes[] = array( + 'product' => $product, + 'from' => $new_stock + $qty, + 'to' => $new_stock, + ); + } + + wc_trigger_stock_change_notifications( $order, $changes ); + + do_action( 'woocommerce_reduce_order_stock', $order ); +} + +/** + * After stock change events, triggers emails and adds order notes. + * + * @since 3.5.0 + * @param WC_Order $order order object. + * @param array $changes Array of changes. + */ +function wc_trigger_stock_change_notifications( $order, $changes ) { + if ( empty( $changes ) ) { + return; + } + + $order_notes = array(); + $no_stock_amount = absint( get_option( 'woocommerce_notify_no_stock_amount', 0 ) ); + + foreach ( $changes as $change ) { + $order_notes[] = $change['product']->get_formatted_name() . ' ' . $change['from'] . '→' . $change['to']; + $low_stock_amount = absint( wc_get_low_stock_amount( wc_get_product( $change['product']->get_id() ) ) ); + if ( $change['to'] <= $no_stock_amount ) { + do_action( 'woocommerce_no_stock', wc_get_product( $change['product']->get_id() ) ); + } elseif ( $change['to'] <= $low_stock_amount ) { + do_action( 'woocommerce_low_stock', wc_get_product( $change['product']->get_id() ) ); + } + + if ( $change['to'] < 0 ) { + do_action( + 'woocommerce_product_on_backorder', + array( + 'product' => wc_get_product( $change['product']->get_id() ), + 'order_id' => $order->get_id(), + 'quantity' => abs( $change['from'] - $change['to'] ), + ) + ); + } + } + + $order->add_order_note( __( 'Stock levels reduced:', 'woocommerce' ) . ' ' . implode( ', ', $order_notes ) ); +} + +/** + * Increase stock levels for items within an order. + * + * @since 3.0.0 + * @param int|WC_Order $order_id Order ID or order instance. + */ +function wc_increase_stock_levels( $order_id ) { + if ( is_a( $order_id, 'WC_Order' ) ) { + $order = $order_id; + $order_id = $order->get_id(); + } else { + $order = wc_get_order( $order_id ); + } + + // We need an order, and a store with stock management to continue. + if ( ! $order || 'yes' !== get_option( 'woocommerce_manage_stock' ) || ! apply_filters( 'woocommerce_can_restore_order_stock', true, $order ) ) { + return; + } + + $changes = array(); + + // Loop over all items. + foreach ( $order->get_items() as $item ) { + if ( ! $item->is_type( 'line_item' ) ) { + continue; + } + + // Only increase stock once for each item. + $product = $item->get_product(); + $item_stock_reduced = $item->get_meta( '_reduced_stock', true ); + + if ( ! $item_stock_reduced || ! $product || ! $product->managing_stock() ) { + continue; + } + + $item_name = $product->get_formatted_name(); + $new_stock = wc_update_product_stock( $product, $item_stock_reduced, 'increase' ); + + if ( is_wp_error( $new_stock ) ) { + /* translators: %s item name. */ + $order->add_order_note( sprintf( __( 'Unable to restore stock for item %s.', 'woocommerce' ), $item_name ) ); + continue; + } + + $item->delete_meta_data( '_reduced_stock' ); + $item->save(); + + $changes[] = $item_name . ' ' . ( $new_stock - $item_stock_reduced ) . '→' . $new_stock; + } + + if ( $changes ) { + $order->add_order_note( __( 'Stock levels increased:', 'woocommerce' ) . ' ' . implode( ', ', $changes ) ); + } + + do_action( 'woocommerce_restore_order_stock', $order ); +} + +/** + * See how much stock is being held in pending orders. + * + * @since 3.5.0 + * @param WC_Product $product Product to check. + * @param integer $exclude_order_id Order ID to exclude. + * @return int + */ +function wc_get_held_stock_quantity( WC_Product $product, $exclude_order_id = 0 ) { + /** + * Filter: woocommerce_hold_stock_for_checkout + * Allows enable/disable hold stock functionality on checkout. + * + * @since 4.3.0 + * @param bool $enabled Default to true if managing stock globally. + */ + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { + return 0; + } + + return ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->get_reserved_stock( $product, $exclude_order_id ); +} + +/** + * Hold stock for an order. + * + * @throws ReserveStockException If reserve stock fails. + * + * @since 4.1.0 + * @param \WC_Order|int $order Order ID or instance. + */ +function wc_reserve_stock_for_order( $order ) { + /** + * Filter: woocommerce_hold_stock_for_checkout + * Allows enable/disable hold stock functionality on checkout. + * + * @since @since 4.1.0 + * @param bool $enabled Default to true if managing stock globally. + */ + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { + return; + } + + $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); + + if ( $order ) { + ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->reserve_stock_for_order( $order ); + } +} +add_action( 'woocommerce_checkout_order_created', 'wc_reserve_stock_for_order' ); + +/** + * Release held stock for an order. + * + * @since 4.3.0 + * @param \WC_Order|int $order Order ID or instance. + */ +function wc_release_stock_for_order( $order ) { + /** + * Filter: woocommerce_hold_stock_for_checkout + * Allows enable/disable hold stock functionality on checkout. + * + * @since 4.3.0 + * @param bool $enabled Default to true if managing stock globally. + */ + if ( ! apply_filters( 'woocommerce_hold_stock_for_checkout', wc_string_to_bool( get_option( 'woocommerce_manage_stock', 'yes' ) ) ) ) { + return; + } + + $order = $order instanceof WC_Order ? $order : wc_get_order( $order ); + + if ( $order ) { + ( new \Automattic\WooCommerce\Checkout\Helpers\ReserveStock() )->release_stock_for_order( $order ); + } +} +add_action( 'woocommerce_checkout_order_exception', 'wc_release_stock_for_order' ); +add_action( 'woocommerce_payment_complete', 'wc_release_stock_for_order', 11 ); +add_action( 'woocommerce_order_status_cancelled', 'wc_release_stock_for_order', 11 ); +add_action( 'woocommerce_order_status_completed', 'wc_release_stock_for_order', 11 ); +add_action( 'woocommerce_order_status_processing', 'wc_release_stock_for_order', 11 ); +add_action( 'woocommerce_order_status_on-hold', 'wc_release_stock_for_order', 11 ); + +/** + * Return low stock amount to determine if notification needs to be sent + * + * Since 5.2.0, this function no longer redirects from variation to its parent product. + * Low stock amount can now be attached to the variation itself and if it isn't, only + * then we check the parent product, and if it's not there, then we take the default + * from the store-wide setting. + * + * @param WC_Product $product Product to get data from. + * @since 3.5.0 + * @return int + */ +function wc_get_low_stock_amount( WC_Product $product ) { + $low_stock_amount = $product->get_low_stock_amount(); + + if ( '' === $low_stock_amount && $product->is_type( 'variation' ) ) { + $product = wc_get_product( $product->get_parent_id() ); + $low_stock_amount = $product->get_low_stock_amount(); + } + + if ( '' === $low_stock_amount ) { + $low_stock_amount = get_option( 'woocommerce_notify_low_stock_amount', 2 ); + } + + return (int) $low_stock_amount; +} diff --git a/includes/wc-template-functions.php b/includes/wc-template-functions.php new file mode 100644 index 0000000..4f84307 --- /dev/null +++ b/includes/wc-template-functions.php @@ -0,0 +1,3836 @@ +cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) { + wc_add_notice( __( 'Checkout is not available whilst your cart is empty.', 'woocommerce' ), 'notice' ); + wp_safe_redirect( wc_get_cart_url() ); + exit; + + } + + // Logout. + if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) { + wp_safe_redirect( str_replace( '&', '&', wp_logout_url( apply_filters( 'woocommerce_logout_default_redirect_url', wc_get_page_permalink( 'myaccount' ) ) ) ) ); + exit; + } + + // Redirect to the correct logout endpoint. + if ( isset( $wp->query_vars['customer-logout'] ) && 'true' === $wp->query_vars['customer-logout'] ) { + wp_safe_redirect( esc_url_raw( wc_get_account_endpoint_url( 'customer-logout' ) ) ); + exit; + } + + // Trigger 404 if trying to access an endpoint on wrong page. + if ( is_wc_endpoint_url() && ! is_account_page() && ! is_checkout() && apply_filters( 'woocommerce_account_endpoint_page_not_found', true ) ) { + $wp_query->set_404(); + status_header( 404 ); + include get_query_template( '404' ); + exit; + } + + // Redirect to the product page if we have a single product. + if ( is_search() && is_post_type_archive( 'product' ) && apply_filters( 'woocommerce_redirect_single_search_result', true ) && 1 === absint( $wp_query->found_posts ) ) { + $product = wc_get_product( $wp_query->post ); + + if ( $product && $product->is_visible() ) { + wp_safe_redirect( get_permalink( $product->get_id() ), 302 ); + exit; + } + } + + // Ensure gateways and shipping methods are loaded early. + if ( is_add_payment_method_page() || is_checkout() ) { + // Buffer the checkout page. + ob_start(); + + // Ensure gateways and shipping methods are loaded early. + WC()->payment_gateways(); + WC()->shipping(); + } +} +add_action( 'template_redirect', 'wc_template_redirect' ); + +/** + * When loading sensitive checkout or account pages, send a HTTP header to limit rendering of pages to same origin iframes for security reasons. + * + * Can be disabled with: remove_action( 'template_redirect', 'wc_send_frame_options_header' ); + * + * @since 2.3.10 + */ +function wc_send_frame_options_header() { + + if ( ( is_checkout() || is_account_page() ) && ! is_customize_preview() ) { + send_frame_options_header(); + } +} +add_action( 'template_redirect', 'wc_send_frame_options_header' ); + +/** + * No index our endpoints. + * Prevent indexing pages like order-received. + * + * @since 2.5.3 + */ +function wc_prevent_endpoint_indexing() { + // phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged + if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) { + @header( 'X-Robots-Tag: noindex' ); + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged +} +add_action( 'template_redirect', 'wc_prevent_endpoint_indexing' ); + +/** + * Remove adjacent_posts_rel_link_wp_head - pointless for products. + * + * @since 3.0.0 + */ +function wc_prevent_adjacent_posts_rel_link_wp_head() { + if ( is_singular( 'product' ) ) { + remove_action( 'wp_head', 'adjacent_posts_rel_link_wp_head', 10, 0 ); + } +} +add_action( 'template_redirect', 'wc_prevent_adjacent_posts_rel_link_wp_head' ); + +/** + * Show the gallery if JS is disabled. + * + * @since 3.0.6 + */ +function wc_gallery_noscript() { + ?> + + post_type ) || ! in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) { + return; + } + + $GLOBALS['product'] = wc_get_product( $post ); + + return $GLOBALS['product']; +} +add_action( 'the_post', 'wc_setup_product_data' ); + +/** + * Sets up the woocommerce_loop global from the passed args or from the main query. + * + * @since 3.3.0 + * @param array $args Args to pass into the global. + */ +function wc_setup_loop( $args = array() ) { + $default_args = array( + 'loop' => 0, + 'columns' => wc_get_default_products_per_row(), + 'name' => '', + 'is_shortcode' => false, + 'is_paginated' => true, + 'is_search' => false, + 'is_filtered' => false, + 'total' => 0, + 'total_pages' => 0, + 'per_page' => 0, + 'current_page' => 1, + ); + + // If this is a main WC query, use global args as defaults. + if ( $GLOBALS['wp_query']->get( 'wc_query' ) ) { + $default_args = array_merge( + $default_args, + array( + 'is_search' => $GLOBALS['wp_query']->is_search(), + 'is_filtered' => is_filtered(), + 'total' => $GLOBALS['wp_query']->found_posts, + 'total_pages' => $GLOBALS['wp_query']->max_num_pages, + 'per_page' => $GLOBALS['wp_query']->get( 'posts_per_page' ), + 'current_page' => max( 1, $GLOBALS['wp_query']->get( 'paged', 1 ) ), + ) + ); + } + + // Merge any existing values. + if ( isset( $GLOBALS['woocommerce_loop'] ) ) { + $default_args = array_merge( $default_args, $GLOBALS['woocommerce_loop'] ); + } + + $GLOBALS['woocommerce_loop'] = wp_parse_args( $args, $default_args ); +} +add_action( 'woocommerce_before_shop_loop', 'wc_setup_loop' ); + +/** + * Resets the woocommerce_loop global. + * + * @since 3.3.0 + */ +function wc_reset_loop() { + unset( $GLOBALS['woocommerce_loop'] ); +} +add_action( 'woocommerce_after_shop_loop', 'woocommerce_reset_loop', 999 ); + +/** + * Gets a property from the woocommerce_loop global. + * + * @since 3.3.0 + * @param string $prop Prop to get. + * @param string $default Default if the prop does not exist. + * @return mixed + */ +function wc_get_loop_prop( $prop, $default = '' ) { + wc_setup_loop(); // Ensure shop loop is setup. + + return isset( $GLOBALS['woocommerce_loop'], $GLOBALS['woocommerce_loop'][ $prop ] ) ? $GLOBALS['woocommerce_loop'][ $prop ] : $default; +} + +/** + * Sets a property in the woocommerce_loop global. + * + * @since 3.3.0 + * @param string $prop Prop to set. + * @param string $value Value to set. + */ +function wc_set_loop_prop( $prop, $value = '' ) { + if ( ! isset( $GLOBALS['woocommerce_loop'] ) ) { + wc_setup_loop(); + } + $GLOBALS['woocommerce_loop'][ $prop ] = $value; +} + +/** + * Set the current visbility for a product in the woocommerce_loop global. + * + * @since 4.4.0 + * @param int $product_id Product it to cache visbiility for. + * @param bool $value The poduct visibility value to cache. + */ +function wc_set_loop_product_visibility( $product_id, $value ) { + wc_set_loop_prop( "product_visibility_$product_id", $value ); +} + +/** + * Gets the cached current visibility for a product from the woocommerce_loop global. + * + * @since 4.4.0 + * @param int $product_id Product id to get the cached visibility for. + * + * @return bool|null The cached product visibility, or null if on visibility has been cached for that product. + */ +function wc_get_loop_product_visibility( $product_id ) { + return wc_get_loop_prop( "product_visibility_$product_id", null ); +} + +/** + * Should the WooCommerce loop be displayed? + * + * This will return true if we have posts (products) or if we have subcats to display. + * + * @since 3.4.0 + * @return bool + */ +function woocommerce_product_loop() { + return have_posts() || 'products' !== woocommerce_get_loop_display_mode(); +} + +/** + * Output generator tag to aid debugging. + * + * @param string $gen Generator. + * @param string $type Type. + * @return string + */ +function wc_generator_tag( $gen, $type ) { + $version = Constants::get_constant( 'WC_VERSION' ); + + switch ( $type ) { + case 'html': + $gen .= "\n" . ''; + break; + case 'xhtml': + $gen .= "\n" . ''; + break; + } + return $gen; +} + +/** + * Add body classes for WC pages. + * + * @param array $classes Body Classes. + * @return array + */ +function wc_body_class( $classes ) { + $classes = (array) $classes; + + if ( is_shop() ) { + + $classes[] = 'woocommerce-shop'; + + } + + if ( is_woocommerce() ) { + + $classes[] = 'woocommerce'; + $classes[] = 'woocommerce-page'; + + } elseif ( is_checkout() ) { + + $classes[] = 'woocommerce-checkout'; + $classes[] = 'woocommerce-page'; + + } elseif ( is_cart() ) { + + $classes[] = 'woocommerce-cart'; + $classes[] = 'woocommerce-page'; + + } elseif ( is_account_page() ) { + + $classes[] = 'woocommerce-account'; + $classes[] = 'woocommerce-page'; + + } + + if ( is_store_notice_showing() ) { + $classes[] = 'woocommerce-demo-store'; + } + + foreach ( WC()->query->get_query_vars() as $key => $value ) { + if ( is_wc_endpoint_url( $key ) ) { + $classes[] = 'woocommerce-' . sanitize_html_class( $key ); + } + } + + $classes[] = 'woocommerce-no-js'; + + add_action( 'wp_footer', 'wc_no_js' ); + + return array_unique( $classes ); +} + +/** + * NO JS handling. + * + * @since 3.4.0 + */ +function wc_no_js() { + ?> + + $max_columns ) { + $columns = $max_columns; + update_option( 'woocommerce_catalog_columns', $columns ); + } + + if ( has_filter( 'loop_shop_columns' ) ) { // Legacy filter handling. + $columns = apply_filters( 'loop_shop_columns', $columns ); + } + + $columns = absint( $columns ); + + return max( 1, $columns ); +} + +/** + * Get the default rows setting - this is how many product rows will be shown in loops. + * + * @since 3.3.0 + * @return int + */ +function wc_get_default_product_rows_per_page() { + $rows = absint( get_option( 'woocommerce_catalog_rows', 4 ) ); + $product_grid = wc_get_theme_support( 'product_grid' ); + $min_rows = isset( $product_grid['min_rows'] ) ? absint( $product_grid['min_rows'] ) : 0; + $max_rows = isset( $product_grid['max_rows'] ) ? absint( $product_grid['max_rows'] ) : 0; + + if ( $min_rows && $rows < $min_rows ) { + $rows = $min_rows; + update_option( 'woocommerce_catalog_rows', $rows ); + } elseif ( $max_rows && $rows > $max_rows ) { + $rows = $max_rows; + update_option( 'woocommerce_catalog_rows', $rows ); + } + + return $rows; +} + +/** + * Reset the product grid settings when a new theme is activated. + * + * @since 3.3.0 + */ +function wc_reset_product_grid_settings() { + $product_grid = wc_get_theme_support( 'product_grid' ); + + if ( ! empty( $product_grid['default_rows'] ) ) { + update_option( 'woocommerce_catalog_rows', absint( $product_grid['default_rows'] ) ); + } + + if ( ! empty( $product_grid['default_columns'] ) ) { + update_option( 'woocommerce_catalog_columns', absint( $product_grid['default_columns'] ) ); + } + + wp_cache_flush(); // Flush any caches which could impact settings or templates. +} +add_action( 'after_switch_theme', 'wc_reset_product_grid_settings' ); + +/** + * Get classname for woocommerce loops. + * + * @since 2.6.0 + * @return string + */ +function wc_get_loop_class() { + $loop_index = wc_get_loop_prop( 'loop', 0 ); + $columns = absint( max( 1, wc_get_loop_prop( 'columns', wc_get_default_products_per_row() ) ) ); + + $loop_index ++; + wc_set_loop_prop( 'loop', $loop_index ); + + if ( 0 === ( $loop_index - 1 ) % $columns || 1 === $columns ) { + return 'first'; + } + + if ( 0 === $loop_index % $columns ) { + return 'last'; + } + + return ''; +} + + +/** + * Get the classes for the product cat div. + * + * @since 2.4.0 + * + * @param string|array $class One or more classes to add to the class list. + * @param object $category object Optional. + * + * @return array + */ +function wc_get_product_cat_class( $class = '', $category = null ) { + $classes = is_array( $class ) ? $class : array_map( 'trim', explode( ' ', $class ) ); + $classes[] = 'product-category'; + $classes[] = 'product'; + $classes[] = wc_get_loop_class(); + $classes = apply_filters( 'product_cat_class', $classes, $class, $category ); + + return array_unique( array_filter( $classes ) ); +} + +/** + * Adds extra post classes for products via the WordPress post_class hook, if used. + * + * Note: For performance reasons we instead recommend using wc_product_class/wc_get_product_class instead. + * + * @since 2.1.0 + * @param array $classes Current classes. + * @param string|array $class Additional class. + * @param int $post_id Post ID. + * @return array + */ +function wc_product_post_class( $classes, $class = '', $post_id = 0 ) { + if ( ! $post_id || ! in_array( get_post_type( $post_id ), array( 'product', 'product_variation' ), true ) ) { + return $classes; + } + + $product = wc_get_product( $post_id ); + + if ( ! $product ) { + return $classes; + } + + $classes[] = 'product'; + $classes[] = wc_get_loop_class(); + $classes[] = $product->get_stock_status(); + + if ( $product->is_on_sale() ) { + $classes[] = 'sale'; + } + if ( $product->is_featured() ) { + $classes[] = 'featured'; + } + if ( $product->is_downloadable() ) { + $classes[] = 'downloadable'; + } + if ( $product->is_virtual() ) { + $classes[] = 'virtual'; + } + if ( $product->is_sold_individually() ) { + $classes[] = 'sold-individually'; + } + if ( $product->is_taxable() ) { + $classes[] = 'taxable'; + } + if ( $product->is_shipping_taxable() ) { + $classes[] = 'shipping-taxable'; + } + if ( $product->is_purchasable() ) { + $classes[] = 'purchasable'; + } + if ( $product->get_type() ) { + $classes[] = 'product-type-' . $product->get_type(); + } + if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) { + $classes[] = 'has-default-attributes'; + } + + $key = array_search( 'hentry', $classes, true ); + if ( false !== $key ) { + unset( $classes[ $key ] ); + } + + return $classes; +} + +/** + * Get product taxonomy HTML classes. + * + * @since 3.4.0 + * @param array $term_ids Array of terms IDs or objects. + * @param string $taxonomy Taxonomy. + * @return array + */ +function wc_get_product_taxonomy_class( $term_ids, $taxonomy ) { + $classes = array(); + + foreach ( $term_ids as $term_id ) { + $term = get_term( $term_id, $taxonomy ); + + if ( empty( $term->slug ) ) { + continue; + } + + $term_class = sanitize_html_class( $term->slug, $term->term_id ); + if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) { + $term_class = $term->term_id; + } + + // 'post_tag' uses the 'tag' prefix for backward compatibility. + if ( 'post_tag' === $taxonomy ) { + $classes[] = 'tag-' . $term_class; + } else { + $classes[] = sanitize_html_class( $taxonomy . '-' . $term_class, $taxonomy . '-' . $term->term_id ); + } + } + + return $classes; +} + +/** + * Retrieves the classes for the post div as an array. + * + * This method was modified from WordPress's get_post_class() to allow the removal of taxonomies + * (for performance reasons). Previously wc_product_post_class was hooked into post_class. @since 3.6.0 + * + * @since 3.4.0 + * @param string|array $class One or more classes to add to the class list. + * @param int|WP_Post|WC_Product $product Product ID or product object. + * @return array + */ +function wc_get_product_class( $class = '', $product = null ) { + if ( is_null( $product ) && ! empty( $GLOBALS['product'] ) ) { + // Product was null so pull from global. + $product = $GLOBALS['product']; + } + + if ( $product && ! is_a( $product, 'WC_Product' ) ) { + // Make sure we have a valid product, or set to false. + $product = wc_get_product( $product ); + } + + if ( $class ) { + if ( ! is_array( $class ) ) { + $class = preg_split( '#\s+#', $class ); + } + } else { + $class = array(); + } + + $post_classes = array_map( 'esc_attr', $class ); + + if ( ! $product ) { + return $post_classes; + } + + // Run through the post_class hook so 3rd parties using this previously can still append classes. + // Note, to change classes you will need to use the newer woocommerce_post_class filter. + // @internal This removes the wc_product_post_class filter so classes are not duplicated. + $filtered = has_filter( 'post_class', 'wc_product_post_class' ); + + if ( $filtered ) { + remove_filter( 'post_class', 'wc_product_post_class', 20 ); + } + + $post_classes = apply_filters( 'post_class', $post_classes, $class, $product->get_id() ); + + if ( $filtered ) { + add_filter( 'post_class', 'wc_product_post_class', 20, 3 ); + } + + $classes = array_merge( + $post_classes, + array( + 'product', + 'type-product', + 'post-' . $product->get_id(), + 'status-' . $product->get_status(), + wc_get_loop_class(), + $product->get_stock_status(), + ), + wc_get_product_taxonomy_class( $product->get_category_ids(), 'product_cat' ), + wc_get_product_taxonomy_class( $product->get_tag_ids(), 'product_tag' ) + ); + + if ( $product->get_image_id() ) { + $classes[] = 'has-post-thumbnail'; + } + if ( $product->get_post_password() ) { + $classes[] = post_password_required( $product->get_id() ) ? 'post-password-required' : 'post-password-protected'; + } + if ( $product->is_on_sale() ) { + $classes[] = 'sale'; + } + if ( $product->is_featured() ) { + $classes[] = 'featured'; + } + if ( $product->is_downloadable() ) { + $classes[] = 'downloadable'; + } + if ( $product->is_virtual() ) { + $classes[] = 'virtual'; + } + if ( $product->is_sold_individually() ) { + $classes[] = 'sold-individually'; + } + if ( $product->is_taxable() ) { + $classes[] = 'taxable'; + } + if ( $product->is_shipping_taxable() ) { + $classes[] = 'shipping-taxable'; + } + if ( $product->is_purchasable() ) { + $classes[] = 'purchasable'; + } + if ( $product->get_type() ) { + $classes[] = 'product-type-' . $product->get_type(); + } + if ( $product->is_type( 'variable' ) && $product->get_default_attributes() ) { + $classes[] = 'has-default-attributes'; + } + + // Include attributes and any extra taxonomies only if enabled via the hook - this is a performance issue. + if ( apply_filters( 'woocommerce_get_product_class_include_taxonomies', false ) ) { + $taxonomies = get_taxonomies( array( 'public' => true ) ); + $type = 'variation' === $product->get_type() ? 'product_variation' : 'product'; + foreach ( (array) $taxonomies as $taxonomy ) { + if ( is_object_in_taxonomy( $type, $taxonomy ) && ! in_array( $taxonomy, array( 'product_cat', 'product_tag' ), true ) ) { + $classes = array_merge( $classes, wc_get_product_taxonomy_class( (array) get_the_terms( $product->get_id(), $taxonomy ), $taxonomy ) ); + } + } + } + + /** + * WooCommerce Post Class filter. + * + * @since 3.6.2 + * @param array $classes Array of CSS classes. + * @param WC_Product $product Product object. + */ + $classes = apply_filters( 'woocommerce_post_class', $classes, $product ); + + return array_map( 'esc_attr', array_unique( array_filter( $classes ) ) ); +} + +/** + * Display the classes for the product div. + * + * @since 3.4.0 + * @param string|array $class One or more classes to add to the class list. + * @param int|WP_Post|WC_Product $product_id Product ID or product object. + */ +function wc_product_class( $class = '', $product_id = null ) { + echo 'class="' . esc_attr( implode( ' ', wc_get_product_class( $class, $product_id ) ) ) . '"'; +} + +/** + * Outputs hidden form inputs for each query string variable. + * + * @since 3.0.0 + * @param string|array $values Name value pairs, or a URL to parse. + * @param array $exclude Keys to exclude. + * @param string $current_key Current key we are outputting. + * @param bool $return Whether to return. + * @return string + */ +function wc_query_string_form_fields( $values = null, $exclude = array(), $current_key = '', $return = false ) { + if ( is_null( $values ) ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $values = $_GET; + } elseif ( is_string( $values ) ) { + $url_parts = wp_parse_url( $values ); + $values = array(); + + if ( ! empty( $url_parts['query'] ) ) { + // This is to preserve full-stops, pluses and spaces in the query string when ran through parse_str. + $replace_chars = array( + '.' => '{dot}', + '+' => '{plus}', + ); + + $query_string = str_replace( array_keys( $replace_chars ), array_values( $replace_chars ), $url_parts['query'] ); + + // Parse the string. + parse_str( $query_string, $parsed_query_string ); + + // Convert the full-stops, pluses and spaces back and add to values array. + foreach ( $parsed_query_string as $key => $value ) { + $new_key = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $key ); + $new_value = str_replace( array_values( $replace_chars ), array_keys( $replace_chars ), $value ); + $values[ $new_key ] = $new_value; + } + } + } + $html = ''; + + foreach ( $values as $key => $value ) { + if ( in_array( $key, $exclude, true ) ) { + continue; + } + if ( $current_key ) { + $key = $current_key . '[' . $key . ']'; + } + if ( is_array( $value ) ) { + $html .= wc_query_string_form_fields( $value, $exclude, $key, true ); + } else { + $html .= ''; + } + } + + if ( $return ) { + return $html; + } + + echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +/** + * Get the terms and conditons page ID. + * + * @since 3.4.0 + * @return int + */ +function wc_terms_and_conditions_page_id() { + $page_id = wc_get_page_id( 'terms' ); + return apply_filters( 'woocommerce_terms_and_conditions_page_id', 0 < $page_id ? absint( $page_id ) : 0 ); +} + +/** + * Get the privacy policy page ID. + * + * @since 3.4.0 + * @return int + */ +function wc_privacy_policy_page_id() { + $page_id = get_option( 'wp_page_for_privacy_policy', 0 ); + return apply_filters( 'woocommerce_privacy_policy_page_id', 0 < $page_id ? absint( $page_id ) : 0 ); +} + +/** + * See if the checkbox is enabled or not based on the existance of the terms page and checkbox text. + * + * @since 3.4.0 + * @return bool + */ +function wc_terms_and_conditions_checkbox_enabled() { + $page_id = wc_terms_and_conditions_page_id(); + $page = $page_id ? get_post( $page_id ) : false; + return $page && wc_get_terms_and_conditions_checkbox_text(); +} + +/** + * Get the terms and conditons checkbox text, if set. + * + * @since 3.4.0 + * @return string + */ +function wc_get_terms_and_conditions_checkbox_text() { + /* translators: %s terms and conditions page name and link */ + return trim( apply_filters( 'woocommerce_get_terms_and_conditions_checkbox_text', get_option( 'woocommerce_checkout_terms_and_conditions_checkbox_text', sprintf( __( 'I have read and agree to the website %s', 'woocommerce' ), '[terms]' ) ) ) ); +} + +/** + * Get the privacy policy text, if set. + * + * @since 3.4.0 + * @param string $type Type of policy to load. Valid values include registration and checkout. + * @return string + */ +function wc_get_privacy_policy_text( $type = '' ) { + $text = ''; + + switch ( $type ) { + case 'checkout': + /* translators: %s privacy policy page name and link */ + $text = get_option( 'woocommerce_checkout_privacy_policy_text', sprintf( __( 'Your personal data will be used to process your order, support your experience throughout this website, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) ); + break; + case 'registration': + /* translators: %s privacy policy page name and link */ + $text = get_option( 'woocommerce_registration_privacy_policy_text', sprintf( __( 'Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our %s.', 'woocommerce' ), '[privacy_policy]' ) ); + break; + } + + return trim( apply_filters( 'woocommerce_get_privacy_policy_text', $text, $type ) ); +} + +/** + * Output t&c checkbox text. + * + * @since 3.4.0 + */ +function wc_terms_and_conditions_checkbox_text() { + $text = wc_get_terms_and_conditions_checkbox_text(); + + if ( ! $text ) { + return; + } + + echo wp_kses_post( wc_replace_policy_page_link_placeholders( $text ) ); +} + +/** + * Output t&c page's content (if set). The page can be set from checkout settings. + * + * @since 3.4.0 + */ +function wc_terms_and_conditions_page_content() { + $terms_page_id = wc_terms_and_conditions_page_id(); + + if ( ! $terms_page_id ) { + return; + } + + $page = get_post( $terms_page_id ); + + if ( $page && 'publish' === $page->post_status && $page->post_content && ! has_shortcode( $page->post_content, 'woocommerce_checkout' ) ) { + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } +} + +/** + * Render privacy policy text on the checkout. + * + * @since 3.4.0 + */ +function wc_checkout_privacy_policy_text() { + echo '
    '; + wc_privacy_policy_text( 'checkout' ); + echo '
    '; +} + +/** + * Render privacy policy text on the register forms. + * + * @since 3.4.0 + */ +function wc_registration_privacy_policy_text() { + echo '
    '; + wc_privacy_policy_text( 'registration' ); + echo '
    '; +} + +/** + * Output privacy policy text. This is custom text which can be added via the customizer/privacy settings section. + * + * Loads the relevant policy for the current page unless a specific policy text is required. + * + * @since 3.4.0 + * @param string $type Type of policy to load. Valid values include registration and checkout. + */ +function wc_privacy_policy_text( $type = 'checkout' ) { + if ( ! wc_privacy_policy_page_id() ) { + return; + } + echo wp_kses_post( wpautop( wc_replace_policy_page_link_placeholders( wc_get_privacy_policy_text( $type ) ) ) ); +} + +/** + * Replaces placeholders with links to WooCommerce policy pages. + * + * @since 3.4.0 + * @param string $text Text to find/replace within. + * @return string + */ +function wc_replace_policy_page_link_placeholders( $text ) { + $privacy_page_id = wc_privacy_policy_page_id(); + $terms_page_id = wc_terms_and_conditions_page_id(); + $privacy_link = $privacy_page_id ? '
    ' . __( 'privacy policy', 'woocommerce' ) . '' : __( 'privacy policy', 'woocommerce' ); + $terms_link = $terms_page_id ? '' . __( 'terms and conditions', 'woocommerce' ) . '' : __( 'terms and conditions', 'woocommerce' ); + + $find_replace = array( + '[terms]' => $terms_link, + '[privacy_policy]' => $privacy_link, + ); + + return str_replace( array_keys( $find_replace ), array_values( $find_replace ), $text ); +} + +/** + * Template pages + */ + +if ( ! function_exists( 'woocommerce_content' ) ) { + + /** + * Output WooCommerce content. + * + * This function is only used in the optional 'woocommerce.php' template. + * which people can add to their themes to add basic woocommerce support. + * without hooks or modifying core templates. + */ + function woocommerce_content() { + + if ( is_singular( 'product' ) ) { + + while ( have_posts() ) : + the_post(); + wc_get_template_part( 'content', 'single-product' ); + endwhile; + + } else { + ?> + + + +

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

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

    ' . get_the_title() . '

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

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

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

    %3$s

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

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

    '; +} + +/** + * Disable search engines indexing core, dynamic, cart/checkout pages. + * + * @todo Deprecated this function after dropping support for WP 5.6. + * @since 3.2.0 + */ +function wc_page_noindex() { + // wp_no_robots is deprecated since WP 5.7. + if ( function_exists( 'wp_robots_no_robots' ) ) { + return; + } + + if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) { + wp_no_robots(); + } +} +add_action( 'wp_head', 'wc_page_noindex' ); + +/** + * Disable search engines indexing core, dynamic, cart/checkout pages. + * Uses "wp_robots" filter introduced in WP 5.7. + * + * @since 5.0.0 + * @param array $robots Associative array of robots directives. + * @return array Filtered robots directives. + */ +function wc_page_no_robots( $robots ) { + if ( is_page( wc_get_page_id( 'cart' ) ) || is_page( wc_get_page_id( 'checkout' ) ) || is_page( wc_get_page_id( 'myaccount' ) ) ) { + return wp_robots_no_robots( $robots ); + } + + return $robots; +} +add_filter( 'wp_robots', 'wc_page_no_robots' ); + +/** + * Get a slug identifying the current theme. + * + * @since 3.3.0 + * @return string + */ +function wc_get_theme_slug_for_templates() { + return apply_filters( 'woocommerce_theme_slug_for_templates', get_option( 'template' ) ); +} + +/** + * Gets and formats a list of cart item data + variations for display on the frontend. + * + * @since 3.3.0 + * @param array $cart_item Cart item object. + * @param bool $flat Should the data be returned flat or in a list. + * @return string + */ +function wc_get_formatted_cart_item_data( $cart_item, $flat = false ) { + $item_data = array(); + + // Variation values are shown only if they are not found in the title as of 3.0. + // This is because variation titles display the attributes. + if ( $cart_item['data']->is_type( 'variation' ) && is_array( $cart_item['variation'] ) ) { + foreach ( $cart_item['variation'] as $name => $value ) { + $taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $name ) ) ); + + if ( taxonomy_exists( $taxonomy ) ) { + // If this is a term slug, get the term's nice name. + $term = get_term_by( 'slug', $value, $taxonomy ); + if ( ! is_wp_error( $term ) && $term && $term->name ) { + $value = $term->name; + } + $label = wc_attribute_label( $taxonomy ); + } else { + // If this is a custom option slug, get the options name. + $value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $cart_item['data'] ); + $label = wc_attribute_label( str_replace( 'attribute_', '', $name ), $cart_item['data'] ); + } + + // Check the nicename against the title. + if ( '' === $value || wc_is_attribute_in_product_name( $value, $cart_item['data']->get_name() ) ) { + continue; + } + + $item_data[] = array( + 'key' => $label, + 'value' => $value, + ); + } + } + + // Filter item data to allow 3rd parties to add more to the array. + $item_data = apply_filters( 'woocommerce_get_item_data', $item_data, $cart_item ); + + // Format item data ready to display. + foreach ( $item_data as $key => $data ) { + // Set hidden to true to not display meta on cart. + if ( ! empty( $data['hidden'] ) ) { + unset( $item_data[ $key ] ); + continue; + } + $item_data[ $key ]['key'] = ! empty( $data['key'] ) ? $data['key'] : $data['name']; + $item_data[ $key ]['display'] = ! empty( $data['display'] ) ? $data['display'] : $data['value']; + } + + // Output flat or in list format. + if ( count( $item_data ) > 0 ) { + ob_start(); + + if ( $flat ) { + foreach ( $item_data as $data ) { + echo esc_html( $data['key'] ) . ': ' . wp_kses_post( $data['display'] ) . "\n"; + } + } else { + wc_get_template( 'cart/cart-item-data.php', array( 'item_data' => $item_data ) ); + } + + return ob_get_clean(); + } + + return ''; +} + +/** + * Gets the url to remove an item from the cart. + * + * @since 3.3.0 + * @param string $cart_item_key contains the id of the cart item. + * @return string url to page + */ +function wc_get_cart_remove_url( $cart_item_key ) { + $cart_page_url = wc_get_cart_url(); + return apply_filters( 'woocommerce_get_remove_url', $cart_page_url ? wp_nonce_url( add_query_arg( 'remove_item', $cart_item_key, $cart_page_url ), 'woocommerce-cart' ) : '' ); +} + +/** + * Gets the url to re-add an item into the cart. + * + * @since 3.3.0 + * @param string $cart_item_key Cart item key to undo. + * @return string url to page + */ +function wc_get_cart_undo_url( $cart_item_key ) { + $cart_page_url = wc_get_cart_url(); + + $query_args = array( + 'undo_item' => $cart_item_key, + ); + + return apply_filters( 'woocommerce_get_undo_url', $cart_page_url ? wp_nonce_url( add_query_arg( $query_args, $cart_page_url ), 'woocommerce-cart' ) : '', $cart_item_key ); +} + +/** + * Outputs all queued notices on WC pages. + * + * @since 3.5.0 + */ +function woocommerce_output_all_notices() { + echo '
    '; + wc_print_notices(); + echo '
    '; +} + +/** + * Products RSS Feed. + * + * @deprecated 2.6 + */ +function wc_products_rss_feed() { + wc_deprecated_function( 'wc_products_rss_feed', '2.6' ); +} + +if ( ! function_exists( 'woocommerce_reset_loop' ) ) { + + /** + * Reset the loop's index and columns when we're done outputting a product loop. + * + * @deprecated 3.3 + */ + function woocommerce_reset_loop() { + wc_reset_loop(); + } +} + +if ( ! function_exists( 'woocommerce_product_reviews_tab' ) ) { + /** + * Output the reviews tab content. + * + * @deprecated 2.4.0 Unused. + */ + function woocommerce_product_reviews_tab() { + wc_deprecated_function( 'woocommerce_product_reviews_tab', '2.4' ); + } +} + +/** + * Display pay buttons HTML. + * + * @since 3.9.0 + */ +function wc_get_pay_buttons() { + $supported_gateways = array(); + $available_gateways = WC()->payment_gateways()->get_available_payment_gateways(); + + foreach ( $available_gateways as $gateway ) { + if ( $gateway->supports( 'pay_button' ) ) { + $supported_gateways[] = $gateway->get_pay_button_id(); + } + } + + if ( ! $supported_gateways ) { + return; + } + + echo '
    '; + foreach ( $supported_gateways as $pay_button_id ) { + echo sprintf( '
    ', esc_attr( $pay_button_id ) ); + } + echo '
    '; +} + +// phpcs:enable Generic.Commenting.Todo.TaskFound diff --git a/includes/wc-template-hooks.php b/includes/wc-template-hooks.php new file mode 100644 index 0000000..cc0b518 --- /dev/null +++ b/includes/wc-template-hooks.php @@ -0,0 +1,314 @@ +query_vars; + + // Put back valid orderby values. + if ( 'menu_order' === $args['orderby'] ) { + $args['orderby'] = 'name'; + $args['force_menu_order_sort'] = true; + } + + if ( 'name_num' === $args['orderby'] ) { + $args['orderby'] = 'name'; + $args['force_numeric_name'] = true; + } + + // When COUNTING, disable custom sorting. + if ( 'count' === $args['fields'] ) { + return; + } + + // Support menu_order arg used in previous versions. + if ( ! empty( $args['menu_order'] ) ) { + $args['order'] = 'DESC' === strtoupper( $args['menu_order'] ) ? 'DESC' : 'ASC'; + $args['force_menu_order_sort'] = true; + } + + if ( ! empty( $args['force_menu_order_sort'] ) ) { + $args['orderby'] = 'meta_value_num'; + $args['meta_key'] = 'order'; // phpcs:ignore + $terms_query->meta_query->parse_query_vars( $args ); + } +} +add_action( 'pre_get_terms', 'wc_change_pre_get_terms', 10, 1 ); + +/** + * Adjust term query to handle custom sorting parameters. + * + * @param array $clauses Clauses. + * @param array $taxonomies Taxonomies. + * @param array $args Arguments. + * @return array + */ +function wc_terms_clauses( $clauses, $taxonomies, $args ) { + global $wpdb; + + // No need to filter when counting. + if ( strpos( $clauses['fields'], 'COUNT(*)' ) !== false ) { + return $clauses; + } + + // Force numeric sort if using name_num custom sorting param. + if ( ! empty( $args['force_numeric_name'] ) ) { + $clauses['orderby'] = str_replace( 'ORDER BY t.name', 'ORDER BY t.name+0', $clauses['orderby'] ); + } + + // For sorting, force left join in case order meta is missing. + if ( ! empty( $args['force_menu_order_sort'] ) ) { + $clauses['join'] = str_replace( "INNER JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id )", "LEFT JOIN {$wpdb->termmeta} ON ( t.term_id = {$wpdb->termmeta}.term_id AND {$wpdb->termmeta}.meta_key='order')", $clauses['join'] ); + $clauses['where'] = str_replace( "{$wpdb->termmeta}.meta_key = 'order'", "( {$wpdb->termmeta}.meta_key = 'order' OR {$wpdb->termmeta}.meta_key IS NULL )", $clauses['where'] ); + $clauses['orderby'] = 'DESC' === $args['order'] ? str_replace( 'meta_value+0', 'meta_value+0 DESC, t.name', $clauses['orderby'] ) : str_replace( 'meta_value+0', 'meta_value+0 ASC, t.name', $clauses['orderby'] ); + } + + return $clauses; +} +add_filter( 'terms_clauses', 'wc_terms_clauses', 99, 3 ); + +/** + * Helper to get cached object terms and filter by field using wp_list_pluck(). + * Works as a cached alternative for wp_get_post_terms() and wp_get_object_terms(). + * + * @since 3.0.0 + * @param int $object_id Object ID. + * @param string $taxonomy Taxonomy slug. + * @param string $field Field name. + * @param string $index_key Index key name. + * @return array + */ +function wc_get_object_terms( $object_id, $taxonomy, $field = null, $index_key = null ) { + // Test if terms exists. get_the_terms() return false when it finds no terms. + $terms = get_the_terms( $object_id, $taxonomy ); + + if ( ! $terms || is_wp_error( $terms ) ) { + return array(); + } + + return is_null( $field ) ? $terms : wp_list_pluck( $terms, $field, $index_key ); +} + +/** + * Cached version of wp_get_post_terms(). + * This is a private function (internal use ONLY). + * + * @since 3.0.0 + * @param int $product_id Product ID. + * @param string $taxonomy Taxonomy slug. + * @param array $args Query arguments. + * @return array + */ +function _wc_get_cached_product_terms( $product_id, $taxonomy, $args = array() ) { + $cache_key = 'wc_' . $taxonomy . md5( wp_json_encode( $args ) ); + $cache_group = WC_Cache_Helper::get_cache_prefix( 'product_' . $product_id ) . $product_id; + $terms = wp_cache_get( $cache_key, $cache_group ); + + if ( false !== $terms ) { + return $terms; + } + + $terms = wp_get_post_terms( $product_id, $taxonomy, $args ); + + wp_cache_add( $cache_key, $terms, $cache_group ); + + return $terms; +} + +/** + * Wrapper used to get terms for a product. + * + * @param int $product_id Product ID. + * @param string $taxonomy Taxonomy slug. + * @param array $args Query arguments. + * @return array + */ +function wc_get_product_terms( $product_id, $taxonomy, $args = array() ) { + if ( ! taxonomy_exists( $taxonomy ) ) { + return array(); + } + + return apply_filters( 'woocommerce_get_product_terms', _wc_get_cached_product_terms( $product_id, $taxonomy, $args ), $product_id, $taxonomy, $args ); +} + +/** + * Sort by name (numeric). + * + * @param WP_Post $a First item to compare. + * @param WP_Post $b Second item to compare. + * @return int + */ +function _wc_get_product_terms_name_num_usort_callback( $a, $b ) { + $a_name = (float) $a->name; + $b_name = (float) $b->name; + + if ( abs( $a_name - $b_name ) < 0.001 ) { + return 0; + } + + return ( $a_name < $b_name ) ? -1 : 1; +} + +/** + * Sort by parent. + * + * @param WP_Post $a First item to compare. + * @param WP_Post $b Second item to compare. + * @return int + */ +function _wc_get_product_terms_parent_usort_callback( $a, $b ) { + if ( $a->parent === $b->parent ) { + return 0; + } + return ( $a->parent < $b->parent ) ? 1 : -1; +} + +/** + * WooCommerce Dropdown categories. + * + * @param array $args Args to control display of dropdown. + */ +function wc_product_dropdown_categories( $args = array() ) { + global $wp_query; + + $args = wp_parse_args( + $args, + array( + 'pad_counts' => 1, + 'show_count' => 1, + 'hierarchical' => 1, + 'hide_empty' => 1, + 'show_uncategorized' => 1, + 'orderby' => 'name', + 'selected' => isset( $wp_query->query_vars['product_cat'] ) ? $wp_query->query_vars['product_cat'] : '', + 'show_option_none' => __( 'Select a category', 'woocommerce' ), + 'option_none_value' => '', + 'value_field' => 'slug', + 'taxonomy' => 'product_cat', + 'name' => 'product_cat', + 'class' => 'dropdown_product_cat', + ) + ); + + if ( 'order' === $args['orderby'] ) { + $args['orderby'] = 'meta_value_num'; + $args['meta_key'] = 'order'; // phpcs:ignore + } + + wp_dropdown_categories( $args ); +} + +/** + * Custom walker for Product Categories. + * + * Previously used by wc_product_dropdown_categories, but wp_dropdown_categories has been fixed in core. + * + * @param mixed ...$args Variable number of parameters to be passed to the walker. + * @return mixed + */ +function wc_walk_category_dropdown_tree( ...$args ) { + if ( ! class_exists( 'WC_Product_Cat_Dropdown_Walker', false ) ) { + include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-dropdown-walker.php'; + } + + // The user's options are the third parameter. + if ( empty( $args[2]['walker'] ) || ! is_a( $args[2]['walker'], 'Walker' ) ) { + $walker = new WC_Product_Cat_Dropdown_Walker(); + } else { + $walker = $args[2]['walker']; + } + + return $walker->walk( ...$args ); +} + +/** + * Migrate data from WC term meta to WP term meta. + * + * When the database is updated to support term meta, migrate WC term meta data across. + * We do this when the new version is >= 34370, and the old version is < 34370 (34370 is when term meta table was added). + * + * @param string $wp_db_version The new $wp_db_version. + * @param string $wp_current_db_version The old (current) $wp_db_version. + */ +function wc_taxonomy_metadata_migrate_data( $wp_db_version, $wp_current_db_version ) { + if ( $wp_db_version >= 34370 && $wp_current_db_version < 34370 ) { + global $wpdb; + if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); + } + } +} +add_action( 'wp_upgrade', 'wc_taxonomy_metadata_migrate_data', 10, 2 ); + +/** + * Move a term before the a given element of its hierarchy level. + * + * @param int $the_term Term ID. + * @param int $next_id The id of the next sibling element in save hierarchy level. + * @param string $taxonomy Taxnomy. + * @param int $index Term index (default: 0). + * @param mixed $terms List of terms. (default: null). + * @return int + */ +function wc_reorder_terms( $the_term, $next_id, $taxonomy, $index = 0, $terms = null ) { + if ( ! $terms ) { + $terms = get_terms( $taxonomy, 'hide_empty=0&parent=0&menu_order=ASC' ); + } + if ( empty( $terms ) ) { + return $index; + } + + $id = intval( $the_term->term_id ); + + $term_in_level = false; // Flag: is our term to order in this level of terms. + + foreach ( $terms as $term ) { + $term_id = intval( $term->term_id ); + + if ( $term_id === $id ) { // Our term to order, we skip. + $term_in_level = true; + continue; // Our term to order, we skip. + } + // the nextid of our term to order, lets move our term here. + if ( null !== $next_id && $term_id === $next_id ) { + $index++; + $index = wc_set_term_order( $id, $index, $taxonomy, true ); + } + + // Set order. + $index++; + $index = wc_set_term_order( $term_id, $index, $taxonomy ); + + /** + * After a term has had it's order set. + */ + do_action( 'woocommerce_after_set_term_order', $term, $index, $taxonomy ); + + // If that term has children we walk through them. + $children = get_terms( $taxonomy, "parent={$term_id}&hide_empty=0&menu_order=ASC" ); + if ( ! empty( $children ) ) { + $index = wc_reorder_terms( $the_term, $next_id, $taxonomy, $index, $children ); + } + } + + // No nextid meaning our term is in last position. + if ( $term_in_level && null === $next_id ) { + $index = wc_set_term_order( $id, $index + 1, $taxonomy, true ); + } + + return $index; +} + +/** + * Set the sort order of a term. + * + * @param int $term_id Term ID. + * @param int $index Index. + * @param string $taxonomy Taxonomy. + * @param bool $recursive Recursive (default: false). + * @return int + */ +function wc_set_term_order( $term_id, $index, $taxonomy, $recursive = false ) { + + $term_id = (int) $term_id; + $index = (int) $index; + + update_term_meta( $term_id, 'order', $index ); + + if ( ! $recursive ) { + return $index; + } + + $children = get_terms( $taxonomy, "parent=$term_id&hide_empty=0&menu_order=ASC" ); + + foreach ( $children as $term ) { + $index++; + $index = wc_set_term_order( $term->term_id, $index, $taxonomy, true ); + } + + clean_term_cache( $term_id, $taxonomy ); + + return $index; +} + +/** + * Function for recounting product terms, ignoring hidden products. + * + * @param array $terms List of terms. + * @param object $taxonomy Taxonomy. + * @param bool $callback Callback. + * @param bool $terms_are_term_taxonomy_ids If terms are from term_taxonomy_id column. + */ +function _wc_term_recount( $terms, $taxonomy, $callback = true, $terms_are_term_taxonomy_ids = true ) { + global $wpdb; + + /** + * Filter to allow/prevent recounting of terms as it could be expensive. + * A likely scenario for this is when bulk importing products. We could + * then prevent it from recounting per product but instead recount it once + * when import is done. Of course this means the import logic has to support this. + * + * @since 5.2 + * @param bool + */ + if ( ! apply_filters( 'woocommerce_product_recount_terms', '__return_true' ) ) { + return; + } + + // Standard callback. + if ( $callback ) { + _update_post_term_count( $terms, $taxonomy ); + } + + $exclude_term_ids = array(); + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + if ( $product_visibility_term_ids['exclude-from-catalog'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['exclude-from-catalog']; + } + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && $product_visibility_term_ids['outofstock'] ) { + $exclude_term_ids[] = $product_visibility_term_ids['outofstock']; + } + + $query = array( + 'fields' => " + SELECT COUNT( DISTINCT ID ) FROM {$wpdb->posts} p + ", + 'join' => '', + 'where' => " + WHERE 1=1 + AND p.post_status = 'publish' + AND p.post_type = 'product' + + ", + ); + + if ( count( $exclude_term_ids ) ) { + $query['join'] .= " LEFT JOIN ( SELECT object_id FROM {$wpdb->term_relationships} WHERE term_taxonomy_id IN ( " . implode( ',', array_map( 'absint', $exclude_term_ids ) ) . ' ) ) AS exclude_join ON exclude_join.object_id = p.ID'; + $query['where'] .= ' AND exclude_join.object_id IS NULL'; + } + + // Pre-process term taxonomy ids. + if ( ! $terms_are_term_taxonomy_ids ) { + // We passed in an array of TERMS in format id=>parent. + $terms = array_filter( (array) array_keys( $terms ) ); + } else { + // If we have term taxonomy IDs we need to get the term ID. + $term_taxonomy_ids = $terms; + $terms = array(); + foreach ( $term_taxonomy_ids as $term_taxonomy_id ) { + $term = get_term_by( 'term_taxonomy_id', $term_taxonomy_id, $taxonomy->name ); + $terms[] = $term->term_id; + } + } + + // Exit if we have no terms to count. + if ( empty( $terms ) ) { + return; + } + + // Ancestors need counting. + if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { + foreach ( $terms as $term_id ) { + $terms = array_merge( $terms, get_ancestors( $term_id, $taxonomy->name ) ); + } + } + + // Unique terms only. + $terms = array_unique( $terms ); + + // Count the terms. + foreach ( $terms as $term_id ) { + $terms_to_count = array( absint( $term_id ) ); + + if ( is_taxonomy_hierarchical( $taxonomy->name ) ) { + // We need to get the $term's hierarchy so we can count its children too. + $children = get_term_children( $term_id, $taxonomy->name ); + + if ( $children && ! is_wp_error( $children ) ) { + $terms_to_count = array_unique( array_map( 'absint', array_merge( $terms_to_count, $children ) ) ); + } + } + + // Generate term query. + $term_query = $query; + $term_query['join'] .= " INNER JOIN ( SELECT object_id FROM {$wpdb->term_relationships} INNER JOIN {$wpdb->term_taxonomy} using( term_taxonomy_id ) WHERE term_id IN ( " . implode( ',', array_map( 'absint', $terms_to_count ) ) . ' ) ) AS include_join ON include_join.object_id = p.ID'; + + // Get the count. + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $count = $wpdb->get_var( implode( ' ', $term_query ) ); + + // Update the count. + update_term_meta( $term_id, 'product_count_' . $taxonomy->name, absint( $count ) ); + } + + delete_transient( 'wc_term_counts' ); +} + +/** + * Recount terms after the stock amount changes. + * + * @param int $product_id Product ID. + */ +function wc_recount_after_stock_change( $product_id ) { + if ( 'yes' !== get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + return; + } + + _wc_recount_terms_by_product( $product_id ); +} +add_action( 'woocommerce_product_set_stock_status', 'wc_recount_after_stock_change' ); + + +/** + * Overrides the original term count for product categories and tags with the product count. + * that takes catalog visibility into account. + * + * @param array $terms List of terms. + * @param string|array $taxonomies Single taxonomy or list of taxonomies. + * @return array + */ +function wc_change_term_counts( $terms, $taxonomies ) { + if ( is_admin() || is_ajax() ) { + return $terms; + } + + if ( ! isset( $taxonomies[0] ) || ! in_array( $taxonomies[0], apply_filters( 'woocommerce_change_term_counts', array( 'product_cat', 'product_tag' ) ), true ) ) { + return $terms; + } + + $o_term_counts = get_transient( 'wc_term_counts' ); + $term_counts = $o_term_counts; + + foreach ( $terms as &$term ) { + if ( is_object( $term ) ) { + $term_counts[ $term->term_id ] = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : get_term_meta( $term->term_id, 'product_count_' . $taxonomies[0], true ); + + if ( '' !== $term_counts[ $term->term_id ] ) { + $term->count = absint( $term_counts[ $term->term_id ] ); + } + } + } + + // Update transient. + if ( $term_counts !== $o_term_counts ) { + set_transient( 'wc_term_counts', $term_counts, DAY_IN_SECONDS * 30 ); + } + + return $terms; +} +add_filter( 'get_terms', 'wc_change_term_counts', 10, 2 ); + +/** + * Return products in a given term, and cache value. + * + * To keep in sync, product_count will be cleared on "set_object_terms". + * + * @param int $term_id Term ID. + * @param string $taxonomy Taxonomy. + * @return array + */ +function wc_get_term_product_ids( $term_id, $taxonomy ) { + $product_ids = get_term_meta( $term_id, 'product_ids', true ); + + if ( false === $product_ids || ! is_array( $product_ids ) ) { + $product_ids = get_objects_in_term( $term_id, $taxonomy ); + update_term_meta( $term_id, 'product_ids', $product_ids ); + } + + return $product_ids; +} + +/** + * When a post is updated and terms recounted (called by _update_post_term_count), clear the ids. + * + * @param int $object_id Object ID. + * @param array $terms An array of object terms. + * @param array $tt_ids An array of term taxonomy IDs. + * @param string $taxonomy Taxonomy slug. + * @param bool $append Whether to append new terms to the old terms. + * @param array $old_tt_ids Old array of term taxonomy IDs. + */ +function wc_clear_term_product_ids( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { + foreach ( $old_tt_ids as $term_id ) { + delete_term_meta( $term_id, 'product_ids' ); + } + foreach ( $tt_ids as $term_id ) { + delete_term_meta( $term_id, 'product_ids' ); + } +} +add_action( 'set_object_terms', 'wc_clear_term_product_ids', 10, 6 ); + +/** + * Get full list of product visibilty term ids. + * + * @since 3.0.0 + * @return int[] + */ +function wc_get_product_visibility_term_ids() { + if ( ! taxonomy_exists( 'product_visibility' ) ) { + wc_doing_it_wrong( __FUNCTION__, 'wc_get_product_visibility_term_ids should not be called before taxonomies are registered (woocommerce_after_register_post_type action).', '3.1' ); + return array(); + } + return array_map( + 'absint', + wp_parse_args( + wp_list_pluck( + get_terms( + array( + 'taxonomy' => 'product_visibility', + 'hide_empty' => false, + ) + ), + 'term_taxonomy_id', + 'name' + ), + array( + 'exclude-from-catalog' => 0, + 'exclude-from-search' => 0, + 'featured' => 0, + 'outofstock' => 0, + 'rated-1' => 0, + 'rated-2' => 0, + 'rated-3' => 0, + 'rated-4' => 0, + 'rated-5' => 0, + ) + ) + ); +} + +/** + * Recounts all terms. + * + * @since 5.2 + * @return void + */ +function wc_recount_all_terms() { + $product_cats = get_terms( + 'product_cat', + array( + 'hide_empty' => false, + 'fields' => 'id=>parent', + ) + ); + _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false ); + $product_tags = get_terms( + 'product_tag', + array( + 'hide_empty' => false, + 'fields' => 'id=>parent', + ) + ); + _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false ); +} + +/** + * Recounts terms by product. + * + * @since 5.2 + * @param int $product_id The ID of the product. + * @return void + */ +function _wc_recount_terms_by_product( $product_id = '' ) { + if ( empty( $product_id ) ) { + return; + } + + $product_terms = get_the_terms( $product_id, 'product_cat' ); + + if ( $product_terms ) { + $product_cats = array(); + + foreach ( $product_terms as $term ) { + $product_cats[ $term->term_id ] = $term->parent; + } + + _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), false, false ); + } + + $product_terms = get_the_terms( $product_id, 'product_tag' ); + + if ( $product_terms ) { + $product_tags = array(); + + foreach ( $product_terms as $term ) { + $product_tags[ $term->term_id ] = $term->parent; + } + + _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), false, false ); + } +} diff --git a/includes/wc-update-functions.php b/includes/wc-update-functions.php new file mode 100644 index 0000000..653b5a4 --- /dev/null +++ b/includes/wc-update-functions.php @@ -0,0 +1,2299 @@ +get_results( "SELECT meta_value, meta_id, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_path' AND meta_value != '';" ); + + if ( $existing_file_paths ) { + + foreach ( $existing_file_paths as $existing_file_path ) { + + $old_file_path = trim( $existing_file_path->meta_value ); + + if ( ! empty( $old_file_path ) ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $file_paths = serialize( array( md5( $old_file_path ) => $old_file_path ) ); + + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_file_paths', meta_value = %s WHERE meta_id = %d", $file_paths, $existing_file_path->meta_id ) ); + + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->prefix}woocommerce_downloadable_product_permissions SET download_id = %s WHERE product_id = %d", md5( $old_file_path ), $existing_file_path->post_id ) ); + + } + } + } +} + +/** + * Update permalinks for 2.0 + * + * @return void + */ +function wc_update_200_permalinks() { + // Setup default permalinks if shop page is defined. + $permalinks = get_option( 'woocommerce_permalinks' ); + $shop_page_id = wc_get_page_id( 'shop' ); + + if ( empty( $permalinks ) && $shop_page_id > 0 ) { + + $base_slug = $shop_page_id > 0 && get_post( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop'; + + $category_base = 'yes' === get_option( 'woocommerce_prepend_shop_page_to_urls' ) ? trailingslashit( $base_slug ) : ''; + $category_slug = get_option( 'woocommerce_product_category_slug' ) ? get_option( 'woocommerce_product_category_slug' ) : _x( 'product-category', 'slug', 'woocommerce' ); + $tag_slug = get_option( 'woocommerce_product_tag_slug' ) ? get_option( 'woocommerce_product_tag_slug' ) : _x( 'product-tag', 'slug', 'woocommerce' ); + + if ( 'yes' === get_option( 'woocommerce_prepend_shop_page_to_products' ) ) { + $product_base = trailingslashit( $base_slug ); + } else { + $product_slug = get_option( 'woocommerce_product_slug' ); + if ( false !== $product_slug && ! empty( $product_slug ) ) { + $product_base = trailingslashit( $product_slug ); + } else { + $product_base = trailingslashit( _x( 'product', 'slug', 'woocommerce' ) ); + } + } + + if ( 'yes' === get_option( 'woocommerce_prepend_category_to_products' ) ) { + $product_base .= trailingslashit( '%product_cat%' ); + } + + $permalinks = array( + 'product_base' => untrailingslashit( $product_base ), + 'category_base' => untrailingslashit( $category_base . $category_slug ), + 'attribute_base' => untrailingslashit( $category_base ), + 'tag_base' => untrailingslashit( $category_base . $tag_slug ), + ); + + update_option( 'woocommerce_permalinks', $permalinks ); + } +} + +/** + * Update sub-category display options for 2.0 + * + * @return void + */ +function wc_update_200_subcat_display() { + // Update subcat display settings. + if ( 'yes' === get_option( 'woocommerce_shop_show_subcategories' ) ) { + if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) { + update_option( 'woocommerce_shop_page_display', 'subcategories' ); + } else { + update_option( 'woocommerce_shop_page_display', 'both' ); + } + } + + if ( 'yes' === get_option( 'woocommerce_show_subcategories' ) ) { + if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) { + update_option( 'woocommerce_category_archive_display', 'subcategories' ); + } else { + update_option( 'woocommerce_category_archive_display', 'both' ); + } + } +} + +/** + * Update tax rates for 2.0 + * + * @return void + */ +function wc_update_200_taxrates() { + global $wpdb; + + // Update tax rates. + $loop = 0; + $tax_rates = get_option( 'woocommerce_tax_rates' ); + + if ( $tax_rates ) { + foreach ( $tax_rates as $tax_rate ) { + + foreach ( $tax_rate['countries'] as $country => $states ) { + + $states = array_reverse( $states ); + + foreach ( $states as $state ) { + + if ( '*' === $state ) { + $state = ''; + } + + $wpdb->insert( + $wpdb->prefix . 'woocommerce_tax_rates', + array( + 'tax_rate_country' => $country, + 'tax_rate_state' => $state, + 'tax_rate' => $tax_rate['rate'], + 'tax_rate_name' => $tax_rate['label'], + 'tax_rate_priority' => 1, + 'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0, + 'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0, + 'tax_rate_order' => $loop, + 'tax_rate_class' => $tax_rate['class'], + ) + ); + + $loop++; + } + } + } + } + + $local_tax_rates = get_option( 'woocommerce_local_tax_rates' ); + + if ( $local_tax_rates ) { + foreach ( $local_tax_rates as $tax_rate ) { + + $location_type = ( 'postcode' === $tax_rate['location_type'] ) ? 'postcode' : 'city'; + + if ( '*' === $tax_rate['state'] ) { + $tax_rate['state'] = ''; + } + + $wpdb->insert( + $wpdb->prefix . 'woocommerce_tax_rates', + array( + 'tax_rate_country' => $tax_rate['country'], + 'tax_rate_state' => $tax_rate['state'], + 'tax_rate' => $tax_rate['rate'], + 'tax_rate_name' => $tax_rate['label'], + 'tax_rate_priority' => 2, + 'tax_rate_compound' => ( 'yes' === $tax_rate['compound'] ) ? 1 : 0, + 'tax_rate_shipping' => ( 'yes' === $tax_rate['shipping'] ) ? 1 : 0, + 'tax_rate_order' => $loop, + 'tax_rate_class' => $tax_rate['class'], + ) + ); + + $tax_rate_id = $wpdb->insert_id; + + if ( $tax_rate['locations'] ) { + foreach ( $tax_rate['locations'] as $location ) { + + $wpdb->insert( + $wpdb->prefix . 'woocommerce_tax_rate_locations', + array( + 'location_code' => $location, + 'tax_rate_id' => $tax_rate_id, + 'location_type' => $location_type, + ) + ); + + } + } + + $loop++; + } + } + + update_option( 'woocommerce_tax_rates_backup', $tax_rates ); + update_option( 'woocommerce_local_tax_rates_backup', $local_tax_rates ); + delete_option( 'woocommerce_tax_rates' ); + delete_option( 'woocommerce_local_tax_rates' ); +} + +/** + * Update order item line items for 2.0 + * + * @return void + */ +function wc_update_200_line_items() { + global $wpdb; + + // Now its time for the massive update to line items - move them to the new DB tables. + // Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_items' WHERE meta_key = '_order_items_old'. + $order_item_rows = $wpdb->get_results( + "SELECT meta_value, post_id FROM {$wpdb->postmeta} WHERE meta_key = '_order_items'" + ); + + foreach ( $order_item_rows as $order_item_row ) { + + $order_items = (array) maybe_unserialize( $order_item_row->meta_value ); + + foreach ( $order_items as $order_item ) { + + if ( ! isset( $order_item['line_total'] ) && isset( $order_item['taxrate'] ) && isset( $order_item['cost'] ) ) { + $order_item['line_tax'] = number_format( ( $order_item['cost'] * $order_item['qty'] ) * ( $order_item['taxrate'] / 100 ), 2, '.', '' ); + $order_item['line_total'] = $order_item['cost'] * $order_item['qty']; + $order_item['line_subtotal_tax'] = $order_item['line_tax']; + $order_item['line_subtotal'] = $order_item['line_total']; + } + + $order_item['line_tax'] = isset( $order_item['line_tax'] ) ? $order_item['line_tax'] : 0; + $order_item['line_total'] = isset( $order_item['line_total'] ) ? $order_item['line_total'] : 0; + $order_item['line_subtotal_tax'] = isset( $order_item['line_subtotal_tax'] ) ? $order_item['line_subtotal_tax'] : 0; + $order_item['line_subtotal'] = isset( $order_item['line_subtotal'] ) ? $order_item['line_subtotal'] : 0; + + $item_id = wc_add_order_item( + $order_item_row->post_id, + array( + 'order_item_name' => $order_item['name'], + 'order_item_type' => 'line_item', + ) + ); + + // Add line item meta. + if ( $item_id ) { + wc_add_order_item_meta( $item_id, '_qty', absint( $order_item['qty'] ) ); + wc_add_order_item_meta( $item_id, '_tax_class', $order_item['tax_class'] ); + wc_add_order_item_meta( $item_id, '_product_id', $order_item['id'] ); + wc_add_order_item_meta( $item_id, '_variation_id', $order_item['variation_id'] ); + wc_add_order_item_meta( $item_id, '_line_subtotal', wc_format_decimal( $order_item['line_subtotal'] ) ); + wc_add_order_item_meta( $item_id, '_line_subtotal_tax', wc_format_decimal( $order_item['line_subtotal_tax'] ) ); + wc_add_order_item_meta( $item_id, '_line_total', wc_format_decimal( $order_item['line_total'] ) ); + wc_add_order_item_meta( $item_id, '_line_tax', wc_format_decimal( $order_item['line_tax'] ) ); + + $meta_rows = array(); + + // Insert meta. + if ( ! empty( $order_item['item_meta'] ) ) { + foreach ( $order_item['item_meta'] as $key => $meta ) { + // Backwards compatibility. + if ( is_array( $meta ) && isset( $meta['meta_name'] ) ) { + $meta_rows[] = '(' . $item_id . ',"' . esc_sql( $meta['meta_name'] ) . '","' . esc_sql( $meta['meta_value'] ) . '")'; + } else { + $meta_rows[] = '(' . $item_id . ',"' . esc_sql( $key ) . '","' . esc_sql( $meta ) . '")'; + } + } + } + + // Insert meta rows at once. + if ( count( $meta_rows ) > 0 ) { + $wpdb->query( + $wpdb->prepare( + "INSERT INTO {$wpdb->prefix}woocommerce_order_itemmeta ( order_item_id, meta_key, meta_value ) + VALUES " . implode( ',', $meta_rows ) . ';', // @codingStandardsIgnoreLine + $order_item_row->post_id + ) + ); + } + + // Delete from DB (rename). + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} + SET meta_key = '_order_items_old' + WHERE meta_key = '_order_items' + AND post_id = %d", + $order_item_row->post_id + ) + ); + } + + unset( $meta_rows, $item_id, $order_item ); + } + } + + // Do the same kind of update for order_taxes - move to lines. + // Reverse with UPDATE `wpwc_postmeta` SET meta_key = '_order_taxes' WHERE meta_key = '_order_taxes_old'. + $order_tax_rows = $wpdb->get_results( + "SELECT meta_value, post_id FROM {$wpdb->postmeta} + WHERE meta_key = '_order_taxes'" + ); + + foreach ( $order_tax_rows as $order_tax_row ) { + + $order_taxes = (array) maybe_unserialize( $order_tax_row->meta_value ); + + if ( ! empty( $order_taxes ) ) { + foreach ( $order_taxes as $order_tax ) { + + if ( ! isset( $order_tax['label'] ) || ! isset( $order_tax['cart_tax'] ) || ! isset( $order_tax['shipping_tax'] ) ) { + continue; + } + + $item_id = wc_add_order_item( + $order_tax_row->post_id, + array( + 'order_item_name' => $order_tax['label'], + 'order_item_type' => 'tax', + ) + ); + + // Add line item meta. + if ( $item_id ) { + wc_add_order_item_meta( $item_id, 'compound', absint( isset( $order_tax['compound'] ) ? $order_tax['compound'] : 0 ) ); + wc_add_order_item_meta( $item_id, 'tax_amount', wc_clean( $order_tax['cart_tax'] ) ); + wc_add_order_item_meta( $item_id, 'shipping_tax_amount', wc_clean( $order_tax['shipping_tax'] ) ); + } + + // Delete from DB (rename). + $wpdb->query( + $wpdb->prepare( + "UPDATE {$wpdb->postmeta} + SET meta_key = '_order_taxes_old' + WHERE meta_key = '_order_taxes' + AND post_id = %d", + $order_tax_row->post_id + ) + ); + + unset( $tax_amount ); + } + } + } +} + +/** + * Update image settings for 2.0 + * + * @return void + */ +function wc_update_200_images() { + // Grab the pre 2.0 Image options and use to populate the new image options settings, + // cleaning up afterwards like nice people do. + foreach ( array( 'catalog', 'single', 'thumbnail' ) as $value ) { + + $old_settings = array_filter( + array( + 'width' => get_option( 'woocommerce_' . $value . '_image_width' ), + 'height' => get_option( 'woocommerce_' . $value . '_image_height' ), + 'crop' => get_option( 'woocommerce_' . $value . '_image_crop' ), + ) + ); + + if ( ! empty( $old_settings ) && update_option( 'shop_' . $value . '_image_size', $old_settings ) ) { + + delete_option( 'woocommerce_' . $value . '_image_width' ); + delete_option( 'woocommerce_' . $value . '_image_height' ); + delete_option( 'woocommerce_' . $value . '_image_crop' ); + + } + } +} + +/** + * Update DB version for 2.0 + * + * @return void + */ +function wc_update_200_db_version() { + WC_Install::update_db_version( '2.0.0' ); +} + +/** + * Update Brazilian States for 2.0.9 + * + * @return void + */ +function wc_update_209_brazillian_state() { + global $wpdb; + + // phpcs:disable WordPress.DB.SlowDBQuery + + // Update brazillian state codes. + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'BA', + ), + array( + 'meta_key' => '_billing_state', + 'meta_value' => 'BH', + ) + ); + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'BA', + ), + array( + 'meta_key' => '_shipping_state', + 'meta_value' => 'BH', + ) + ); + $wpdb->update( + $wpdb->usermeta, + array( + 'meta_value' => 'BA', + ), + array( + 'meta_key' => 'billing_state', + 'meta_value' => 'BH', + ) + ); + $wpdb->update( + $wpdb->usermeta, + array( + 'meta_value' => 'BA', + ), + array( + 'meta_key' => 'shipping_state', + 'meta_value' => 'BH', + ) + ); + + // phpcs:enable WordPress.DB.SlowDBQuery +} + +/** + * Update DB version for 2.0.9 + * + * @return void + */ +function wc_update_209_db_version() { + WC_Install::update_db_version( '2.0.9' ); +} + +/** + * Remove pages for 2.1 + * + * @return void + */ +function wc_update_210_remove_pages() { + // Pages no longer used. + wp_trash_post( get_option( 'woocommerce_pay_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_thanks_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_view_order_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_change_password_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_edit_address_page_id' ) ); + wp_trash_post( get_option( 'woocommerce_lost_password_page_id' ) ); +} + +/** + * Update file paths to support multiple files for 2.1 + * + * @return void + */ +function wc_update_210_file_paths() { + global $wpdb; + + // Upgrade file paths to support multiple file paths + names etc. + $existing_file_paths = $wpdb->get_results( "SELECT meta_value, meta_id FROM {$wpdb->postmeta} WHERE meta_key = '_file_paths' AND meta_value != '';" ); + + if ( $existing_file_paths ) { + + foreach ( $existing_file_paths as $existing_file_path ) { + + $needs_update = false; + $new_value = array(); + $value = maybe_unserialize( trim( $existing_file_path->meta_value ) ); + + if ( $value ) { + foreach ( $value as $key => $file ) { + if ( ! is_array( $file ) ) { + $needs_update = true; + $new_value[ $key ] = array( + 'file' => $file, + 'name' => wc_get_filename_from_url( $file ), + ); + } else { + $new_value[ $key ] = $file; + } + } + if ( $needs_update ) { + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + $new_value = serialize( $new_value ); + + $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = %s, meta_value = %s WHERE meta_id = %d", '_downloadable_files', $new_value, $existing_file_path->meta_id ) ); + } + } + } + } +} + +/** + * Update DB version for 2.1 + * + * @return void + */ +function wc_update_210_db_version() { + WC_Install::update_db_version( '2.1.0' ); +} + +/** + * Update shipping options for 2.2 + * + * @return void + */ +function wc_update_220_shipping() { + $woocommerce_ship_to_destination = 'shipping'; + + if ( get_option( 'woocommerce_ship_to_billing_address_only' ) === 'yes' ) { + $woocommerce_ship_to_destination = 'billing_only'; + } elseif ( get_option( 'woocommerce_ship_to_billing' ) === 'yes' ) { + $woocommerce_ship_to_destination = 'billing'; + } + + add_option( 'woocommerce_ship_to_destination', $woocommerce_ship_to_destination, '', 'no' ); +} + +/** + * Update order statuses for 2.2 + * + * @return void + */ +function wc_update_220_order_status() { + global $wpdb; + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-pending' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'pending%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-processing' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'processing%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-on-hold' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'on-hold%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-completed' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'completed%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-cancelled' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'cancelled%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-refunded' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'refunded%';" + ); + $wpdb->query( + "UPDATE {$wpdb->posts} as posts + LEFT JOIN {$wpdb->term_relationships} AS rel ON posts.ID = rel.object_id + LEFT JOIN {$wpdb->term_taxonomy} AS tax USING( term_taxonomy_id ) + LEFT JOIN {$wpdb->terms} AS term USING( term_id ) + SET posts.post_status = 'wc-failed' + WHERE posts.post_type = 'shop_order' + AND posts.post_status = 'publish' + AND tax.taxonomy = 'shop_order_status' + AND term.slug LIKE 'failed%';" + ); +} + +/** + * Update variations for 2.2 + * + * @return void + */ +function wc_update_220_variations() { + global $wpdb; + // Update variations which manage stock. + $update_variations = $wpdb->get_results( + "SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent FROM {$wpdb->posts} as posts + LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock' + LEFT OUTER JOIN {$wpdb->postmeta} as postmeta2 ON posts.ID = postmeta2.post_id AND postmeta2.meta_key = '_manage_stock' + WHERE posts.post_type = 'product_variation' + AND postmeta.meta_value IS NOT NULL + AND postmeta.meta_value != '' + AND postmeta2.meta_value IS NULL" + ); + + foreach ( $update_variations as $variation ) { + $parent_backorders = get_post_meta( $variation->variation_parent, '_backorders', true ); + add_post_meta( $variation->variation_id, '_manage_stock', 'yes', true ); + add_post_meta( $variation->variation_id, '_backorders', $parent_backorders ? $parent_backorders : 'no', true ); + } +} + +/** + * Update attributes for 2.2 + * + * @return void + */ +function wc_update_220_attributes() { + global $wpdb; + // Update taxonomy names with correct sanitized names. + $attribute_taxonomies = $wpdb->get_results( 'SELECT attribute_name, attribute_id FROM ' . $wpdb->prefix . 'woocommerce_attribute_taxonomies' ); + + foreach ( $attribute_taxonomies as $attribute_taxonomy ) { + $sanitized_attribute_name = wc_sanitize_taxonomy_name( $attribute_taxonomy->attribute_name ); + if ( $sanitized_attribute_name !== $attribute_taxonomy->attribute_name ) { + if ( ! $wpdb->get_var( $wpdb->prepare( "SELECT 1=1 FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name = %s;", $sanitized_attribute_name ) ) ) { + // Update attribute. + $wpdb->update( + "{$wpdb->prefix}woocommerce_attribute_taxonomies", + array( + 'attribute_name' => $sanitized_attribute_name, + ), + array( + 'attribute_id' => $attribute_taxonomy->attribute_id, + ) + ); + + // Update terms. + $wpdb->update( + $wpdb->term_taxonomy, + array( 'taxonomy' => wc_attribute_taxonomy_name( $sanitized_attribute_name ) ), + array( 'taxonomy' => 'pa_' . $attribute_taxonomy->attribute_name ) + ); + } + } + } + + delete_transient( 'wc_attribute_taxonomies' ); + WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); +} + +/** + * Update DB version for 2.2 + * + * @return void + */ +function wc_update_220_db_version() { + WC_Install::update_db_version( '2.2.0' ); +} + +/** + * Update options for 2.3 + * + * @return void + */ +function wc_update_230_options() { + // _money_spent and _order_count may be out of sync - clear them + delete_metadata( 'user', 0, '_money_spent', '', true ); + delete_metadata( 'user', 0, '_order_count', '', true ); + delete_metadata( 'user', 0, '_last_order', '', true ); + + // To prevent taxes being hidden when using a default 'no address' in a store with tax inc prices, set the woocommerce_default_customer_address to use the store base address by default. + if ( '' === get_option( 'woocommerce_default_customer_address', false ) && wc_prices_include_tax() ) { + update_option( 'woocommerce_default_customer_address', 'base' ); + } +} + +/** + * Update DB version for 2.3 + * + * @return void + */ +function wc_update_230_db_version() { + WC_Install::update_db_version( '2.3.0' ); +} + +/** + * Update calc discount options for 2.4 + * + * @return void + */ +function wc_update_240_options() { + /** + * Coupon discount calculations. + * Maintain the old coupon logic for upgrades. + */ + update_option( 'woocommerce_calc_discounts_sequentially', 'yes' ); +} + +/** + * Update shipping methods for 2.4 + * + * @return void + */ +function wc_update_240_shipping_methods() { + /** + * Flat Rate Shipping. + * Update legacy options to new math based options. + */ + $shipping_methods = array( + 'woocommerce_flat_rates' => new WC_Shipping_Legacy_Flat_Rate(), + 'woocommerce_international_delivery_flat_rates' => new WC_Shipping_Legacy_International_Delivery(), + ); + foreach ( $shipping_methods as $flat_rate_option_key => $shipping_method ) { + // Stop this running more than once if routine is repeated. + if ( version_compare( $shipping_method->get_option( 'version', 0 ), '2.4.0', '<' ) ) { + $shipping_classes = WC()->shipping()->get_shipping_classes(); + $has_classes = count( $shipping_classes ) > 0; + $cost_key = $has_classes ? 'no_class_cost' : 'cost'; + $min_fee = $shipping_method->get_option( 'minimum_fee' ); + $math_cost_strings = array( + 'cost' => array(), + 'no_class_cost' => array(), + ); + + $math_cost_strings[ $cost_key ][] = $shipping_method->get_option( 'cost' ); + $fee = $shipping_method->get_option( 'fee' ); + + if ( $fee ) { + $math_cost_strings[ $cost_key ][] = strstr( $fee, '%' ) ? '[fee percent="' . str_replace( '%', '', $fee ) . '" min="' . esc_attr( $min_fee ) . '"]' : $fee; + } + + foreach ( $shipping_classes as $shipping_class ) { + $rate_key = 'class_cost_' . $shipping_class->slug; + $math_cost_strings[ $rate_key ] = $math_cost_strings['no_class_cost']; + } + + $flat_rates = array_filter( (array) get_option( $flat_rate_option_key, array() ) ); + + if ( $flat_rates ) { + foreach ( $flat_rates as $shipping_class => $rate ) { + $rate_key = 'class_cost_' . $shipping_class; + if ( $rate['cost'] || $rate['fee'] ) { + $math_cost_strings[ $rate_key ][] = $rate['cost']; + $math_cost_strings[ $rate_key ][] = strstr( $rate['fee'], '%' ) ? '[fee percent="' . str_replace( '%', '', $rate['fee'] ) . '" min="' . esc_attr( $min_fee ) . '"]' : $rate['fee']; + } + } + } + + if ( 'item' === $shipping_method->type ) { + foreach ( $math_cost_strings as $key => $math_cost_string ) { + $math_cost_strings[ $key ] = array_filter( array_map( 'trim', $math_cost_strings[ $key ] ) ); + if ( ! empty( $math_cost_strings[ $key ] ) ) { + $last_key = max( 0, count( $math_cost_strings[ $key ] ) - 1 ); + $math_cost_strings[ $key ][0] = '( ' . $math_cost_strings[ $key ][0]; + $math_cost_strings[ $key ][ $last_key ] .= ' ) * [qty]'; + } + } + } + + $math_cost_strings['cost'][] = $shipping_method->get_option( 'cost_per_order' ); + + // Save settings. + foreach ( $math_cost_strings as $option_id => $math_cost_string ) { + $shipping_method->settings[ $option_id ] = implode( ' + ', array_filter( $math_cost_string ) ); + } + + $shipping_method->settings['version'] = '2.4.0'; + $shipping_method->settings['type'] = 'item' === $shipping_method->settings['type'] ? 'class' : $shipping_method->settings['type']; + + update_option( $shipping_method->plugin_id . $shipping_method->id . '_settings', $shipping_method->settings ); + } + } +} + +/** + * Update API keys for 2.4 + * + * @return void + */ +function wc_update_240_api_keys() { + global $wpdb; + /** + * Update the old user API keys to the new Apps keys. + */ + $api_users = $wpdb->get_results( "SELECT user_id FROM $wpdb->usermeta WHERE meta_key = 'woocommerce_api_consumer_key'" ); + $apps_keys = array(); + + // Get user data. + foreach ( $api_users as $_user ) { + $user = get_userdata( $_user->user_id ); + $apps_keys[] = array( + 'user_id' => $user->ID, + 'permissions' => $user->woocommerce_api_key_permissions, + 'consumer_key' => wc_api_hash( $user->woocommerce_api_consumer_key ), + 'consumer_secret' => $user->woocommerce_api_consumer_secret, + 'truncated_key' => substr( $user->woocommerce_api_consumer_secret, -7 ), + ); + } + + if ( ! empty( $apps_keys ) ) { + // Create new apps. + foreach ( $apps_keys as $app ) { + $wpdb->insert( + $wpdb->prefix . 'woocommerce_api_keys', + $app, + array( + '%d', + '%s', + '%s', + '%s', + '%s', + ) + ); + } + + // Delete old user keys from usermeta. + foreach ( $api_users as $_user ) { + $user_id = intval( $_user->user_id ); + delete_user_meta( $user_id, 'woocommerce_api_consumer_key' ); + delete_user_meta( $user_id, 'woocommerce_api_consumer_secret' ); + delete_user_meta( $user_id, 'woocommerce_api_key_permissions' ); + } + } +} + +/** + * Update webhooks for 2.4 + * + * @return void + */ +function wc_update_240_webhooks() { + // phpcs:disable WordPress.DB.SlowDBQuery + + /** + * Webhooks. + * Make sure order.update webhooks get the woocommerce_order_edit_status hook. + */ + $order_update_webhooks = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'shop_webhook', + 'meta_key' => '_topic', + 'meta_value' => 'order.updated', + ) + ); + foreach ( $order_update_webhooks as $order_update_webhook ) { + $webhook = new WC_Webhook( $order_update_webhook->ID ); + $webhook->set_topic( 'order.updated' ); + } + + // phpcs:enable WordPress.DB.SlowDBQuery +} + +/** + * Update refunds for 2.4 + * + * @return void + */ +function wc_update_240_refunds() { + global $wpdb; + /** + * Refunds for full refunded orders. + * Update fully refunded orders to ensure they have a refund line item so reports add up. + */ + $refunded_orders = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'shop_order', + 'post_status' => array( 'wc-refunded' ), + ) + ); + + // Ensure emails are disabled during this update routine. + remove_all_actions( 'woocommerce_order_status_refunded_notification' ); + remove_all_actions( 'woocommerce_order_partially_refunded_notification' ); + remove_action( 'woocommerce_order_status_refunded', array( 'WC_Emails', 'send_transactional_email' ) ); + remove_action( 'woocommerce_order_partially_refunded', array( 'WC_Emails', 'send_transactional_email' ) ); + + foreach ( $refunded_orders as $refunded_order ) { + $order_total = get_post_meta( $refunded_order->ID, '_order_total', true ); + $refunded_total = $wpdb->get_var( + $wpdb->prepare( + "SELECT SUM( postmeta.meta_value ) + FROM $wpdb->postmeta AS postmeta + INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) + WHERE postmeta.meta_key = '_refund_amount' + AND postmeta.post_id = posts.ID", + $refunded_order->ID + ) + ); + + if ( $order_total > $refunded_total ) { + wc_create_refund( + array( + 'amount' => $order_total - $refunded_total, + 'reason' => __( 'Order fully refunded', 'woocommerce' ), + 'order_id' => $refunded_order->ID, + 'line_items' => array(), + 'date' => $refunded_order->post_modified, + ) + ); + } + } + + wc_delete_shop_order_transients(); +} + +/** + * Update DB version for 2.4 + * + * @return void + */ +function wc_update_240_db_version() { + WC_Install::update_db_version( '2.4.0' ); +} + +/** + * Update variations for 2.4.1 + * + * @return void + */ +function wc_update_241_variations() { + global $wpdb; + + // Select variations that don't have any _stock_status implemented on WooCommerce 2.2. + $update_variations = $wpdb->get_results( + "SELECT DISTINCT posts.ID AS variation_id, posts.post_parent AS variation_parent + FROM {$wpdb->posts} as posts + LEFT OUTER JOIN {$wpdb->postmeta} AS postmeta ON posts.ID = postmeta.post_id AND postmeta.meta_key = '_stock_status' + WHERE posts.post_type = 'product_variation' + AND postmeta.meta_value IS NULL" + ); + + foreach ( $update_variations as $variation ) { + // Get the parent _stock_status. + $parent_stock_status = get_post_meta( $variation->variation_parent, '_stock_status', true ); + + // Set the _stock_status. + add_post_meta( $variation->variation_id, '_stock_status', $parent_stock_status ? $parent_stock_status : 'instock', true ); + + // Delete old product children array. + delete_transient( 'wc_product_children_' . $variation->variation_parent ); + } + + // Invalidate old transients such as wc_var_price. + WC_Cache_Helper::get_transient_version( 'product', true ); +} + +/** + * Update DB version for 2.4.1 + * + * @return void + */ +function wc_update_241_db_version() { + WC_Install::update_db_version( '2.4.1' ); +} + +/** + * Update currency settings for 2.5 + * + * @return void + */ +function wc_update_250_currency() { + global $wpdb; + // Fix currency settings for LAK currency. + $current_currency = get_option( 'woocommerce_currency' ); + + if ( 'KIP' === $current_currency ) { + update_option( 'woocommerce_currency', 'LAK' ); + } + + // phpcs:disable WordPress.DB.SlowDBQuery + + // Update LAK currency code. + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'LAK', + ), + array( + 'meta_key' => '_order_currency', + 'meta_value' => 'KIP', + ) + ); + + // phpcs:enable WordPress.DB.SlowDBQuery +} + +/** + * Update DB version for 2.5 + * + * @return void + */ +function wc_update_250_db_version() { + WC_Install::update_db_version( '2.5.0' ); +} + +/** + * Update ship to countries options for 2.6 + * + * @return void + */ +function wc_update_260_options() { + // woocommerce_calc_shipping option has been removed in 2.6. + if ( 'no' === get_option( 'woocommerce_calc_shipping' ) ) { + update_option( 'woocommerce_ship_to_countries', 'disabled' ); + } + + WC_Admin_Notices::add_notice( 'legacy_shipping' ); +} + +/** + * Update term meta for 2.6 + * + * @return void + */ +function wc_update_260_termmeta() { + global $wpdb; + /** + * Migrate term meta to WordPress tables. + */ + if ( get_option( 'db_version' ) >= 34370 && $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_termmeta';" ) ) { + if ( $wpdb->query( "INSERT INTO {$wpdb->termmeta} ( term_id, meta_key, meta_value ) SELECT woocommerce_term_id, meta_key, meta_value FROM {$wpdb->prefix}woocommerce_termmeta;" ) ) { + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_termmeta" ); + wp_cache_flush(); + } + } +} + +/** + * Update zones for 2.6 + * + * @return void + */ +function wc_update_260_zones() { + global $wpdb; + /** + * Old (table rate) shipping zones to new core shipping zones migration. + * zone_enabled and zone_type are no longer used, but it's safe to leave them be. + */ + if ( $wpdb->get_var( "SHOW COLUMNS FROM `{$wpdb->prefix}woocommerce_shipping_zones` LIKE 'zone_enabled';" ) ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_type` `zone_type` VARCHAR(40) NOT NULL DEFAULT '';" ); + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zones CHANGE `zone_enabled` `zone_enabled` INT(1) NOT NULL DEFAULT 1;" ); + } +} + +/** + * Update zone methods for 2.6 + * + * @return void + */ +function wc_update_260_zone_methods() { + global $wpdb; + + /** + * Shipping zones in WC 2.6.0 use a table named woocommerce_shipping_zone_methods. + * Migrate the old data out of woocommerce_shipping_zone_shipping_methods into the new table and port over any known options (used by table rates and flat rate boxes). + */ + if ( $wpdb->get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_shipping_zone_shipping_methods';" ) ) { + $old_methods = $wpdb->get_results( "SELECT zone_id, shipping_method_type, shipping_method_order, shipping_method_id FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods;" ); + + if ( $old_methods ) { + $max_new_id = $wpdb->get_var( "SELECT MAX(instance_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_methods" ); + $max_old_id = $wpdb->get_var( "SELECT MAX(shipping_method_id) FROM {$wpdb->prefix}woocommerce_shipping_zone_shipping_methods" ); + + // Avoid ID conflicts. + $wpdb->query( $wpdb->prepare( "ALTER TABLE {$wpdb->prefix}woocommerce_shipping_zone_methods AUTO_INCREMENT = %d;", max( $max_new_id, $max_old_id ) + 1 ) ); + + // Store changes. + $changes = array(); + + // Move data. + foreach ( $old_methods as $old_method ) { + $wpdb->insert( + $wpdb->prefix . 'woocommerce_shipping_zone_methods', + array( + 'zone_id' => $old_method->zone_id, + 'method_id' => $old_method->shipping_method_type, + 'method_order' => $old_method->shipping_method_order, + ) + ); + + $new_instance_id = $wpdb->insert_id; + + // Move main settings. + $older_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '-' . $old_method->shipping_method_id . '_settings'; + $old_settings_key = 'woocommerce_' . $old_method->shipping_method_type . '_' . $old_method->shipping_method_id . '_settings'; + add_option( 'woocommerce_' . $old_method->shipping_method_type . '_' . $new_instance_id . '_settings', get_option( $old_settings_key, get_option( $older_settings_key ) ) ); + + // Handling for table rate and flat rate box shipping. + if ( 'table_rate' === $old_method->shipping_method_type ) { + // Move priority settings. + add_option( 'woocommerce_table_rate_default_priority_' . $new_instance_id, get_option( 'woocommerce_table_rate_default_priority_' . $old_method->shipping_method_id ) ); + add_option( 'woocommerce_table_rate_priorities_' . $new_instance_id, get_option( 'woocommerce_table_rate_priorities_' . $old_method->shipping_method_id ) ); + + // Move rates. + $wpdb->update( + $wpdb->prefix . 'woocommerce_shipping_table_rates', + array( + 'shipping_method_id' => $new_instance_id, + ), + array( + 'shipping_method_id' => $old_method->shipping_method_id, + ) + ); + } elseif ( 'flat_rate_boxes' === $old_method->shipping_method_type ) { + $wpdb->update( + $wpdb->prefix . 'woocommerce_shipping_flat_rate_boxes', + array( + 'shipping_method_id' => $new_instance_id, + ), + array( + 'shipping_method_id' => $old_method->shipping_method_id, + ) + ); + } + + $changes[ $old_method->shipping_method_id ] = $new_instance_id; + } + + // $changes contains keys (old method ids) and values (new instance ids) if extra processing is needed in plugins. + // Store this to an option so extensions can pick it up later, then fire an action. + update_option( 'woocommerce_updated_instance_ids', $changes ); + do_action( 'woocommerce_updated_instance_ids', $changes ); + } + } + + // Change ranges used to ... + $wpdb->query( "UPDATE {$wpdb->prefix}woocommerce_shipping_zone_locations SET location_code = REPLACE( location_code, '-', '...' );" ); +} + +/** + * Update refunds for 2.6 + * + * @return void + */ +function wc_update_260_refunds() { + global $wpdb; + /** + * Refund item qty should be negative. + */ + $wpdb->query( + "UPDATE {$wpdb->prefix}woocommerce_order_itemmeta as item_meta + LEFT JOIN {$wpdb->prefix}woocommerce_order_items as items ON item_meta.order_item_id = items.order_item_id + LEFT JOIN {$wpdb->posts} as posts ON items.order_id = posts.ID + SET item_meta.meta_value = item_meta.meta_value * -1 + WHERE item_meta.meta_value > 0 AND item_meta.meta_key = '_qty' AND posts.post_type = 'shop_order_refund'" + ); +} + +/** + * Update DB version for 2.6 + * + * @return void + */ +function wc_update_260_db_version() { + WC_Install::update_db_version( '2.6.0' ); +} + +/** + * Update webhooks for 3.0 + * + * @return void + */ +function wc_update_300_webhooks() { + // phpcs:disable WordPress.DB.SlowDBQuery + + /** + * Make sure product.update webhooks get the woocommerce_product_quick_edit_save + * and woocommerce_product_bulk_edit_save hooks. + */ + $product_update_webhooks = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'shop_webhook', + 'meta_key' => '_topic', + 'meta_value' => 'product.updated', + ) + ); + foreach ( $product_update_webhooks as $product_update_webhook ) { + $webhook = new WC_Webhook( $product_update_webhook->ID ); + $webhook->set_topic( 'product.updated' ); + } + + // phpcs:enable WordPress.DB.SlowDBQuery +} + +/** + * Add an index to the field comment_type to improve the response time of the query + * used by WC_Comments::wp_count_comments() to get the number of comments by type. + */ +function wc_update_300_comment_type_index() { + global $wpdb; + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->comments} WHERE column_name = 'comment_type' and key_name = 'woo_idx_comment_type'" ); + + if ( is_null( $index_exists ) ) { + // Add an index to the field comment_type to improve the response time of the query + // used by WC_Comments::wp_count_comments() to get the number of comments by type. + $wpdb->query( "ALTER TABLE {$wpdb->comments} ADD INDEX woo_idx_comment_type (comment_type)" ); + } +} + +/** + * Update grouped products for 3.0 + * + * @return void + */ +function wc_update_300_grouped_products() { + global $wpdb; + $parents = $wpdb->get_col( "SELECT DISTINCT( post_parent ) FROM {$wpdb->posts} WHERE post_parent > 0 AND post_type = 'product';" ); + foreach ( $parents as $parent_id ) { + $parent = wc_get_product( $parent_id ); + if ( $parent && $parent->is_type( 'grouped' ) ) { + $children_ids = get_posts( + array( + 'post_parent' => $parent_id, + 'posts_per_page' => -1, + 'post_type' => 'product', + 'fields' => 'ids', + ) + ); + update_post_meta( $parent_id, '_children', $children_ids ); + + // Update children to remove the parent. + $wpdb->update( + $wpdb->posts, + array( + 'post_parent' => 0, + ), + array( + 'post_parent' => $parent_id, + ) + ); + } + } +} + +/** + * Update shipping tax classes for 3.0 + * + * @return void + */ +function wc_update_300_settings() { + $woocommerce_shipping_tax_class = get_option( 'woocommerce_shipping_tax_class' ); + if ( '' === $woocommerce_shipping_tax_class ) { + update_option( 'woocommerce_shipping_tax_class', 'inherit' ); + } elseif ( 'standard' === $woocommerce_shipping_tax_class ) { + update_option( 'woocommerce_shipping_tax_class', '' ); + } +} + +/** + * Convert meta values into term for product visibility. + */ +function wc_update_300_product_visibility() { + global $wpdb; + + WC_Install::create_terms(); + + $featured_term = get_term_by( 'name', 'featured', 'product_visibility' ); + + if ( $featured_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_featured' AND meta_value = 'yes';", $featured_term->term_taxonomy_id ) ); + } + + $exclude_search_term = get_term_by( 'name', 'exclude-from-search', 'product_visibility' ); + + if ( $exclude_search_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'catalog');", $exclude_search_term->term_taxonomy_id ) ); + } + + $exclude_catalog_term = get_term_by( 'name', 'exclude-from-catalog', 'product_visibility' ); + + if ( $exclude_catalog_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_visibility' AND meta_value IN ('hidden', 'search');", $exclude_catalog_term->term_taxonomy_id ) ); + } + + $outofstock_term = get_term_by( 'name', 'outofstock', 'product_visibility' ); + + if ( $outofstock_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_stock_status' AND meta_value = 'outofstock';", $outofstock_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-1', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 1;", $rating_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-2', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 2;", $rating_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-3', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 3;", $rating_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-4', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 4;", $rating_term->term_taxonomy_id ) ); + } + + $rating_term = get_term_by( 'name', 'rated-5', 'product_visibility' ); + + if ( $rating_term ) { + $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->term_relationships} SELECT post_id, %d, 0 FROM {$wpdb->postmeta} WHERE meta_key = '_wc_average_rating' AND ROUND( meta_value ) = 5;", $rating_term->term_taxonomy_id ) ); + } +} + +/** + * Update DB Version. + */ +function wc_update_300_db_version() { + WC_Install::update_db_version( '3.0.0' ); +} + +/** + * Add an index to the downloadable product permissions table to improve performance of update_user_by_order_id. + */ +function wc_update_310_downloadable_products() { + global $wpdb; + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE column_name = 'order_id' and key_name = 'order_id'" ); + + if ( is_null( $index_exists ) ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX order_id (order_id)" ); + } +} + +/** + * Find old order notes and ensure they have the correct type for exclusion. + */ +function wc_update_310_old_comments() { + global $wpdb; + + $wpdb->query( "UPDATE $wpdb->comments comments LEFT JOIN $wpdb->posts as posts ON comments.comment_post_ID = posts.ID SET comment_type = 'order_note' WHERE posts.post_type = 'shop_order' AND comment_type = '';" ); +} + +/** + * Update DB Version. + */ +function wc_update_310_db_version() { + WC_Install::update_db_version( '3.1.0' ); +} + +/** + * Update shop_manager capabilities. + */ +function wc_update_312_shop_manager_capabilities() { + $role = get_role( 'shop_manager' ); + $role->remove_cap( 'unfiltered_html' ); +} + +/** + * Update DB Version. + */ +function wc_update_312_db_version() { + WC_Install::update_db_version( '3.1.2' ); +} + +/** + * Update state codes for Mexico. + */ +function wc_update_320_mexican_states() { + global $wpdb; + + $mx_states = array( + 'Distrito Federal' => 'CMX', + 'Jalisco' => 'JAL', + 'Nuevo Leon' => 'NLE', + 'Aguascalientes' => 'AGS', + 'Baja California' => 'BCN', + 'Baja California Sur' => 'BCS', + 'Campeche' => 'CAM', + 'Chiapas' => 'CHP', + 'Chihuahua' => 'CHH', + 'Coahuila' => 'COA', + 'Colima' => 'COL', + 'Durango' => 'DGO', + 'Guanajuato' => 'GTO', + 'Guerrero' => 'GRO', + 'Hidalgo' => 'HGO', + 'Estado de Mexico' => 'MEX', + 'Michoacan' => 'MIC', + 'Morelos' => 'MOR', + 'Nayarit' => 'NAY', + 'Oaxaca' => 'OAX', + 'Puebla' => 'PUE', + 'Queretaro' => 'QRO', + 'Quintana Roo' => 'ROO', + 'San Luis Potosi' => 'SLP', + 'Sinaloa' => 'SIN', + 'Sonora' => 'SON', + 'Tabasco' => 'TAB', + 'Tamaulipas' => 'TMP', + 'Tlaxcala' => 'TLA', + 'Veracruz' => 'VER', + 'Yucatan' => 'YUC', + 'Zacatecas' => 'ZAC', + ); + + foreach ( $mx_states as $old => $new ) { + $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->postmeta + SET meta_value = %s + WHERE meta_key IN ( '_billing_state', '_shipping_state' ) + AND meta_value = %s", + $new, + $old + ) + ); + $wpdb->update( + "{$wpdb->prefix}woocommerce_shipping_zone_locations", + array( + 'location_code' => 'MX:' . $new, + ), + array( + 'location_code' => 'MX:' . $old, + ) + ); + $wpdb->update( + "{$wpdb->prefix}woocommerce_tax_rates", + array( + 'tax_rate_state' => strtoupper( $new ), + ), + array( + 'tax_rate_state' => strtoupper( $old ), + ) + ); + } +} + +/** + * Update DB Version. + */ +function wc_update_320_db_version() { + WC_Install::update_db_version( '3.2.0' ); +} + +/** + * Update image settings to use new aspect ratios and widths. + */ +function wc_update_330_image_options() { + $old_thumbnail_size = get_option( 'shop_catalog_image_size', array() ); + $old_single_size = get_option( 'shop_single_image_size', array() ); + + if ( ! empty( $old_thumbnail_size['width'] ) ) { + $width = absint( $old_thumbnail_size['width'] ); + $height = absint( $old_thumbnail_size['height'] ); + $hard_crop = ! empty( $old_thumbnail_size['crop'] ); + + if ( ! $width ) { + $width = 300; + } + + if ( ! $height ) { + $height = $width; + } + + update_option( 'woocommerce_thumbnail_image_width', $width ); + + // Calculate cropping mode from old image options. + if ( ! $hard_crop ) { + update_option( 'woocommerce_thumbnail_cropping', 'uncropped' ); + } elseif ( $width === $height ) { + update_option( 'woocommerce_thumbnail_cropping', '1:1' ); + } else { + $ratio = $width / $height; + $fraction = wc_decimal_to_fraction( $ratio ); + + if ( $fraction ) { + update_option( 'woocommerce_thumbnail_cropping', 'custom' ); + update_option( 'woocommerce_thumbnail_cropping_custom_width', $fraction[0] ); + update_option( 'woocommerce_thumbnail_cropping_custom_height', $fraction[1] ); + } + } + } + + // Single is uncropped. + if ( ! empty( $old_single_size['width'] ) ) { + update_option( 'woocommerce_single_image_width', absint( $old_single_size['width'] ) ); + } +} + +/** + * Migrate webhooks from post type to CRUD. + */ +function wc_update_330_webhooks() { + register_post_type( 'shop_webhook' ); + + // Map statuses from post_type to Webhooks CRUD. + $statuses = array( + 'publish' => 'active', + 'draft' => 'paused', + 'pending' => 'disabled', + ); + + $posts = get_posts( + array( + 'posts_per_page' => -1, + 'post_type' => 'shop_webhook', + 'post_status' => 'any', + ) + ); + + foreach ( $posts as $post ) { + $webhook = new WC_Webhook(); + $webhook->set_name( $post->post_title ); + $webhook->set_status( isset( $statuses[ $post->post_status ] ) ? $statuses[ $post->post_status ] : 'disabled' ); + $webhook->set_delivery_url( get_post_meta( $post->ID, '_delivery_url', true ) ); + $webhook->set_secret( get_post_meta( $post->ID, '_secret', true ) ); + $webhook->set_topic( get_post_meta( $post->ID, '_topic', true ) ); + $webhook->set_api_version( get_post_meta( $post->ID, '_api_version', true ) ); + $webhook->set_user_id( $post->post_author ); + $webhook->set_pending_delivery( false ); + $webhook->save(); + + wp_delete_post( $post->ID, true ); + } + + unregister_post_type( 'shop_webhook' ); +} + +/** + * Assign default cat to all products with no cats. + */ +function wc_update_330_set_default_product_cat() { + /* + * When a product category is deleted, we need to check + * if the product has no categories assigned. Then assign + * it a default category. + */ + wc_get_container()->get( AssignDefaultCategory::class )->maybe_assign_default_product_cat(); +} + +/** + * Update product stock status to use the new onbackorder status. + */ +function wc_update_330_product_stock_status() { + global $wpdb; + + if ( 'yes' !== get_option( 'woocommerce_manage_stock' ) ) { + return; + } + + $min_stock_amount = (int) get_option( 'woocommerce_notify_no_stock_amount', 0 ); + + // Get all products that have stock management enabled, stock less than or equal to min stock amount, and backorders enabled. + $post_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT t1.post_id FROM $wpdb->postmeta t1 + INNER JOIN $wpdb->postmeta t2 + ON t1.post_id = t2.post_id + AND t1.meta_key = '_manage_stock' AND t1.meta_value = 'yes' + AND t2.meta_key = '_stock' AND t2.meta_value <= %d + INNER JOIN $wpdb->postmeta t3 + ON t2.post_id = t3.post_id + AND t3.meta_key = '_backorders' AND ( t3.meta_value = 'yes' OR t3.meta_value = 'notify' )", + $min_stock_amount + ) + ); + + if ( empty( $post_ids ) ) { + return; + } + + $post_ids = array_map( 'absint', $post_ids ); + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + // Set the status to onbackorder for those products. + $wpdb->query( + "UPDATE $wpdb->postmeta + SET meta_value = 'onbackorder' + WHERE meta_key = '_stock_status' AND post_id IN ( " . implode( ',', $post_ids ) . ' )' + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared +} + +/** + * Clear addons page transients + */ +function wc_update_330_clear_transients() { + delete_transient( 'wc_addons_sections' ); + delete_transient( 'wc_addons_featured' ); +} + +/** + * Set PayPal's sandbox credentials. + */ +function wc_update_330_set_paypal_sandbox_credentials() { + + $paypal_settings = get_option( 'woocommerce_paypal_settings' ); + + if ( isset( $paypal_settings['testmode'] ) && 'yes' === $paypal_settings['testmode'] ) { + foreach ( array( 'api_username', 'api_password', 'api_signature' ) as $credential ) { + if ( ! empty( $paypal_settings[ $credential ] ) ) { + $paypal_settings[ 'sandbox_' . $credential ] = $paypal_settings[ $credential ]; + } + } + + update_option( 'woocommerce_paypal_settings', $paypal_settings ); + } +} + +/** + * Update DB Version. + */ +function wc_update_330_db_version() { + WC_Install::update_db_version( '3.3.0' ); +} + +/** + * Update state codes for Ireland and BD. + */ +function wc_update_340_states() { + $country_states = array( + 'IE' => array( + 'CK' => 'CO', + 'DN' => 'D', + 'GY' => 'G', + 'TY' => 'TA', + ), + 'BD' => array( + 'BAG' => 'BD-05', + 'BAN' => 'BD-01', + 'BAR' => 'BD-02', + 'BARI' => 'BD-06', + 'BHO' => 'BD-07', + 'BOG' => 'BD-03', + 'BRA' => 'BD-04', + 'CHA' => 'BD-09', + 'CHI' => 'BD-10', + 'CHU' => 'BD-12', + 'COX' => 'BD-11', + 'COM' => 'BD-08', + 'DHA' => 'BD-13', + 'DIN' => 'BD-14', + 'FAR' => 'BD-15', + 'FEN' => 'BD-16', + 'GAI' => 'BD-19', + 'GAZI' => 'BD-18', + 'GOP' => 'BD-17', + 'HAB' => 'BD-20', + 'JAM' => 'BD-21', + 'JES' => 'BD-22', + 'JHA' => 'BD-25', + 'JHE' => 'BD-23', + 'JOY' => 'BD-24', + 'KHA' => 'BD-29', + 'KHU' => 'BD-27', + 'KIS' => 'BD-26', + 'KUR' => 'BD-28', + 'KUS' => 'BD-30', + 'LAK' => 'BD-31', + 'LAL' => 'BD-32', + 'MAD' => 'BD-36', + 'MAG' => 'BD-37', + 'MAN' => 'BD-33', + 'MEH' => 'BD-39', + 'MOU' => 'BD-38', + 'MUN' => 'BD-35', + 'MYM' => 'BD-34', + 'NAO' => 'BD-48', + 'NAR' => 'BD-43', + 'NARG' => 'BD-40', + 'NARD' => 'BD-42', + 'NAT' => 'BD-44', + 'NAW' => 'BD-45', + 'NET' => 'BD-41', + 'NIL' => 'BD-46', + 'NOA' => 'BD-47', + 'PAB' => 'BD-49', + 'PAN' => 'BD-52', + 'PAT' => 'BD-51', + 'PIR' => 'BD-50', + 'RAJB' => 'BD-53', + 'RAJ' => 'BD-54', + 'RAN' => 'BD-56', + 'RANP' => 'BD-55', + 'SAT' => 'BD-58', + 'SHA' => 'BD-57', + 'SIR' => 'BD-59', + 'SUN' => 'BD-61', + 'SYL' => 'BD-60', + 'TAN' => 'BD-63', + 'THA' => 'BD-64', + ), + ); + + update_option( 'woocommerce_update_340_states', $country_states ); +} + +/** + * Update next state in the queue. + * + * @return bool True to run again, false if completed. + */ +function wc_update_340_state() { + global $wpdb; + + $country_states = array_filter( (array) get_option( 'woocommerce_update_340_states', array() ) ); + + if ( empty( $country_states ) ) { + return false; + } + + foreach ( $country_states as $country => $states ) { + foreach ( $states as $old => $new ) { + $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->postmeta + SET meta_value = %s + WHERE meta_key IN ( '_billing_state', '_shipping_state' ) + AND meta_value = %s", + $new, + $old + ) + ); + $wpdb->update( + "{$wpdb->prefix}woocommerce_shipping_zone_locations", + array( + 'location_code' => $country . ':' . $new, + ), + array( + 'location_code' => $country . ':' . $old, + ) + ); + $wpdb->update( + "{$wpdb->prefix}woocommerce_tax_rates", + array( + 'tax_rate_state' => strtoupper( $new ), + ), + array( + 'tax_rate_state' => strtoupper( $old ), + ) + ); + unset( $country_states[ $country ][ $old ] ); + + if ( empty( $country_states[ $country ] ) ) { + unset( $country_states[ $country ] ); + } + break 2; + } + } + + if ( ! empty( $country_states ) ) { + return update_option( 'woocommerce_update_340_states', $country_states ); + } + + delete_option( 'woocommerce_update_340_states' ); + + return false; +} + +/** + * Set last active prop for users. + */ +function wc_update_340_last_active() { + global $wpdb; + // @codingStandardsIgnoreStart. + $wpdb->query( + $wpdb->prepare( " + INSERT INTO {$wpdb->usermeta} (user_id, meta_key, meta_value) + SELECT DISTINCT users.ID, 'wc_last_active', %s + FROM {$wpdb->users} as users + LEFT OUTER JOIN {$wpdb->usermeta} AS usermeta ON users.ID = usermeta.user_id AND usermeta.meta_key = 'wc_last_active' + WHERE usermeta.meta_value IS NULL + ", + (string) strtotime( date( 'Y-m-d', current_time( 'timestamp', true ) ) ) + ) + ); + // @codingStandardsIgnoreEnd. +} + +/** + * Update DB Version. + */ +function wc_update_340_db_version() { + WC_Install::update_db_version( '3.4.0' ); +} + +/** + * Remove duplicate foreign keys + * + * @return void + */ +function wc_update_343_cleanup_foreign_keys() { + global $wpdb; + + $results = $wpdb->get_results( + "SELECT CONSTRAINT_NAME + FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}' + AND CONSTRAINT_NAME LIKE '%wc_download_log_ib%' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'" + ); + + if ( $results ) { + foreach ( $results as $fk ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY {$fk->CONSTRAINT_NAME}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + } +} + +/** + * Update DB version. + * + * @return void + */ +function wc_update_343_db_version() { + WC_Install::update_db_version( '3.4.3' ); +} + +/** + * Recreate user roles so existing users will get the new capabilities. + * + * @return void + */ +function wc_update_344_recreate_roles() { + WC_Install::remove_roles(); + WC_Install::create_roles(); +} + +/** + * Update DB version. + * + * @return void + */ +function wc_update_344_db_version() { + WC_Install::update_db_version( '3.4.4' ); +} + +/** + * Set the comment type to 'review' for product reviews that don't have a comment type. + */ +function wc_update_350_reviews_comment_type() { + global $wpdb; + + $wpdb->query( + "UPDATE {$wpdb->prefix}comments JOIN {$wpdb->prefix}posts ON {$wpdb->prefix}posts.ID = {$wpdb->prefix}comments.comment_post_ID AND ( {$wpdb->prefix}posts.post_type = 'product' OR {$wpdb->prefix}posts.post_type = 'product_variation' ) SET {$wpdb->prefix}comments.comment_type = 'review' WHERE {$wpdb->prefix}comments.comment_type = ''" + ); +} + +/** + * Update DB Version. + */ +function wc_update_350_db_version() { + WC_Install::update_db_version( '3.5.0' ); +} + +/** + * Drop the fk_wc_download_log_permission_id FK as we use a new one with the table and blog prefix for MS compatability. + * + * @return void + */ +function wc_update_352_drop_download_log_fk() { + global $wpdb; + $results = $wpdb->get_results( + "SELECT CONSTRAINT_NAME + FROM information_schema.TABLE_CONSTRAINTS + WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}' + AND CONSTRAINT_NAME = 'fk_wc_download_log_permission_id' + AND CONSTRAINT_TYPE = 'FOREIGN KEY' + AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'" + ); + + // We only need to drop the old key as WC_Install::create_tables() takes care of creating the new FK. + if ( $results ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_download_log DROP FOREIGN KEY fk_wc_download_log_permission_id" ); // phpcs:ignore WordPress.WP.PreparedSQL.NotPrepared + } +} + +/** + * Remove edit_user capabilities from shop managers and use "translated" capabilities instead. + * See wc_shop_manager_has_capability function. + */ +function wc_update_354_modify_shop_manager_caps() { + global $wp_roles; + + if ( ! class_exists( 'WP_Roles' ) ) { + return; + } + + if ( ! isset( $wp_roles ) ) { + $wp_roles = new WP_Roles(); // @codingStandardsIgnoreLine + } + + $wp_roles->remove_cap( 'shop_manager', 'edit_users' ); +} + +/** + * Update DB Version. + */ +function wc_update_354_db_version() { + WC_Install::update_db_version( '3.5.4' ); +} + +/** + * Update product lookup tables in bulk. + */ +function wc_update_360_product_lookup_tables() { + wc_update_product_lookup_tables(); +} + +/** + * Renames ordering meta to be consistent across taxonomies. + */ +function wc_update_360_term_meta() { + global $wpdb; + + $wpdb->query( "UPDATE {$wpdb->termmeta} SET meta_key = 'order' WHERE meta_key LIKE 'order_pa_%';" ); +} + +/** + * Add new user_order_remaining_expires to speed up user download permission fetching. + * + * @return void + */ +function wc_update_360_downloadable_product_permissions_index() { + global $wpdb; + + $index_exists = $wpdb->get_row( "SHOW INDEX FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions WHERE key_name = 'user_order_remaining_expires'" ); + + if ( is_null( $index_exists ) ) { + $wpdb->query( "ALTER TABLE {$wpdb->prefix}woocommerce_downloadable_product_permissions ADD INDEX user_order_remaining_expires (user_id,order_id,downloads_remaining,access_expires)" ); + } +} + +/** + * Update DB Version. + */ +function wc_update_360_db_version() { + WC_Install::update_db_version( '3.6.0' ); +} + +/** + * Put tax classes into a DB table. + * + * @return void + */ +function wc_update_370_tax_rate_classes() { + global $wpdb; + + $classes = array_map( 'trim', explode( "\n", get_option( 'woocommerce_tax_classes' ) ) ); + + if ( $classes ) { + foreach ( $classes as $class ) { + if ( empty( $class ) ) { + continue; + } + WC_Tax::create_tax_class( $class ); + } + } + delete_option( 'woocommerce_tax_classes' ); +} + +/** + * Update currency settings for 3.7.0 + * + * @return void + */ +function wc_update_370_mro_std_currency() { + global $wpdb; + + // Fix currency settings for MRU and STN currency. + $current_currency = get_option( 'woocommerce_currency' ); + + if ( 'MRO' === $current_currency ) { + update_option( 'woocommerce_currency', 'MRU' ); + } + + if ( 'STD' === $current_currency ) { + update_option( 'woocommerce_currency', 'STN' ); + } + + // Update MRU currency code. + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'MRU', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ), + array( + 'meta_key' => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => 'MRO', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ) + ); + + // Update STN currency code. + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 'STN', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ), + array( + 'meta_key' => '_order_currency', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => 'STD', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ) + ); +} + +/** + * Update DB Version. + */ +function wc_update_370_db_version() { + WC_Install::update_db_version( '3.7.0' ); +} + +/** + * We've moved the MaxMind database to a new location, as per the TOS' requirement that the database not + * be publicly accessible. + */ +function wc_update_390_move_maxmind_database() { + // Make sure to use all of the correct filters to pull the local database path. + $old_path = apply_filters( 'woocommerce_geolocation_local_database_path', WP_CONTENT_DIR . '/uploads/GeoLite2-Country.mmdb', 2 ); + + // Generate a prefix for the old file and store it in the integration as it would expect it. + $prefix = wp_generate_password( 32, false ); + update_option( 'woocommerce_maxmind_geolocation_settings', array( 'database_prefix' => $prefix ) ); + + // Generate the new path in the same way that the integration will. + $uploads_dir = wp_upload_dir(); + $new_path = trailingslashit( $uploads_dir['basedir'] ) . 'woocommerce_uploads/' . $prefix . '-GeoLite2-Country.mmdb'; + $new_path = apply_filters( 'woocommerce_geolocation_local_database_path', $new_path, 2 ); + $new_path = apply_filters( 'woocommerce_maxmind_geolocation_database_path', $new_path ); + + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + @rename( $old_path, $new_path ); +} + +/** + * So that we can best meet MaxMind's TOS, the geolocation database update cron should run once per 15 days. + */ +function wc_update_390_change_geolocation_database_update_cron() { + wp_clear_scheduled_hook( 'woocommerce_geoip_updater' ); + wp_schedule_event( time() + ( DAY_IN_SECONDS * 15 ), 'fifteendays', 'woocommerce_geoip_updater' ); +} + +/** + * Update DB version. + */ +function wc_update_390_db_version() { + WC_Install::update_db_version( '3.9.0' ); +} + +/** + * Increase column size + */ +function wc_update_400_increase_size_of_column() { + global $wpdb; + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `min_price` decimal(19,4) NULL default NULL" ); + $wpdb->query( "ALTER TABLE {$wpdb->prefix}wc_product_meta_lookup MODIFY COLUMN `max_price` decimal(19,4) NULL default NULL" ); +} + +/** + * Reset ActionScheduler migration status. Needs AS >= 3.0 shipped with WC >= 4.0. + */ +function wc_update_400_reset_action_scheduler_migration_status() { + if ( + class_exists( 'ActionScheduler_DataController' ) && + method_exists( 'ActionScheduler_DataController', 'mark_migration_incomplete' ) + ) { + \ActionScheduler_DataController::mark_migration_incomplete(); + } +} + +/** + * Update DB version. + */ +function wc_update_400_db_version() { + WC_Install::update_db_version( '4.0.0' ); +} + +/** + * Register attributes as terms for variable products, in increments of 100 products. + * + * This migration was added to support a new mechanism to improve the filtering of + * variable products by attribute (https://github.com/woocommerce/woocommerce/pull/26260), + * however that mechanism was later reverted (https://github.com/woocommerce/woocommerce/pull/27625) + * due to numerous issues found. Thus the migration is no longer needed. + * + * @return bool true if the migration needs to be run again. + */ +function wc_update_440_insert_attribute_terms_for_variable_products() { + return false; +} + +/** + * Update DB version. + */ +function wc_update_440_db_version() { + WC_Install::update_db_version( '4.4.0' ); +} + +/** + * Update DB version to 4.5.0. + */ +function wc_update_450_db_version() { + WC_Install::update_db_version( '4.5.0' ); +} + +/** + * Sanitize all coupons code. + * + * @return bool True to run again, false if completed. + */ +function wc_update_450_sanitize_coupons_code() { + global $wpdb; + + $coupon_id = 0; + $last_coupon_id = get_option( 'woocommerce_update_450_last_coupon_id', '0' ); + + $coupons = $wpdb->get_results( + $wpdb->prepare( + "SELECT ID, post_title FROM $wpdb->posts WHERE ID > %d AND post_type = 'shop_coupon' LIMIT 10", + $last_coupon_id + ), + ARRAY_A + ); + + if ( empty( $coupons ) ) { + delete_option( 'woocommerce_update_450_last_coupon_id' ); + return false; + } + + foreach ( $coupons as $key => $data ) { + $coupon_id = intval( $data['ID'] ); + $code = trim( wp_filter_kses( $data['post_title'] ) ); + + if ( ! empty( $code ) && $data['post_title'] !== $code ) { + $wpdb->update( + $wpdb->posts, + array( + 'post_title' => $code, + ), + array( + 'ID' => $coupon_id, + ), + array( + '%s', + ), + array( + '%d', + ) + ); + + // Clean cache. + clean_post_cache( $coupon_id ); + wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $data['post_title'], 'coupons' ); + } + } + + // Start the run again. + if ( $coupon_id ) { + return update_option( 'woocommerce_update_450_last_coupon_id', $coupon_id ); + } + + delete_option( 'woocommerce_update_450_last_coupon_id' ); + return false; +} + +/** + * Fixes product review count that might have been incorrect. + * + * See @link https://github.com/woocommerce/woocommerce/issues/27688. + */ +function wc_update_500_fix_product_review_count() { + global $wpdb; + + $product_id = 0; + $last_product_id = get_option( 'woocommerce_update_500_last_product_id', '0' ); + + $products_data = $wpdb->get_results( + $wpdb->prepare( + " + SELECT post_id, meta_value + FROM $wpdb->postmeta + JOIN $wpdb->posts + ON $wpdb->postmeta.post_id = $wpdb->posts.ID + WHERE + post_type = 'product' + AND post_status = 'publish' + AND post_id > %d + AND meta_key = '_wc_review_count' + ORDER BY post_id ASC + LIMIT 10 + ", + $last_product_id + ), + ARRAY_A + ); + + if ( empty( $products_data ) ) { + delete_option( 'woocommerce_update_500_last_product_id' ); + return false; + } + + $product_ids_to_check = array_column( $products_data, 'post_id' ); + $actual_review_counts = WC_Comments::get_review_counts_for_product_ids( $product_ids_to_check ); + + foreach ( $products_data as $product_data ) { + $product_id = intval( $product_data['post_id'] ); + $current_review_count = intval( $product_data['meta_value'] ); + + if ( intval( $actual_review_counts[ $product_id ] ) !== $current_review_count ) { + WC_Comments::clear_transients( $product_id ); + } + } + + // Start the run again. + if ( $product_id ) { + return update_option( 'woocommerce_update_500_last_product_id', $product_id ); + } + + delete_option( 'woocommerce_update_500_last_product_id' ); + return false; +} + +/** + * Update DB version to 5.0.0. + */ +function wc_update_500_db_version() { + WC_Install::update_db_version( '5.0.0' ); +} + +/** + * Creates the refund and returns policy page. + * + * See @link https://github.com/woocommerce/woocommerce/issues/29235. + */ +function wc_update_560_create_refund_returns_page() { + /** + * Filter on the pages created to return what we expect. + * + * @param array $pages The default WC pages. + */ + function filter_created_pages( $pages ) { + $page_to_create = array( 'refund_returns' ); + + return array_intersect_key( $pages, array_flip( $page_to_create ) ); + } + + add_filter( 'woocommerce_create_pages', 'filter_created_pages' ); + + WC_Install::create_pages(); + + remove_filter( 'woocommerce_create_pages', 'filter_created_pages' ); +} + +/** + * Update DB version to 5.6.0. + */ +function wc_update_560_db_version() { + WC_Install::update_db_version( '5.6.0' ); +} diff --git a/includes/wc-user-functions.php b/includes/wc-user-functions.php new file mode 100644 index 0000000..0c30c4f --- /dev/null +++ b/includes/wc-user-functions.php @@ -0,0 +1,926 @@ +Please log in.', 'woocommerce' ), $email ) ); + } + + if ( 'yes' === get_option( 'woocommerce_registration_generate_username', 'yes' ) && empty( $username ) ) { + $username = wc_create_new_customer_username( $email, $args ); + } + + $username = sanitize_user( $username ); + + if ( empty( $username ) || ! validate_username( $username ) ) { + return new WP_Error( 'registration-error-invalid-username', __( 'Please enter a valid account username.', 'woocommerce' ) ); + } + + if ( username_exists( $username ) ) { + return new WP_Error( 'registration-error-username-exists', __( 'An account is already registered with that username. Please choose another.', 'woocommerce' ) ); + } + + // Handle password creation. + $password_generated = false; + if ( 'yes' === get_option( 'woocommerce_registration_generate_password' ) && empty( $password ) ) { + $password = wp_generate_password(); + $password_generated = true; + } + + if ( empty( $password ) ) { + return new WP_Error( 'registration-error-missing-password', __( 'Please enter an account password.', 'woocommerce' ) ); + } + + // Use WP_Error to handle registration errors. + $errors = new WP_Error(); + + do_action( 'woocommerce_register_post', $username, $email, $errors ); + + $errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $email ); + + if ( $errors->get_error_code() ) { + return $errors; + } + + $new_customer_data = apply_filters( + 'woocommerce_new_customer_data', + array_merge( + $args, + array( + 'user_login' => $username, + 'user_pass' => $password, + 'user_email' => $email, + 'role' => 'customer', + ) + ) + ); + + $customer_id = wp_insert_user( $new_customer_data ); + + if ( is_wp_error( $customer_id ) ) { + return $customer_id; + } + + do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated ); + + return $customer_id; + } +} + +/** + * Create a unique username for a new customer. + * + * @since 3.6.0 + * @param string $email New customer email address. + * @param array $new_user_args Array of new user args, maybe including first and last names. + * @param string $suffix Append string to username to make it unique. + * @return string Generated username. + */ +function wc_create_new_customer_username( $email, $new_user_args = array(), $suffix = '' ) { + $username_parts = array(); + + if ( isset( $new_user_args['first_name'] ) ) { + $username_parts[] = sanitize_user( $new_user_args['first_name'], true ); + } + + if ( isset( $new_user_args['last_name'] ) ) { + $username_parts[] = sanitize_user( $new_user_args['last_name'], true ); + } + + // Remove empty parts. + $username_parts = array_filter( $username_parts ); + + // If there are no parts, e.g. name had unicode chars, or was not provided, fallback to email. + if ( empty( $username_parts ) ) { + $email_parts = explode( '@', $email ); + $email_username = $email_parts[0]; + + // Exclude common prefixes. + if ( in_array( + $email_username, + array( + 'sales', + 'hello', + 'mail', + 'contact', + 'info', + ), + true + ) ) { + // Get the domain part. + $email_username = $email_parts[1]; + } + + $username_parts[] = sanitize_user( $email_username, true ); + } + + $username = wc_strtolower( implode( '.', $username_parts ) ); + + if ( $suffix ) { + $username .= $suffix; + } + + /** + * WordPress 4.4 - filters the list of blocked usernames. + * + * @since 3.7.0 + * @param array $usernames Array of blocked usernames. + */ + $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); + + // Stop illegal logins and generate a new random username. + if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ), true ) ) { + $new_args = array(); + + /** + * Filter generated customer username. + * + * @since 3.7.0 + * @param string $username Generated username. + * @param string $email New customer email address. + * @param array $new_user_args Array of new user args, maybe including first and last names. + * @param string $suffix Append string to username to make it unique. + */ + $new_args['first_name'] = apply_filters( + 'woocommerce_generated_customer_username', + 'woo_user_' . zeroise( wp_rand( 0, 9999 ), 4 ), + $email, + $new_user_args, + $suffix + ); + + return wc_create_new_customer_username( $email, $new_args, $suffix ); + } + + if ( username_exists( $username ) ) { + // Generate something unique to append to the username in case of a conflict with another user. + $suffix = '-' . zeroise( wp_rand( 0, 9999 ), 4 ); + return wc_create_new_customer_username( $email, $new_user_args, $suffix ); + } + + /** + * Filter new customer username. + * + * @since 3.7.0 + * @param string $username Customer username. + * @param string $email New customer email address. + * @param array $new_user_args Array of new user args, maybe including first and last names. + * @param string $suffix Append string to username to make it unique. + */ + return apply_filters( 'woocommerce_new_customer_username', $username, $email, $new_user_args, $suffix ); +} + +/** + * Login a customer (set auth cookie and set global user object). + * + * @param int $customer_id Customer ID. + */ +function wc_set_customer_auth_cookie( $customer_id ) { + wp_set_current_user( $customer_id ); + wp_set_auth_cookie( $customer_id, true ); + + // Update session. + WC()->session->init_session_cookie(); +} + +/** + * Get past orders (by email) and update them. + * + * @param int $customer_id Customer ID. + * @return int + */ +function wc_update_new_customer_past_orders( $customer_id ) { + $linked = 0; + $complete = 0; + $customer = get_user_by( 'id', absint( $customer_id ) ); + $customer_orders = wc_get_orders( + array( + 'limit' => -1, + 'customer' => array( array( 0, $customer->user_email ) ), + 'return' => 'ids', + ) + ); + + if ( ! empty( $customer_orders ) ) { + foreach ( $customer_orders as $order_id ) { + $order = wc_get_order( $order_id ); + if ( ! $order ) { + continue; + } + + $order->set_customer_id( $customer->ID ); + $order->save(); + + if ( $order->has_downloadable_item() ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + $data_store->delete_by_order_id( $order->get_id() ); + wc_downloadable_product_permissions( $order->get_id(), true ); + } + + do_action( 'woocommerce_update_new_customer_past_order', $order_id, $customer ); + + if ( get_post_status( $order_id ) === 'wc-completed' ) { + $complete++; + } + + $linked++; + } + } + + if ( $complete ) { + update_user_meta( $customer_id, 'paying_customer', 1 ); + update_user_meta( $customer_id, '_order_count', '' ); + update_user_meta( $customer_id, '_money_spent', '' ); + delete_user_meta( $customer_id, '_last_order' ); + } + + return $linked; +} + +/** + * Order payment completed - This is a paying customer. + * + * @param int $order_id Order ID. + */ +function wc_paying_customer( $order_id ) { + $order = wc_get_order( $order_id ); + $customer_id = $order->get_customer_id(); + + if ( $customer_id > 0 && 'shop_order_refund' !== $order->get_type() ) { + $customer = new WC_Customer( $customer_id ); + + if ( ! $customer->get_is_paying_customer() ) { + $customer->set_is_paying_customer( true ); + $customer->save(); + } + } +} +add_action( 'woocommerce_payment_complete', 'wc_paying_customer' ); +add_action( 'woocommerce_order_status_completed', 'wc_paying_customer' ); + +/** + * Checks if a user (by email or ID or both) has bought an item. + * + * @param string $customer_email Customer email to check. + * @param int $user_id User ID to check. + * @param int $product_id Product ID to check. + * @return bool + */ +function wc_customer_bought_product( $customer_email, $user_id, $product_id ) { + global $wpdb; + + $result = apply_filters( 'woocommerce_pre_customer_bought_product', null, $customer_email, $user_id, $product_id ); + + if ( null !== $result ) { + return $result; + } + + $transient_name = 'wc_customer_bought_product_' . md5( $customer_email . $user_id ); + $transient_version = WC_Cache_Helper::get_transient_version( 'orders' ); + $transient_value = get_transient( $transient_name ); + + if ( isset( $transient_value['value'], $transient_value['version'] ) && $transient_value['version'] === $transient_version ) { + $result = $transient_value['value']; + } else { + $customer_data = array( $user_id ); + + if ( $user_id ) { + $user = get_user_by( 'id', $user_id ); + + if ( isset( $user->user_email ) ) { + $customer_data[] = $user->user_email; + } + } + + if ( is_email( $customer_email ) ) { + $customer_data[] = $customer_email; + } + + $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); + $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + + if ( count( $customer_data ) === 0 ) { + return false; + } + + $result = $wpdb->get_col( + " + SELECT im.meta_value FROM {$wpdb->posts} AS p + INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id + INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id + WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) + AND pm.meta_key IN ( '_billing_email', '_customer_user' ) + AND im.meta_key IN ( '_product_id', '_variation_id' ) + AND im.meta_value != 0 + AND pm.meta_value IN ( '" . implode( "','", $customer_data ) . "' ) + " + ); // WPCS: unprepared SQL ok. + $result = array_map( 'absint', $result ); + + $transient_value = array( + 'version' => $transient_version, + 'value' => $result, + ); + + set_transient( $transient_name, $transient_value, DAY_IN_SECONDS * 30 ); + } + return in_array( absint( $product_id ), $result, true ); +} + +/** + * Checks if the current user has a role. + * + * @param string $role The role. + * @return bool + */ +function wc_current_user_has_role( $role ) { + return wc_user_has_role( wp_get_current_user(), $role ); +} + +/** + * Checks if a user has a role. + * + * @param int|\WP_User $user The user. + * @param string $role The role. + * @return bool + */ +function wc_user_has_role( $user, $role ) { + if ( ! is_object( $user ) ) { + $user = get_userdata( $user ); + } + + if ( ! $user || ! $user->exists() ) { + return false; + } + + return in_array( $role, $user->roles, true ); +} + +/** + * Checks if a user has a certain capability. + * + * @param array $allcaps All capabilities. + * @param array $caps Capabilities. + * @param array $args Arguments. + * + * @return array The filtered array of all capabilities. + */ +function wc_customer_has_capability( $allcaps, $caps, $args ) { + if ( isset( $caps[0] ) ) { + switch ( $caps[0] ) { + case 'view_order': + $user_id = intval( $args[1] ); + $order = wc_get_order( $args[2] ); + + if ( $order && $user_id === $order->get_user_id() ) { + $allcaps['view_order'] = true; + } + break; + case 'pay_for_order': + $user_id = intval( $args[1] ); + $order_id = isset( $args[2] ) ? $args[2] : null; + + // When no order ID, we assume it's a new order + // and thus, customer can pay for it. + if ( ! $order_id ) { + $allcaps['pay_for_order'] = true; + break; + } + + $order = wc_get_order( $order_id ); + + if ( $order && ( $user_id === $order->get_user_id() || ! $order->get_user_id() ) ) { + $allcaps['pay_for_order'] = true; + } + break; + case 'order_again': + $user_id = intval( $args[1] ); + $order = wc_get_order( $args[2] ); + + if ( $order && $user_id === $order->get_user_id() ) { + $allcaps['order_again'] = true; + } + break; + case 'cancel_order': + $user_id = intval( $args[1] ); + $order = wc_get_order( $args[2] ); + + if ( $order && $user_id === $order->get_user_id() ) { + $allcaps['cancel_order'] = true; + } + break; + case 'download_file': + $user_id = intval( $args[1] ); + $download = $args[2]; + + if ( $download && $user_id === $download->get_user_id() ) { + $allcaps['download_file'] = true; + } + break; + } + } + return $allcaps; +} +add_filter( 'user_has_cap', 'wc_customer_has_capability', 10, 3 ); + +/** + * Safe way of allowing shop managers restricted capabilities that will remove + * access to the capabilities if WooCommerce is deactivated. + * + * @since 3.5.4 + * @param bool[] $allcaps Array of key/value pairs where keys represent a capability name and boolean values + * represent whether the user has that capability. + * @param string[] $caps Required primitive capabilities for the requested capability. + * @param array $args Arguments that accompany the requested capability check. + * @param WP_User $user The user object. + * @return bool[] + */ +function wc_shop_manager_has_capability( $allcaps, $caps, $args, $user ) { + + if ( wc_user_has_role( $user, 'shop_manager' ) ) { + // @see wc_modify_map_meta_cap, which limits editing to customers. + $allcaps['edit_users'] = true; + } + + return $allcaps; +} +add_filter( 'user_has_cap', 'wc_shop_manager_has_capability', 10, 4 ); + +/** + * Modify the list of editable roles to prevent non-admin adding admin users. + * + * @param array $roles Roles. + * @return array + */ +function wc_modify_editable_roles( $roles ) { + if ( is_multisite() && is_super_admin() ) { + return $roles; + } + if ( ! wc_current_user_has_role( 'administrator' ) ) { + unset( $roles['administrator'] ); + + if ( wc_current_user_has_role( 'shop_manager' ) ) { + $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); + return array_intersect_key( $roles, array_flip( $shop_manager_editable_roles ) ); + } + } + + return $roles; +} +add_filter( 'editable_roles', 'wc_modify_editable_roles' ); + +/** + * Modify capabilities to prevent non-admin users editing admin users. + * + * $args[0] will be the user being edited in this case. + * + * @param array $caps Array of caps. + * @param string $cap Name of the cap we are checking. + * @param int $user_id ID of the user being checked against. + * @param array $args Arguments. + * @return array + */ +function wc_modify_map_meta_cap( $caps, $cap, $user_id, $args ) { + if ( is_multisite() && is_super_admin() ) { + return $caps; + } + switch ( $cap ) { + case 'edit_user': + case 'remove_user': + case 'promote_user': + case 'delete_user': + if ( ! isset( $args[0] ) || $args[0] === $user_id ) { + break; + } else { + if ( ! wc_current_user_has_role( 'administrator' ) ) { + if ( wc_user_has_role( $args[0], 'administrator' ) ) { + $caps[] = 'do_not_allow'; + } elseif ( wc_current_user_has_role( 'shop_manager' ) ) { + // Shop managers can only edit customer info. + $userdata = get_userdata( $args[0] ); + $shop_manager_editable_roles = apply_filters( 'woocommerce_shop_manager_editable_roles', array( 'customer' ) ); + if ( property_exists( $userdata, 'roles' ) && ! empty( $userdata->roles ) && ! array_intersect( $userdata->roles, $shop_manager_editable_roles ) ) { + $caps[] = 'do_not_allow'; + } + } + } + } + break; + } + return $caps; +} +add_filter( 'map_meta_cap', 'wc_modify_map_meta_cap', 10, 4 ); + +/** + * Get customer download permissions from the database. + * + * @param int $customer_id Customer/User ID. + * @return array + */ +function wc_get_customer_download_permissions( $customer_id ) { + $data_store = WC_Data_Store::load( 'customer-download' ); + return apply_filters( 'woocommerce_permission_list', $data_store->get_downloads_for_customer( $customer_id ), $customer_id ); +} + +/** + * Get customer available downloads. + * + * @param int $customer_id Customer/User ID. + * @return array + */ +function wc_get_customer_available_downloads( $customer_id ) { + $downloads = array(); + $_product = null; + $order = null; + $file_number = 0; + + // Get results from valid orders only. + $results = wc_get_customer_download_permissions( $customer_id ); + + if ( $results ) { + foreach ( $results as $result ) { + $order_id = intval( $result->order_id ); + + if ( ! $order || $order->get_id() !== $order_id ) { + // New order. + $order = wc_get_order( $order_id ); + $_product = null; + } + + // Make sure the order exists for this download. + if ( ! $order ) { + continue; + } + + // Check if downloads are permitted. + if ( ! $order->is_download_permitted() ) { + continue; + } + + $product_id = intval( $result->product_id ); + + if ( ! $_product || $_product->get_id() !== $product_id ) { + // New product. + $file_number = 0; + $_product = wc_get_product( $product_id ); + } + + // Check product exists and has the file. + if ( ! $_product || ! $_product->exists() || ! $_product->has_file( $result->download_id ) ) { + continue; + } + + $download_file = $_product->get_file( $result->download_id ); + + // Download name will be 'Product Name' for products with a single downloadable file, and 'Product Name - File X' for products with multiple files. + $download_name = apply_filters( + 'woocommerce_downloadable_product_name', + $download_file['name'], + $_product, + $result->download_id, + $file_number + ); + + $downloads[] = array( + 'download_url' => add_query_arg( + array( + 'download_file' => $product_id, + 'order' => $result->order_key, + 'email' => rawurlencode( $result->user_email ), + 'key' => $result->download_id, + ), + home_url( '/' ) + ), + 'download_id' => $result->download_id, + 'product_id' => $_product->get_id(), + 'product_name' => $_product->get_name(), + 'product_url' => $_product->is_visible() ? $_product->get_permalink() : '', // Since 3.3.0. + 'download_name' => $download_name, + 'order_id' => $order->get_id(), + 'order_key' => $order->get_order_key(), + 'downloads_remaining' => $result->downloads_remaining, + 'access_expires' => $result->access_expires, + 'file' => array( + 'name' => $download_file->get_name(), + 'file' => $download_file->get_file(), + ), + ); + + $file_number++; + } + } + + return apply_filters( 'woocommerce_customer_available_downloads', $downloads, $customer_id ); +} + +/** + * Get total spent by customer. + * + * @param int $user_id User ID. + * @return string + */ +function wc_get_customer_total_spent( $user_id ) { + $customer = new WC_Customer( $user_id ); + return $customer->get_total_spent(); +} + +/** + * Get total orders by customer. + * + * @param int $user_id User ID. + * @return int + */ +function wc_get_customer_order_count( $user_id ) { + $customer = new WC_Customer( $user_id ); + return $customer->get_order_count(); +} + +/** + * Reset _customer_user on orders when a user is deleted. + * + * @param int $user_id User ID. + */ +function wc_reset_order_customer_id_on_deleted_user( $user_id ) { + global $wpdb; + + $wpdb->update( + $wpdb->postmeta, + array( + 'meta_value' => 0, + ), + array( + 'meta_key' => '_customer_user', + 'meta_value' => $user_id, + ) + ); // WPCS: slow query ok. +} + +add_action( 'deleted_user', 'wc_reset_order_customer_id_on_deleted_user' ); + +/** + * Get review verification status. + * + * @param int $comment_id Comment ID. + * @return bool + */ +function wc_review_is_from_verified_owner( $comment_id ) { + $verified = get_comment_meta( $comment_id, 'verified', true ); + return '' === $verified ? WC_Comments::add_comment_purchase_verification( $comment_id ) : (bool) $verified; +} + +/** + * Disable author archives for customers. + * + * @since 2.5.0 + */ +function wc_disable_author_archives_for_customers() { + global $author; + + if ( is_author() ) { + $user = get_user_by( 'id', $author ); + + if ( user_can( $user, 'customer' ) && ! user_can( $user, 'edit_posts' ) ) { + wp_safe_redirect( wc_get_page_permalink( 'shop' ) ); + exit; + } + } +} + +add_action( 'template_redirect', 'wc_disable_author_archives_for_customers' ); + +/** + * Hooks into the `profile_update` hook to set the user last updated timestamp. + * + * @since 2.6.0 + * @param int $user_id The user that was updated. + * @param array $old The profile fields pre-change. + */ +function wc_update_profile_last_update_time( $user_id, $old ) { + wc_set_user_last_update_time( $user_id ); +} + +add_action( 'profile_update', 'wc_update_profile_last_update_time', 10, 2 ); + +/** + * Hooks into the update user meta function to set the user last updated timestamp. + * + * @since 2.6.0 + * @param int $meta_id ID of the meta object that was changed. + * @param int $user_id The user that was updated. + * @param string $meta_key Name of the meta key that was changed. + * @param string $_meta_value Value of the meta that was changed. + */ +function wc_meta_update_last_update_time( $meta_id, $user_id, $meta_key, $_meta_value ) { + $keys_to_track = apply_filters( 'woocommerce_user_last_update_fields', array( 'first_name', 'last_name' ) ); + + $update_time = in_array( $meta_key, $keys_to_track, true ) ? true : false; + $update_time = 'billing_' === substr( $meta_key, 0, 8 ) ? true : $update_time; + $update_time = 'shipping_' === substr( $meta_key, 0, 9 ) ? true : $update_time; + + if ( $update_time ) { + wc_set_user_last_update_time( $user_id ); + } +} + +add_action( 'update_user_meta', 'wc_meta_update_last_update_time', 10, 4 ); + +/** + * Sets a user's "last update" time to the current timestamp. + * + * @since 2.6.0 + * @param int $user_id The user to set a timestamp for. + */ +function wc_set_user_last_update_time( $user_id ) { + update_user_meta( $user_id, 'last_update', gmdate( 'U' ) ); +} + +/** + * Get customer saved payment methods list. + * + * @since 2.6.0 + * @param int $customer_id Customer ID. + * @return array + */ +function wc_get_customer_saved_methods_list( $customer_id ) { + return apply_filters( 'woocommerce_saved_payment_methods_list', array(), $customer_id ); +} + +/** + * Get info about customer's last order. + * + * @since 2.6.0 + * @param int $customer_id Customer ID. + * @return WC_Order|bool Order object if successful or false. + */ +function wc_get_customer_last_order( $customer_id ) { + $customer = new WC_Customer( $customer_id ); + + return $customer->get_last_order(); +} + +/** + * Add support for searching by display_name. + * + * @since 3.2.0 + * @param array $search_columns Column names. + * @return array + */ +function wc_user_search_columns( $search_columns ) { + $search_columns[] = 'display_name'; + return $search_columns; +} +add_filter( 'user_search_columns', 'wc_user_search_columns' ); + +/** + * When a user is deleted in WordPress, delete corresponding WooCommerce data. + * + * @param int $user_id User ID being deleted. + */ +function wc_delete_user_data( $user_id ) { + global $wpdb; + + // Clean up sessions. + $wpdb->delete( + $wpdb->prefix . 'woocommerce_sessions', + array( + 'session_key' => $user_id, + ) + ); + + // Revoke API keys. + $wpdb->delete( + $wpdb->prefix . 'woocommerce_api_keys', + array( + 'user_id' => $user_id, + ) + ); + + // Clean up payment tokens. + $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id ); + + foreach ( $payment_tokens as $payment_token ) { + $payment_token->delete(); + } +} +add_action( 'delete_user', 'wc_delete_user_data' ); + +/** + * Store user agents. Used for tracker. + * + * @since 3.0.0 + * @param string $user_login User login. + * @param int|object $user User. + */ +function wc_maybe_store_user_agent( $user_login, $user ) { + if ( 'yes' === get_option( 'woocommerce_allow_tracking', 'no' ) && user_can( $user, 'manage_woocommerce' ) ) { + $admin_user_agents = array_filter( (array) get_option( 'woocommerce_tracker_ua', array() ) ); + $admin_user_agents[] = wc_get_user_agent(); + update_option( 'woocommerce_tracker_ua', array_unique( $admin_user_agents ) ); + } +} +add_action( 'wp_login', 'wc_maybe_store_user_agent', 10, 2 ); + +/** + * Update logic triggered on login. + * + * @since 3.4.0 + * @param string $user_login User login. + * @param object $user User. + */ +function wc_user_logged_in( $user_login, $user ) { + wc_update_user_last_active( $user->ID ); + update_user_meta( $user->ID, '_woocommerce_load_saved_cart_after_login', 1 ); +} +add_action( 'wp_login', 'wc_user_logged_in', 10, 2 ); + +/** + * Update when the user was last active. + * + * @since 3.4.0 + */ +function wc_current_user_is_active() { + if ( ! is_user_logged_in() ) { + return; + } + wc_update_user_last_active( get_current_user_id() ); +} +add_action( 'wp', 'wc_current_user_is_active', 10 ); + +/** + * Set the user last active timestamp to now. + * + * @since 3.4.0 + * @param int $user_id User ID to mark active. + */ +function wc_update_user_last_active( $user_id ) { + if ( ! $user_id ) { + return; + } + update_user_meta( $user_id, 'wc_last_active', (string) strtotime( gmdate( 'Y-m-d', time() ) ) ); +} + +/** + * Translate WC roles using the woocommerce textdomain. + * + * @since 3.7.0 + * @param string $translation Translated text. + * @param string $text Text to translate. + * @param string $context Context information for the translators. + * @param string $domain Text domain. Unique identifier for retrieving translated strings. + * @return string + */ +function wc_translate_user_roles( $translation, $text, $context, $domain ) { + // translate_user_role() only accepts a second parameter starting in WP 5.2. + if ( version_compare( get_bloginfo( 'version' ), '5.2', '<' ) ) { + return $translation; + } + + if ( 'User role' === $context && 'default' === $domain && in_array( $text, array( 'Shop manager', 'Customer' ), true ) ) { + return translate_user_role( $text, 'woocommerce' ); + } + + return $translation; +} +add_filter( 'gettext_with_context', 'wc_translate_user_roles', 10, 4 ); diff --git a/includes/wc-webhook-functions.php b/includes/wc-webhook-functions.php new file mode 100644 index 0000000..bc1d159 --- /dev/null +++ b/includes/wc-webhook-functions.php @@ -0,0 +1,204 @@ + $data['webhook']->get_id(), + 'arg' => $data['arg'], + ); + + $next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); + + // Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0. + if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) { + WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' ); + } + } else { + // Deliver immediately. + $data['webhook']->deliver( $data['arg'] ); + } + } +} +add_action( 'shutdown', 'wc_webhook_execute_queue' ); + +/** + * Process webhook delivery. + * + * @since 3.3.0 + * @param WC_Webhook $webhook Webhook instance. + * @param array $arg Delivery arguments. + */ +function wc_webhook_process_delivery( $webhook, $arg ) { + // We need to queue the webhook so that it can be ran after the request has finished processing. + global $wc_queued_webhooks; + if ( ! isset( $wc_queued_webhooks ) ) { + $wc_queued_webhooks = array(); + } + $wc_queued_webhooks[] = array( + 'webhook' => $webhook, + 'arg' => $arg, + ); +} +add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery', 10, 2 ); + +/** + * Wrapper function to execute the `woocommerce_deliver_webhook_async` cron. + * hook, see WC_Webhook::process(). + * + * @since 2.2.0 + * @param int $webhook_id Webhook ID to deliver. + * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. + * @param mixed $arg Hook argument. + */ +function wc_deliver_webhook_async( $webhook_id, $arg ) { + $webhook = new WC_Webhook( $webhook_id ); + $webhook->deliver( $arg ); +} +add_action( 'woocommerce_deliver_webhook_async', 'wc_deliver_webhook_async', 10, 2 ); + +/** + * Check if the given topic is a valid webhook topic, a topic is valid if: + * + * + starts with `action.woocommerce_` or `action.wc_`. + * + it has a valid resource & event. + * + * @since 2.2.0 + * @param string $topic Webhook topic. + * @return bool + */ +function wc_is_webhook_valid_topic( $topic ) { + $invalid_topics = array( + 'action.woocommerce_login_credentials', + 'action.woocommerce_product_csv_importer_check_import_file_path', + 'action.woocommerce_webhook_should_deliver', + ); + + if ( in_array( $topic, $invalid_topics, true ) ) { + return false; + } + + // Custom topics are prefixed with woocommerce_ or wc_ are valid. + if ( 0 === strpos( $topic, 'action.woocommerce_' ) || 0 === strpos( $topic, 'action.wc_' ) ) { + return true; + } + + $data = explode( '.', $topic ); + + if ( ! isset( $data[0] ) || ! isset( $data[1] ) ) { + return false; + } + + $valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) ); + $valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored' ) ); + + if ( in_array( $data[0], $valid_resources, true ) && in_array( $data[1], $valid_events, true ) ) { + return true; + } + + return false; +} + +/** + * Check if given status is a valid webhook status. + * + * @since 3.5.3 + * @param string $status Status to check. + * @return bool + */ +function wc_is_webhook_valid_status( $status ) { + return in_array( $status, array_keys( wc_get_webhook_statuses() ), true ); +} + +/** + * Get Webhook statuses. + * + * @since 2.3.0 + * @return array + */ +function wc_get_webhook_statuses() { + return apply_filters( + 'woocommerce_webhook_statuses', + array( + 'active' => __( 'Active', 'woocommerce' ), + 'paused' => __( 'Paused', 'woocommerce' ), + 'disabled' => __( 'Disabled', 'woocommerce' ), + ) + ); +} + +/** + * Load webhooks. + * + * @since 3.3.0 + * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. + * @param string $status Optional - status to filter results by. Must be a key in return value of @see wc_get_webhook_statuses(). @since 3.5.0. + * @param null|int $limit Limit number of webhooks loaded. @since 3.6.0. + * @return bool + */ +function wc_load_webhooks( $status = '', $limit = null ) { + $data_store = WC_Data_Store::load( 'webhook' ); + $webhooks = $data_store->get_webhooks_ids( $status ); + $loaded = 0; + + foreach ( $webhooks as $webhook_id ) { + $webhook = new WC_Webhook( $webhook_id ); + $webhook->enqueue(); + $loaded ++; + + if ( ! is_null( $limit ) && $loaded >= $limit ) { + break; + } + } + + return 0 < $loaded; +} + +/** + * Get webhook. + * + * @param int|WC_Webhook $id Webhook ID or object. + * @throws Exception If webhook cannot be read/found and $data parameter of WC_Webhook class constructor is set. + * @return WC_Webhook|null + */ +function wc_get_webhook( $id ) { + $webhook = new WC_Webhook( $id ); + + return 0 !== $webhook->get_id() ? $webhook : null; +} + +/** + * Get webhoook REST API versions. + * + * @since 3.5.1 + * @return array + */ +function wc_get_webhook_rest_api_versions() { + return array( + 'wp_api_v1', + 'wp_api_v2', + 'wp_api_v3', + ); +} diff --git a/includes/wc-widget-functions.php b/includes/wc-widget-functions.php new file mode 100644 index 0000000..49da8f7 --- /dev/null +++ b/includes/wc-widget-functions.php @@ -0,0 +1,52 @@ + 400 ) ); + } + + return true; + } + + /** + * Validates if WP CRON is enabled. + * + * @since 3.8.0 + * @return bool + */ + private static function met_wp_cron_requirement() { + return ! Constants::is_true( 'DISABLE_WP_CRON' ); + } + + /** + * Validates if `WP_CONTENT_DIR` is writable. + * + * @since 3.8.0 + * @return bool + */ + private static function met_filesystem_requirement() { + return is_writable( WP_CONTENT_DIR ); + } +} diff --git a/includes/wccom-site/class-wc-wccom-site-installer.php b/includes/wccom-site/class-wc-wccom-site-installer.php new file mode 100644 index 0000000..e066141 --- /dev/null +++ b/includes/wccom-site/class-wc-wccom-site-installer.php @@ -0,0 +1,577 @@ + 'idle', + 'steps' => array(), + 'current_step' => null, + ); + + /** + * Represents product step state. + * + * @var array + */ + private static $default_step_state = array( + 'download_url' => '', + 'product_type' => '', + 'last_step' => '', + 'last_error' => '', + 'download_path' => '', + 'unpacked_path' => '', + 'installed_path' => '', + 'activate' => false, + ); + + /** + * Product install steps. Each step is a method name in this class that + * will be passed with product ID arg \WP_Upgrader instance. + * + * @var array + */ + private static $install_steps = array( + 'get_product_info', + 'download_product', + 'unpack_product', + 'move_product', + 'activate_product', + ); + + /** + * Get the product install state. + * + * @since 3.7.0 + * @param string $key Key in state data. If empty key is passed array of + * state will be returned. + * @return array Product install state. + */ + public static function get_state( $key = '' ) { + $state = WC_Helper_Options::get( 'product_install', self::$default_state ); + if ( ! empty( $key ) ) { + return isset( $state[ $key ] ) ? $state[ $key ] : null; + } + + return $state; + } + + /** + * Update the product install state. + * + * @since 3.7.0 + * @param string $key Key in state data. + * @param mixed $value Value. + */ + public static function update_state( $key, $value ) { + $state = WC_Helper_Options::get( 'product_install', self::$default_state ); + + $state[ $key ] = $value; + WC_Helper_Options::update( 'product_install', $state ); + } + + /** + * Reset product install state. + * + * @since 3.7.0 + * @param array $products List of product IDs. + */ + public static function reset_state( $products = array() ) { + WC()->queue()->cancel_all( 'woocommerce_wccom_install_products' ); + WC_Helper_Options::update( 'product_install', self::$default_state ); + } + + /** + * Schedule installing given list of products. + * + * @since 3.7.0 + * @param array $products Array of products where key is product ID and + * element is install args. + * @return array State. + */ + public static function schedule_install( $products ) { + $state = self::get_state(); + $status = ! empty( $state['status'] ) ? $state['status'] : ''; + if ( 'in-progress' === $status ) { + return $state; + } + self::update_state( 'status', 'in-progress' ); + + $steps = array_fill_keys( array_keys( $products ), self::$default_step_state ); + self::update_state( 'steps', $steps ); + + self::update_state( 'current_step', null ); + + $args = array( + 'products' => $products, + ); + + // Clear the cache of customer's subscription before asking for them. + // Thus, they will be re-fetched from WooCommerce.com after a purchase. + WC_Helper::_flush_subscriptions_cache(); + + WC()->queue()->cancel_all( 'woocommerce_wccom_install_products', $args ); + WC()->queue()->add( 'woocommerce_wccom_install_products', $args ); + + return self::get_state(); + } + + /** + * Install a given product IDs. + * + * Run via `woocommerce_wccom_install_products` hook. + * + * @since 3.7.0 + * @param array $products Array of products where key is product ID and + * element is install args. + */ + public static function install( $products ) { + 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(); + $upgrader = new WP_Upgrader( new Automatic_Upgrader_Skin() ); + $upgrader->init(); + wp_clean_plugins_cache(); + + foreach ( $products as $product_id => $install_args ) { + self::install_product( $product_id, $install_args, $upgrader ); + } + + self::finish_installation(); + } + + /** + * Finish installation by updating the state. + * + * @since 3.7.0 + */ + private static function finish_installation() { + $state = self::get_state(); + if ( empty( $state['steps'] ) ) { + return; + } + + foreach ( $state['steps'] as $step ) { + if ( ! empty( $step['last_error'] ) ) { + $state['status'] = 'has_error'; + break; + } + } + + if ( 'has_error' !== $state['status'] ) { + $state['status'] = 'finished'; + } + + WC_Helper_Options::update( 'product_install', $state ); + } + + /** + * Install a single product given its ID. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @param array $install_args Install args. + * @param \WP_Upgrader $upgrader Core class to handle installation. + */ + private static function install_product( $product_id, $install_args, $upgrader ) { + foreach ( self::$install_steps as $step ) { + self::do_install_step( $product_id, $install_args, $step, $upgrader ); + } + } + + /** + * Perform product installation step. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @param array $install_args Install args. + * @param string $step Installation step. + * @param \WP_Upgrader $upgrader Core class to handle installation. + */ + private static function do_install_step( $product_id, $install_args, $step, $upgrader ) { + $state_steps = self::get_state( 'steps' ); + if ( empty( $state_steps[ $product_id ] ) ) { + $state_steps[ $product_id ] = self::$default_step_state; + } + + if ( ! empty( $state_steps[ $product_id ]['last_error'] ) ) { + return; + } + + $state_steps[ $product_id ]['last_step'] = $step; + + if ( ! empty( $install_args['activate'] ) ) { + $state_steps[ $product_id ]['activate'] = true; + } + + self::update_state( + 'current_step', + array( + 'product_id' => $product_id, + 'step' => $step, + ) + ); + + $result = call_user_func( array( __CLASS__, $step ), $product_id, $upgrader ); + if ( is_wp_error( $result ) ) { + $state_steps[ $product_id ]['last_error'] = $result->get_error_message(); + } else { + switch ( $step ) { + case 'get_product_info': + $state_steps[ $product_id ]['download_url'] = $result['download_url']; + $state_steps[ $product_id ]['product_type'] = $result['product_type']; + $state_steps[ $product_id ]['product_name'] = $result['product_name']; + break; + case 'download_product': + $state_steps[ $product_id ]['download_path'] = $result; + break; + case 'unpack_product': + $state_steps[ $product_id ]['unpacked_path'] = $result; + break; + case 'move_product': + $state_steps[ $product_id ]['installed_path'] = $result['destination']; + if ( isset( $result[ self::$folder_exists ] ) ) { + $state_steps[ $product_id ]['warning'] = array( + 'message' => self::$folder_exists, + 'plugin_info' => self::get_plugin_info( $state_steps[ $product_id ]['installed_path'] ), + ); + } + break; + } + } + + self::update_state( 'steps', $state_steps ); + } + + /** + * Get product info from its ID. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @return array|\WP_Error + */ + private static function get_product_info( $product_id ) { + $product_info = array( + 'download_url' => '', + 'product_type' => '', + ); + + // Get product info from woocommerce.com. + $request = WC_Helper_API::get( + add_query_arg( + array( 'product_id' => absint( $product_id ) ), + 'info' + ), + array( + 'authenticated' => true, + ) + ); + + if ( 200 !== wp_remote_retrieve_response_code( $request ) ) { + return new WP_Error( 'product_info_failed', __( 'Failed to retrieve product info from woocommerce.com', 'woocommerce' ) ); + } + + $result = json_decode( wp_remote_retrieve_body( $request ), true ); + + $product_info['product_type'] = $result['_product_type']; + $product_info['product_name'] = $result['name']; + + if ( ! empty( $result['_wporg_product'] ) && ! empty( $result['download_link'] ) ) { + // For wporg product, download is set already from info response. + $product_info['download_url'] = $result['download_link']; + } elseif ( ! WC_Helper::has_product_subscription( $product_id ) ) { + // Non-wporg product needs subscription. + return new WP_Error( 'missing_subscription', __( 'Missing product subscription', 'woocommerce' ) ); + } else { + // Retrieve download URL for non-wporg product. + WC_Helper_Updater::flush_updates_cache(); + $updates = WC_Helper_Updater::get_update_data(); + if ( empty( $updates[ $product_id ]['package'] ) ) { + return new WP_Error( 'missing_product_package', __( 'Could not find product package.', 'woocommerce' ) ); + } + + $product_info['download_url'] = $updates[ $product_id ]['package']; + } + + return $product_info; + } + + /** + * Download product by its ID and returns the path of the zip package. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @param \WP_Upgrader $upgrader Core class to handle installation. + * @return \WP_Error|string + */ + private static function download_product( $product_id, $upgrader ) { + $steps = self::get_state( 'steps' ); + if ( empty( $steps[ $product_id ]['download_url'] ) ) { + return new WP_Error( 'missing_download_url', __( 'Could not find download url for the product.', 'woocommerce' ) ); + } + return $upgrader->download_package( $steps[ $product_id ]['download_url'] ); + } + + /** + * Unpack downloaded product. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @param \WP_Upgrader $upgrader Core class to handle installation. + * @return \WP_Error|string + */ + private static function unpack_product( $product_id, $upgrader ) { + $steps = self::get_state( 'steps' ); + if ( empty( $steps[ $product_id ]['download_path'] ) ) { + return new WP_Error( 'missing_download_path', __( 'Could not find download path.', 'woocommerce' ) ); + } + + return $upgrader->unpack_package( $steps[ $product_id ]['download_path'], true ); + } + + /** + * Move product to plugins directory. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @param \WP_Upgrader $upgrader Core class to handle installation. + * @return array|\WP_Error + */ + private static function move_product( $product_id, $upgrader ) { + $steps = self::get_state( 'steps' ); + if ( empty( $steps[ $product_id ]['unpacked_path'] ) ) { + return new WP_Error( 'missing_unpacked_path', __( 'Could not find unpacked path.', 'woocommerce' ) ); + } + + $destination = 'plugin' === $steps[ $product_id ]['product_type'] + ? WP_PLUGIN_DIR + : get_theme_root(); + + $package = array( + 'source' => $steps[ $product_id ]['unpacked_path'], + 'destination' => $destination, + 'clear_working' => true, + 'hook_extra' => array( + 'type' => $steps[ $product_id ]['product_type'], + 'action' => 'install', + ), + ); + + $result = $upgrader->install_package( $package ); + + /** + * If install package returns error 'folder_exists' threat as success. + */ + if ( is_wp_error( $result ) && array_key_exists( self::$folder_exists, $result->errors ) ) { + return array( + self::$folder_exists => true, + 'destination' => $result->error_data[ self::$folder_exists ], + ); + } + return $result; + } + + /** + * Activate product given its product ID. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @return \WP_Error|null + */ + private static function activate_product( $product_id ) { + $steps = self::get_state( 'steps' ); + if ( ! $steps[ $product_id ]['activate'] ) { + return null; + } + + if ( 'plugin' === $steps[ $product_id ]['product_type'] ) { + return self::activate_plugin( $product_id ); + } + return self::activate_theme( $product_id ); + } + + /** + * Activate plugin given its product ID. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @return \WP_Error|null + */ + private static function activate_plugin( $product_id ) { + // Clear plugins cache used in `WC_Helper::get_local_woo_plugins`. + wp_clean_plugins_cache(); + $filename = false; + + // If product is WP.org one, find out its filename. + $dir_name = self::get_wporg_product_dir_name( $product_id ); + if ( false !== $dir_name ) { + $filename = self::get_wporg_plugin_main_file( $dir_name ); + } + + if ( false === $filename ) { + $plugins = wp_list_filter( + WC_Helper::get_local_woo_plugins(), + array( + '_product_id' => $product_id, + ) + ); + + $filename = is_array( $plugins ) && ! empty( $plugins ) ? key( $plugins ) : ''; + } + + if ( empty( $filename ) ) { + return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) ); + } + + return activate_plugin( $filename ); + } + + /** + * Activate theme given its product ID. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @return \WP_Error|null + */ + private static function activate_theme( $product_id ) { + // Clear plugins cache used in `WC_Helper::get_local_woo_themes`. + wp_clean_themes_cache(); + $theme_slug = false; + + // If product is WP.org theme, find out its slug. + $dir_name = self::get_wporg_product_dir_name( $product_id ); + if ( false !== $dir_name ) { + $theme_slug = basename( $dir_name ); + } + + if ( false === $theme_slug ) { + $themes = wp_list_filter( + WC_Helper::get_local_woo_themes(), + array( + '_product_id' => $product_id, + ) + ); + + $theme_slug = is_array( $themes ) && ! empty( $themes ) ? dirname( key( $themes ) ) : ''; + } + + if ( empty( $theme_slug ) ) { + return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) ); + } + + return switch_theme( $theme_slug ); + } + + /** + * Get installed directory of WP.org product. + * + * @since 3.7.0 + * @param int $product_id Product ID. + * @return bool|string + */ + private static function get_wporg_product_dir_name( $product_id ) { + $steps = self::get_state( 'steps' ); + $product = $steps[ $product_id ]; + + if ( empty( $product['download_url'] ) || empty( $product['installed_path'] ) ) { + return false; + } + + // Check whether product was downloaded from WordPress.org. + $parsed_url = wp_parse_url( $product['download_url'] ); + if ( ! empty( $parsed_url['host'] ) && 'downloads.wordpress.org' !== $parsed_url['host'] ) { + return false; + } + + return basename( $product['installed_path'] ); + } + + /** + * Get WP.org plugin's main file. + * + * @since 3.7.0 + * @param string $dir Directory name of the plugin. + * @return bool|string + */ + private static function get_wporg_plugin_main_file( $dir ) { + // Ensure that exact dir name is used. + $dir = trailingslashit( $dir ); + + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + foreach ( $plugins as $path => $plugin ) { + if ( 0 === strpos( $path, $dir ) ) { + return $path; + } + } + + return false; + } + + + /** + * Get plugin info + * + * @since 3.9.0 + * @param string $dir Directory name of the plugin. + * @return bool|array + */ + private static function get_plugin_info( $dir ) { + $plugin_folder = basename( $dir ); + + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugins = get_plugins(); + + $related_plugins = array_filter( + $plugins, + function( $key ) use ( $plugin_folder ) { + return strpos( $key, $plugin_folder . '/' ) === 0; + }, + ARRAY_FILTER_USE_KEY + ); + + if ( 1 === count( $related_plugins ) ) { + $plugin_key = array_keys( $related_plugins )[0]; + $plugin_data = $plugins[ $plugin_key ]; + return array( + 'name' => $plugin_data['Name'], + 'version' => $plugin_data['Version'], + 'active' => is_plugin_active( $plugin_key ), + ); + } + return false; + } +} diff --git a/includes/wccom-site/class-wc-wccom-site.php b/includes/wccom-site/class-wc-wccom-site.php new file mode 100644 index 0000000..d09cb06 --- /dev/null +++ b/includes/wccom-site/class-wc-wccom-site.php @@ -0,0 +1,253 @@ + WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_HTTP_CODE ) + ); + } + ); + return false; + } + + if ( ! empty( $_SERVER['HTTP_X_WOO_SIGNATURE'] ) ) { + $signature = trim( $_SERVER['HTTP_X_WOO_SIGNATURE'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } elseif ( ! empty( $_GET['signature'] ) && is_string( $_GET['signature'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $signature = trim( $_GET['signature'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } else { + add_filter( + self::AUTH_ERROR_FILTER_NAME, + function() { + return new WP_Error( + WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_CODE, + WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_MESSAGE, + array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_HTTP_CODE ) + ); + } + ); + return false; + } + + require_once WC_ABSPATH . 'includes/admin/helper/class-wc-helper-options.php'; + $site_auth = WC_Helper_Options::get( 'auth' ); + + if ( empty( $site_auth['access_token'] ) ) { + add_filter( + self::AUTH_ERROR_FILTER_NAME, + function() { + return new WP_Error( + WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_CODE, + WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_MESSAGE, + array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_HTTP_CODE ) + ); + } + ); + return false; + } + + if ( ! hash_equals( $access_token, $site_auth['access_token'] ) ) { + add_filter( + self::AUTH_ERROR_FILTER_NAME, + function() { + return new WP_Error( + WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_CODE, + WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_MESSAGE, + array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_HTTP_CODE ) + ); + } + ); + return false; + } + + $body = WP_REST_Server::get_raw_data(); + + if ( ! self::verify_wccom_request( $body, $signature, $site_auth['access_token_secret'] ) ) { + add_filter( + self::AUTH_ERROR_FILTER_NAME, + function() { + return new WP_Error( + WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_CODE, + WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_MESSAGE, + array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_HTTP_CODE ) + ); + } + ); + return false; + } + + $user = get_user_by( 'id', $site_auth['user_id'] ); + if ( ! $user ) { + add_filter( + self::AUTH_ERROR_FILTER_NAME, + function() { + return new WP_Error( + WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_CODE, + WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_MESSAGE, + array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_HTTP_CODE ) + ); + } + ); + return false; + } + + return $user; + } + + /** + * 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.7.0 + * @return string Authorization header if set. + */ + protected static function get_authorization_header() { + if ( ! empty( $_SERVER['HTTP_AUTHORIZATION'] ) ) { + return wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + } + + 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 ''; + } + + /** + * Check if this is a request to WCCOM Site REST API. + * + * @since 3.7.0 + * @return bool + */ + protected static function is_request_to_wccom_site_rest_api() { + + if ( isset( $_REQUEST['rest_route'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $route = wp_unslash( $_REQUEST['rest_route'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Recommended + $rest_prefix = ''; + } else { + $route = wp_unslash( add_query_arg( array() ) ); + $rest_prefix = trailingslashit( rest_get_url_prefix() ); + } + + return false !== strpos( $route, $rest_prefix . 'wccom-site/' ); + } + + /** + * Verify WooCommerce.com request from a given body and signature request. + * + * @since 3.7.0 + * @param string $body Request body. + * @param string $signature Request signature found in X-Woo-Signature header. + * @param string $access_token_secret Access token secret for this site. + * @return bool + */ + protected static function verify_wccom_request( $body, $signature, $access_token_secret ) { + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $data = array( + 'host' => $_SERVER['HTTP_HOST'], + 'request_uri' => urldecode( remove_query_arg( array( 'token', 'signature' ), $_SERVER['REQUEST_URI'] ) ), + 'method' => strtoupper( $_SERVER['REQUEST_METHOD'] ), + ); + // phpcs:enable + + if ( ! empty( $body ) ) { + $data['body'] = $body; + } + + $expected_signature = hash_hmac( 'sha256', wp_json_encode( $data ), $access_token_secret ); + + return hash_equals( $expected_signature, $signature ); + } + + /** + * Register wccom-site REST namespace. + * + * @since 3.7.0 + * @param array $namespaces List of registered namespaces. + * @return array Registered namespaces. + */ + public static function register_rest_namespace( $namespaces ) { + require_once WC_ABSPATH . 'includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php'; + require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php'; + + $namespaces['wccom-site/v1'] = array( + 'installer' => 'WC_REST_WCCOM_Site_Installer_Controller', + ); + + return $namespaces; + } +} + +WC_WCCOM_Site::load(); diff --git a/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php b/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php new file mode 100644 index 0000000..366b9d0 --- /dev/null +++ b/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php @@ -0,0 +1,73 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_install_state' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'install' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'products' => array( + 'required' => true, + 'type' => 'object', + ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'reset_install' ), + 'permission_callback' => array( $this, 'check_permission' ), + ), + ) + ); + } + + /** + * Check permissions. + * + * @since 3.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function check_permission( $request ) { + $current_user = wp_get_current_user(); + + if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) { + return apply_filters( + WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME, + new WP_Error( + WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_CODE, + WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_MESSAGE, + array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_HTTP_CODE ) + ) + ); + } + + if ( ! user_can( $current_user, 'install_plugins' ) || ! user_can( $current_user, 'install_themes' ) ) { + return new WP_Error( + WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_CODE, + WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_MESSAGE, + array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_HTTP_CODE ) + ); + } + + return true; + } + + /** + * Get installation state. + * + * @since 3.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function get_install_state( $request ) { + $requirements_met = WC_WCCOM_Site_Installer_Requirements_Check::met_requirements(); + if ( is_wp_error( $requirements_met ) ) { + return $requirements_met; + } + + return rest_ensure_response( WC_WCCOM_Site_Installer::get_state() ); + } + + /** + * Install WooCommerce.com products. + * + * @since 3.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function install( $request ) { + $requirements_met = WC_WCCOM_Site_Installer_Requirements_Check::met_requirements(); + if ( is_wp_error( $requirements_met ) ) { + return $requirements_met; + } + + if ( empty( $request['products'] ) ) { + return new WP_Error( 'missing_products', __( 'Missing products in request body.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $validation_result = $this->validate_products( $request['products'] ); + if ( is_wp_error( $validation_result ) ) { + return $validation_result; + } + + return rest_ensure_response( WC_WCCOM_Site_Installer::schedule_install( $request['products'] ) ); + } + + /** + * Reset installation state. + * + * @since 3.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function reset_install( $request ) { + $resp = rest_ensure_response( WC_WCCOM_Site_Installer::reset_state() ); + $resp->set_status( 204 ); + + return $resp; + } + + /** + * Validate products from request body. + * + * @since 3.7.0 + * @param array $products Array of products where key is product ID and + * element is install args. + * @return bool|WP_Error + */ + protected function validate_products( $products ) { + $err = new WP_Error( 'invalid_products', __( 'Invalid products in request body.', 'woocommerce' ), array( 'status' => 400 ) ); + + if ( ! is_array( $products ) ) { + return $err; + } + + foreach ( $products as $product_id => $install_args ) { + if ( ! absint( $product_id ) ) { + return $err; + } + + if ( empty( $install_args ) || ! is_array( $install_args ) ) { + return $err; + } + } + + return true; + } +} diff --git a/includes/widgets/class-wc-widget-cart.php b/includes/widgets/class-wc-widget-cart.php new file mode 100644 index 0000000..cc76280 --- /dev/null +++ b/includes/widgets/class-wc-widget-cart.php @@ -0,0 +1,80 @@ +widget_cssclass = 'woocommerce widget_shopping_cart'; + $this->widget_description = __( 'Display the customer shopping cart.', 'woocommerce' ); + $this->widget_id = 'woocommerce_widget_cart'; + $this->widget_name = __( 'Cart', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Cart', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'hide_if_empty' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Hide if cart is empty', 'woocommerce' ), + ), + ); + + if ( is_customize_preview() ) { + wp_enqueue_script( 'wc-cart-fragments' ); + } + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + if ( apply_filters( 'woocommerce_widget_cart_is_hidden', is_cart() || is_checkout() ) ) { + return; + } + + $hide_if_empty = empty( $instance['hide_if_empty'] ) ? 0 : 1; + + if ( ! isset( $instance['title'] ) ) { + $instance['title'] = __( 'Cart', 'woocommerce' ); + } + + $this->widget_start( $args, $instance ); + + if ( $hide_if_empty ) { + echo '
    '; + } + + // Insert cart widget placeholder - code in woocommerce.js will update this on page load. + echo '
    '; + + if ( $hide_if_empty ) { + echo '
    '; + } + + $this->widget_end( $args ); + } +} diff --git a/includes/widgets/class-wc-widget-layered-nav-filters.php b/includes/widgets/class-wc-widget-layered-nav-filters.php new file mode 100644 index 0000000..a333081 --- /dev/null +++ b/includes/widgets/class-wc-widget-layered-nav-filters.php @@ -0,0 +1,113 @@ +widget_cssclass = 'woocommerce widget_layered_nav_filters'; + $this->widget_description = __( 'Display a list of active product filters.', 'woocommerce' ); + $this->widget_id = 'woocommerce_layered_nav_filters'; + $this->widget_name = __( 'Active Product Filters', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Active filters', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + if ( ! is_shop() && ! is_product_taxonomy() ) { + return; + } + + $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); + $min_price = isset( $_GET['min_price'] ) ? wc_clean( wp_unslash( $_GET['min_price'] ) ) : 0; // WPCS: input var ok, CSRF ok. + $max_price = isset( $_GET['max_price'] ) ? wc_clean( wp_unslash( $_GET['max_price'] ) ) : 0; // WPCS: input var ok, CSRF ok. + $rating_filter = isset( $_GET['rating_filter'] ) ? array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ) : array(); // WPCS: sanitization ok, input var ok, CSRF ok. + $base_link = $this->get_current_page_url(); + + if ( 0 < count( $_chosen_attributes ) || 0 < $min_price || 0 < $max_price || ! empty( $rating_filter ) ) { + + $this->widget_start( $args, $instance ); + + echo '
      '; + + // Attributes. + if ( ! empty( $_chosen_attributes ) ) { + foreach ( $_chosen_attributes as $taxonomy => $data ) { + foreach ( $data['terms'] as $term_slug ) { + $term = get_term_by( 'slug', $term_slug, $taxonomy ); + if ( ! $term ) { + continue; + } + + $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy ); + $current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); // WPCS: input var ok, CSRF ok. + $current_filter = array_map( 'sanitize_title', $current_filter ); + $new_filter = array_diff( $current_filter, array( $term_slug ) ); + + $link = remove_query_arg( array( 'add-to-cart', $filter_name ), $base_link ); + + if ( count( $new_filter ) > 0 ) { + $link = add_query_arg( $filter_name, implode( ',', $new_filter ), $link ); + } + + $filter_classes = array( 'chosen', 'chosen-' . sanitize_html_class( str_replace( 'pa_', '', $taxonomy ) ), 'chosen-' . sanitize_html_class( str_replace( 'pa_', '', $taxonomy ) . '-' . $term_slug ) ); + + echo '
    • ' . esc_html( $term->name ) . '
    • '; + } + } + } + + if ( $min_price ) { + $link = remove_query_arg( 'min_price', $base_link ); + /* translators: %s: minimum price */ + echo '
    • ' . sprintf( __( 'Min %s', 'woocommerce' ), wc_price( $min_price ) ) . '
    • '; // WPCS: XSS ok. + } + + if ( $max_price ) { + $link = remove_query_arg( 'max_price', $base_link ); + /* translators: %s: maximum price */ + echo '
    • ' . sprintf( __( 'Max %s', 'woocommerce' ), wc_price( $max_price ) ) . '
    • '; // WPCS: XSS ok. + } + + if ( ! empty( $rating_filter ) ) { + foreach ( $rating_filter as $rating ) { + $link_ratings = implode( ',', array_diff( $rating_filter, array( $rating ) ) ); + $link = $link_ratings ? add_query_arg( 'rating_filter', $link_ratings ) : remove_query_arg( 'rating_filter', $base_link ); + + /* translators: %s: rating */ + echo '
    • ' . sprintf( esc_html__( 'Rated %s out of 5', 'woocommerce' ), esc_html( $rating ) ) . '
    • '; + } + } + + echo '
    '; + + $this->widget_end( $args ); + } + } +} diff --git a/includes/widgets/class-wc-widget-layered-nav.php b/includes/widgets/class-wc-widget-layered-nav.php new file mode 100644 index 0000000..c4a4bb1 --- /dev/null +++ b/includes/widgets/class-wc-widget-layered-nav.php @@ -0,0 +1,469 @@ +widget_cssclass = 'woocommerce widget_layered_nav woocommerce-widget-layered-nav'; + $this->widget_description = __( 'Display a list of attributes to filter products in your store.', 'woocommerce' ); + $this->widget_id = 'woocommerce_layered_nav'; + $this->widget_name = __( 'Filter Products by Attribute', 'woocommerce' ); + parent::__construct(); + } + + /** + * Updates a particular instance of a widget. + * + * @see WP_Widget->update + * + * @param array $new_instance New Instance. + * @param array $old_instance Old Instance. + * + * @return array + */ + public function update( $new_instance, $old_instance ) { + $this->init_settings(); + return parent::update( $new_instance, $old_instance ); + } + + /** + * Outputs the settings update form. + * + * @see WP_Widget->form + * + * @param array $instance Instance. + */ + public function form( $instance ) { + $this->init_settings(); + parent::form( $instance ); + } + + /** + * Init settings after post types are registered. + */ + public function init_settings() { + $attribute_array = array(); + $std_attribute = ''; + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $tax ) { + if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { + $attribute_array[ $tax->attribute_name ] = $tax->attribute_name; + } + } + $std_attribute = current( $attribute_array ); + } + + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Filter by', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'attribute' => array( + 'type' => 'select', + 'std' => $std_attribute, + 'label' => __( 'Attribute', 'woocommerce' ), + 'options' => $attribute_array, + ), + 'display_type' => array( + 'type' => 'select', + 'std' => 'list', + 'label' => __( 'Display type', 'woocommerce' ), + 'options' => array( + 'list' => __( 'List', 'woocommerce' ), + 'dropdown' => __( 'Dropdown', 'woocommerce' ), + ), + ), + 'query_type' => array( + 'type' => 'select', + 'std' => 'and', + 'label' => __( 'Query type', 'woocommerce' ), + 'options' => array( + 'and' => __( 'AND', 'woocommerce' ), + 'or' => __( 'OR', 'woocommerce' ), + ), + ), + ); + } + + /** + * Get this widgets taxonomy. + * + * @param array $instance Array of instance options. + * @return string + */ + protected function get_instance_taxonomy( $instance ) { + if ( isset( $instance['attribute'] ) ) { + return wc_attribute_taxonomy_name( $instance['attribute'] ); + } + + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( ! empty( $attribute_taxonomies ) ) { + foreach ( $attribute_taxonomies as $tax ) { + if ( taxonomy_exists( wc_attribute_taxonomy_name( $tax->attribute_name ) ) ) { + return wc_attribute_taxonomy_name( $tax->attribute_name ); + } + } + } + + return ''; + } + + /** + * Get this widgets query type. + * + * @param array $instance Array of instance options. + * @return string + */ + protected function get_instance_query_type( $instance ) { + return isset( $instance['query_type'] ) ? $instance['query_type'] : 'and'; + } + + /** + * Get this widgets display type. + * + * @param array $instance Array of instance options. + * @return string + */ + protected function get_instance_display_type( $instance ) { + return isset( $instance['display_type'] ) ? $instance['display_type'] : 'list'; + } + + /** + * Output widget. + * + * @see WP_Widget + * + * @param array $args Arguments. + * @param array $instance Instance. + */ + public function widget( $args, $instance ) { + if ( ! is_shop() && ! is_product_taxonomy() ) { + return; + } + + $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); + $taxonomy = $this->get_instance_taxonomy( $instance ); + $query_type = $this->get_instance_query_type( $instance ); + $display_type = $this->get_instance_display_type( $instance ); + + if ( ! taxonomy_exists( $taxonomy ) ) { + return; + } + + $terms = get_terms( $taxonomy, array( 'hide_empty' => '1' ) ); + + if ( 0 === count( $terms ) ) { + return; + } + + ob_start(); + + $this->widget_start( $args, $instance ); + + if ( 'dropdown' === $display_type ) { + wp_enqueue_script( 'selectWoo' ); + wp_enqueue_style( 'select2' ); + $found = $this->layered_nav_dropdown( $terms, $taxonomy, $query_type ); + } else { + $found = $this->layered_nav_list( $terms, $taxonomy, $query_type ); + } + + $this->widget_end( $args ); + + // Force found when option is selected - do not force found on taxonomy attributes. + if ( ! is_tax() && is_array( $_chosen_attributes ) && array_key_exists( $taxonomy, $_chosen_attributes ) ) { + $found = true; + } + + if ( ! $found ) { + ob_end_clean(); + } else { + echo ob_get_clean(); // @codingStandardsIgnoreLine + } + } + + /** + * Return the currently viewed taxonomy name. + * + * @return string + */ + protected function get_current_taxonomy() { + return is_tax() ? get_queried_object()->taxonomy : ''; + } + + /** + * Return the currently viewed term ID. + * + * @return int + */ + protected function get_current_term_id() { + return absint( is_tax() ? get_queried_object()->term_id : 0 ); + } + + /** + * Return the currently viewed term slug. + * + * @return int + */ + protected function get_current_term_slug() { + return absint( is_tax() ? get_queried_object()->slug : 0 ); + } + + /** + * Show dropdown layered nav. + * + * @param array $terms Terms. + * @param string $taxonomy Taxonomy. + * @param string $query_type Query Type. + * @return bool Will nav display? + */ + protected function layered_nav_dropdown( $terms, $taxonomy, $query_type ) { + global $wp; + $found = false; + + if ( $taxonomy !== $this->get_current_taxonomy() ) { + $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); + $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); + $taxonomy_filter_name = wc_attribute_taxonomy_slug( $taxonomy ); + $taxonomy_label = wc_attribute_label( $taxonomy ); + + /* translators: %s: taxonomy name */ + $any_label = apply_filters( 'woocommerce_layered_nav_any_label', sprintf( __( 'Any %s', 'woocommerce' ), $taxonomy_label ), $taxonomy_label, $taxonomy ); + $multiple = 'or' === $query_type; + $current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); + + if ( '' === get_option( 'permalink_structure' ) ) { + $form_action = remove_query_arg( array( 'page', 'paged' ), add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) ); + } else { + $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( user_trailingslashit( $wp->request ) ) ); + } + + echo '
    '; + echo ''; + + if ( $multiple ) { + echo ''; + } + + if ( 'or' === $query_type ) { + echo ''; + } + + echo ''; + echo wc_query_string_form_fields( null, array( 'filter_' . $taxonomy_filter_name, 'query_type_' . $taxonomy_filter_name ), '', true ); // @codingStandardsIgnoreLine + echo '
    '; + + wc_enqueue_js( + " + // Update value on change. + jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).on( 'change', function() { + var slug = jQuery( this ).val(); + jQuery( ':input[name=\"filter_" . esc_js( $taxonomy_filter_name ) . "\"]' ).val( slug ); + + // Submit form on change if standard dropdown. + if ( ! jQuery( this ).attr( 'multiple' ) ) { + jQuery( this ).closest( 'form' ).trigger( 'submit' ); + } + }); + + // Use Select2 enhancement if possible + if ( jQuery().selectWoo ) { + var wc_layered_nav_select = function() { + jQuery( '.dropdown_layered_nav_" . esc_js( $taxonomy_filter_name ) . "' ).selectWoo( { + placeholder: decodeURIComponent('" . rawurlencode( (string) wp_specialchars_decode( $any_label ) ) . "'), + minimumResultsForSearch: 5, + width: '100%', + allowClear: " . ( $multiple ? 'false' : 'true' ) . ", + language: { + noResults: function() { + return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "'; + } + } + } ); + }; + wc_layered_nav_select(); + } + " + ); + } + + return $found; + } + + /** + * Count products within certain terms, taking the main WP query into consideration. + * + * This query allows counts to be generated based on the viewed products, not all products. + * + * @param array $term_ids Term IDs. + * @param string $taxonomy Taxonomy. + * @param string $query_type Query Type. + * @return array + */ + protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { + return wc_get_container()->get( Filterer::class )->get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ); + } + + /** + * Wrapper for WC_Query::get_main_tax_query() to ease unit testing. + * + * @since 4.4.0 + * @return array + */ + protected function get_main_tax_query() { + return WC_Query::get_main_tax_query(); + } + + /** + * Wrapper for WC_Query::get_main_search_query_sql() to ease unit testing. + * + * @since 4.4.0 + * @return string + */ + protected function get_main_search_query_sql() { + return WC_Query::get_main_search_query_sql(); + } + + /** + * Wrapper for WC_Query::get_main_search_queryget_main_meta_query to ease unit testing. + * + * @since 4.4.0 + * @return array + */ + protected function get_main_meta_query() { + return WC_Query::get_main_meta_query(); + } + + /** + * Show list based layered nav. + * + * @param array $terms Terms. + * @param string $taxonomy Taxonomy. + * @param string $query_type Query Type. + * @return bool Will nav display? + */ + protected function layered_nav_list( $terms, $taxonomy, $query_type ) { + // List display. + echo '
      '; + + $term_counts = $this->get_filtered_term_product_counts( wp_list_pluck( $terms, 'term_id' ), $taxonomy, $query_type ); + $_chosen_attributes = WC_Query::get_layered_nav_chosen_attributes(); + $found = false; + $base_link = $this->get_current_page_url(); + + foreach ( $terms as $term ) { + $current_values = isset( $_chosen_attributes[ $taxonomy ]['terms'] ) ? $_chosen_attributes[ $taxonomy ]['terms'] : array(); + $option_is_set = in_array( $term->slug, $current_values, true ); + $count = isset( $term_counts[ $term->term_id ] ) ? $term_counts[ $term->term_id ] : 0; + + // Skip the term for the current archive. + if ( $this->get_current_term_id() === $term->term_id ) { + continue; + } + + // Only show options with count > 0. + if ( 0 < $count ) { + $found = true; + } elseif ( 0 === $count && ! $option_is_set ) { + continue; + } + + $filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy ); + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); + $current_filter = array_map( 'sanitize_title', $current_filter ); + + if ( ! in_array( $term->slug, $current_filter, true ) ) { + $current_filter[] = $term->slug; + } + + $link = remove_query_arg( $filter_name, $base_link ); + + // Add current filters to URL. + foreach ( $current_filter as $key => $value ) { + // Exclude query arg for current term archive term. + if ( $value === $this->get_current_term_slug() ) { + unset( $current_filter[ $key ] ); + } + + // Exclude self so filter can be unset on click. + if ( $option_is_set && $value === $term->slug ) { + unset( $current_filter[ $key ] ); + } + } + + if ( ! empty( $current_filter ) ) { + asort( $current_filter ); + $link = add_query_arg( $filter_name, implode( ',', $current_filter ), $link ); + + // Add Query type Arg to URL. + if ( 'or' === $query_type && ! ( 1 === count( $current_filter ) && $option_is_set ) ) { + $link = add_query_arg( 'query_type_' . wc_attribute_taxonomy_slug( $taxonomy ), 'or', $link ); + } + $link = str_replace( '%2C', ',', $link ); + } + + if ( $count > 0 || $option_is_set ) { + $link = apply_filters( 'woocommerce_layered_nav_link', $link, $term, $taxonomy ); + $term_html = '' . esc_html( $term->name ) . ''; + } else { + $link = false; + $term_html = '' . esc_html( $term->name ) . ''; + } + + $term_html .= ' ' . apply_filters( 'woocommerce_layered_nav_count', '(' . absint( $count ) . ')', $count, $term ); + + echo '
    • '; + // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.EscapeOutput.OutputNotEscaped + echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count ); + echo '
    • '; + } + + echo '
    '; + + return $found; + } +} diff --git a/includes/widgets/class-wc-widget-price-filter.php b/includes/widgets/class-wc-widget-price-filter.php new file mode 100644 index 0000000..cd8c590 --- /dev/null +++ b/includes/widgets/class-wc-widget-price-filter.php @@ -0,0 +1,186 @@ +widget_cssclass = 'woocommerce widget_price_filter'; + $this->widget_description = __( 'Display a slider to filter products in your store by price.', 'woocommerce' ); + $this->widget_id = 'woocommerce_price_filter'; + $this->widget_name = __( 'Filter Products by Price', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Filter by price', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + ); + $suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min'; + $version = Constants::get_constant( 'WC_VERSION' ); + wp_register_script( 'accounting', WC()->plugin_url() . '/assets/js/accounting/accounting' . $suffix . '.js', array( 'jquery' ), '0.4.2', true ); + wp_register_script( 'wc-jquery-ui-touchpunch', WC()->plugin_url() . '/assets/js/jquery-ui-touch-punch/jquery-ui-touch-punch' . $suffix . '.js', array( 'jquery-ui-slider' ), $version, true ); + wp_register_script( 'wc-price-slider', WC()->plugin_url() . '/assets/js/frontend/price-slider' . $suffix . '.js', array( 'jquery-ui-slider', 'wc-jquery-ui-touchpunch', 'accounting' ), $version, true ); + wp_localize_script( + 'wc-price-slider', + 'woocommerce_price_slider_params', + array( + 'currency_format_num_decimals' => 0, + 'currency_format_symbol' => get_woocommerce_currency_symbol(), + 'currency_format_decimal_sep' => esc_attr( wc_get_price_decimal_separator() ), + 'currency_format_thousand_sep' => esc_attr( wc_get_price_thousand_separator() ), + 'currency_format' => esc_attr( str_replace( array( '%1$s', '%2$s' ), array( '%s', '%v' ), get_woocommerce_price_format() ) ), + ) + ); + + if ( is_customize_preview() ) { + wp_enqueue_script( 'wc-price-slider' ); + } + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + global $wp; + + // Requires lookup table added in 3.6. + if ( version_compare( get_option( 'woocommerce_db_version', null ), '3.6', '<' ) ) { + return; + } + + if ( ! is_shop() && ! is_product_taxonomy() ) { + return; + } + + // If there are not posts and we're not filtering, hide the widget. + if ( ! WC()->query->get_main_query()->post_count && ! isset( $_GET['min_price'] ) && ! isset( $_GET['max_price'] ) ) { // WPCS: input var ok, CSRF ok. + return; + } + + wp_enqueue_script( 'wc-price-slider' ); + + // Round values to nearest 10 by default. + $step = max( apply_filters( 'woocommerce_price_filter_widget_step', 10 ), 1 ); + + // Find min and max price in current result set. + $prices = $this->get_filtered_price(); + $min_price = $prices->min_price; + $max_price = $prices->max_price; + + // Check to see if we should add taxes to the prices if store are excl tax but display incl. + $tax_display_mode = get_option( 'woocommerce_tax_display_shop' ); + + if ( wc_tax_enabled() && ! wc_prices_include_tax() && 'incl' === $tax_display_mode ) { + $tax_class = apply_filters( 'woocommerce_price_filter_widget_tax_class', '' ); // Uses standard tax class. + $tax_rates = WC_Tax::get_rates( $tax_class ); + + if ( $tax_rates ) { + $min_price += WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $min_price, $tax_rates ) ); + $max_price += WC_Tax::get_tax_total( WC_Tax::calc_exclusive_tax( $max_price, $tax_rates ) ); + } + } + + $min_price = apply_filters( 'woocommerce_price_filter_widget_min_amount', floor( $min_price / $step ) * $step ); + $max_price = apply_filters( 'woocommerce_price_filter_widget_max_amount', ceil( $max_price / $step ) * $step ); + + // If both min and max are equal, we don't need a slider. + if ( $min_price === $max_price ) { + return; + } + + $current_min_price = isset( $_GET['min_price'] ) ? floor( floatval( wp_unslash( $_GET['min_price'] ) ) / $step ) * $step : $min_price; // WPCS: input var ok, CSRF ok. + $current_max_price = isset( $_GET['max_price'] ) ? ceil( floatval( wp_unslash( $_GET['max_price'] ) ) / $step ) * $step : $max_price; // WPCS: input var ok, CSRF ok. + + $this->widget_start( $args, $instance ); + + if ( '' === get_option( 'permalink_structure' ) ) { + $form_action = remove_query_arg( array( 'page', 'paged', 'product-page' ), add_query_arg( $wp->query_string, '', home_url( $wp->request ) ) ); + } else { + $form_action = preg_replace( '%\/page/[0-9]+%', '', home_url( trailingslashit( $wp->request ) ) ); + } + + wc_get_template( + 'content-widget-price-filter.php', + array( + 'form_action' => $form_action, + 'step' => $step, + 'min_price' => $min_price, + 'max_price' => $max_price, + 'current_min_price' => $current_min_price, + 'current_max_price' => $current_max_price, + ) + ); + + $this->widget_end( $args ); + } + + /** + * Get filtered min price for current products. + * + * @return int + */ + protected function get_filtered_price() { + global $wpdb; + + $args = WC()->query->get_main_query()->query_vars; + $tax_query = isset( $args['tax_query'] ) ? $args['tax_query'] : array(); + $meta_query = isset( $args['meta_query'] ) ? $args['meta_query'] : array(); + + if ( ! is_post_type_archive( 'product' ) && ! empty( $args['taxonomy'] ) && ! empty( $args['term'] ) ) { + $tax_query[] = WC()->query->get_main_tax_query(); + } + + foreach ( $meta_query + $tax_query as $key => $query ) { + if ( ! empty( $query['price_filter'] ) || ! empty( $query['rating_filter'] ) ) { + unset( $meta_query[ $key ] ); + } + } + + $meta_query = new WP_Meta_Query( $meta_query ); + $tax_query = new WP_Tax_Query( $tax_query ); + $search = WC_Query::get_main_search_query_sql(); + + $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); + $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); + $search_query_sql = $search ? ' AND ' . $search : ''; + + $sql = " + SELECT min( min_price ) as min_price, MAX( max_price ) as max_price + FROM {$wpdb->wc_product_meta_lookup} + WHERE product_id IN ( + SELECT ID FROM {$wpdb->posts} + " . $tax_query_sql['join'] . $meta_query_sql['join'] . " + WHERE {$wpdb->posts}.post_type IN ('" . implode( "','", array_map( 'esc_sql', apply_filters( 'woocommerce_price_filter_post_type', array( 'product' ) ) ) ) . "') + AND {$wpdb->posts}.post_status = 'publish' + " . $tax_query_sql['where'] . $meta_query_sql['where'] . $search_query_sql . ' + )'; + + $sql = apply_filters( 'woocommerce_price_filter_sql', $sql, $meta_query_sql, $tax_query_sql ); + + return $wpdb->get_row( $sql ); // WPCS: unprepared SQL ok. + } +} diff --git a/includes/widgets/class-wc-widget-product-categories.php b/includes/widgets/class-wc-widget-product-categories.php new file mode 100644 index 0000000..559cb13 --- /dev/null +++ b/includes/widgets/class-wc-widget-product-categories.php @@ -0,0 +1,301 @@ +widget_cssclass = 'woocommerce widget_product_categories'; + $this->widget_description = __( 'A list or dropdown of product categories.', 'woocommerce' ); + $this->widget_id = 'woocommerce_product_categories'; + $this->widget_name = __( 'Product Categories', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Product categories', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'orderby' => array( + 'type' => 'select', + 'std' => 'name', + 'label' => __( 'Order by', 'woocommerce' ), + 'options' => array( + 'order' => __( 'Category order', 'woocommerce' ), + 'name' => __( 'Name', 'woocommerce' ), + ), + ), + 'dropdown' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Show as dropdown', 'woocommerce' ), + ), + 'count' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Show product counts', 'woocommerce' ), + ), + 'hierarchical' => array( + 'type' => 'checkbox', + 'std' => 1, + 'label' => __( 'Show hierarchy', 'woocommerce' ), + ), + 'show_children_only' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Only show children of the current category', 'woocommerce' ), + ), + 'hide_empty' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Hide empty categories', 'woocommerce' ), + ), + 'max_depth' => array( + 'type' => 'text', + 'std' => '', + 'label' => __( 'Maximum depth', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * @param array $args Widget arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + global $wp_query, $post; + + $count = isset( $instance['count'] ) ? $instance['count'] : $this->settings['count']['std']; + $hierarchical = isset( $instance['hierarchical'] ) ? $instance['hierarchical'] : $this->settings['hierarchical']['std']; + $show_children_only = isset( $instance['show_children_only'] ) ? $instance['show_children_only'] : $this->settings['show_children_only']['std']; + $dropdown = isset( $instance['dropdown'] ) ? $instance['dropdown'] : $this->settings['dropdown']['std']; + $orderby = isset( $instance['orderby'] ) ? $instance['orderby'] : $this->settings['orderby']['std']; + $hide_empty = isset( $instance['hide_empty'] ) ? $instance['hide_empty'] : $this->settings['hide_empty']['std']; + $dropdown_args = array( + 'hide_empty' => $hide_empty, + ); + $list_args = array( + 'show_count' => $count, + 'hierarchical' => $hierarchical, + 'taxonomy' => 'product_cat', + 'hide_empty' => $hide_empty, + ); + $max_depth = absint( isset( $instance['max_depth'] ) ? $instance['max_depth'] : $this->settings['max_depth']['std'] ); + + $list_args['menu_order'] = false; + $dropdown_args['depth'] = $max_depth; + $list_args['depth'] = $max_depth; + + if ( 'order' === $orderby ) { + $list_args['orderby'] = 'meta_value_num'; + $dropdown_args['orderby'] = 'meta_value_num'; + $list_args['meta_key'] = 'order'; + $dropdown_args['meta_key'] = 'order'; + } + + $this->current_cat = false; + $this->cat_ancestors = array(); + + if ( is_tax( 'product_cat' ) ) { + $this->current_cat = $wp_query->queried_object; + $this->cat_ancestors = get_ancestors( $this->current_cat->term_id, 'product_cat' ); + + } elseif ( is_singular( 'product' ) ) { + $terms = wc_get_product_terms( + $post->ID, + 'product_cat', + apply_filters( + 'woocommerce_product_categories_widget_product_terms_args', + array( + 'orderby' => 'parent', + 'order' => 'DESC', + ) + ) + ); + + if ( $terms ) { + $main_term = apply_filters( 'woocommerce_product_categories_widget_main_term', $terms[0], $terms ); + $this->current_cat = $main_term; + $this->cat_ancestors = get_ancestors( $main_term->term_id, 'product_cat' ); + } + } + + // Show Siblings and Children Only. + if ( $show_children_only && $this->current_cat ) { + if ( $hierarchical ) { + $include = array_merge( + $this->cat_ancestors, + array( $this->current_cat->term_id ), + get_terms( + 'product_cat', + array( + 'fields' => 'ids', + 'parent' => 0, + 'hierarchical' => true, + 'hide_empty' => false, + ) + ), + get_terms( + 'product_cat', + array( + 'fields' => 'ids', + 'parent' => $this->current_cat->term_id, + 'hierarchical' => true, + 'hide_empty' => false, + ) + ) + ); + // Gather siblings of ancestors. + if ( $this->cat_ancestors ) { + foreach ( $this->cat_ancestors as $ancestor ) { + $include = array_merge( + $include, + get_terms( + 'product_cat', + array( + 'fields' => 'ids', + 'parent' => $ancestor, + 'hierarchical' => false, + 'hide_empty' => false, + ) + ) + ); + } + } + } else { + // Direct children. + $include = get_terms( + 'product_cat', + array( + 'fields' => 'ids', + 'parent' => $this->current_cat->term_id, + 'hierarchical' => true, + 'hide_empty' => false, + ) + ); + } + + $list_args['include'] = implode( ',', $include ); + $dropdown_args['include'] = $list_args['include']; + + if ( empty( $include ) ) { + return; + } + } elseif ( $show_children_only ) { + $dropdown_args['depth'] = 1; + $dropdown_args['child_of'] = 0; + $dropdown_args['hierarchical'] = 1; + $list_args['depth'] = 1; + $list_args['child_of'] = 0; + $list_args['hierarchical'] = 1; + } + + $this->widget_start( $args, $instance ); + + if ( $dropdown ) { + wc_product_dropdown_categories( + apply_filters( + 'woocommerce_product_categories_widget_dropdown_args', + wp_parse_args( + $dropdown_args, + array( + 'show_count' => $count, + 'hierarchical' => $hierarchical, + 'show_uncategorized' => 0, + 'selected' => $this->current_cat ? $this->current_cat->slug : '', + ) + ) + ) + ); + + wp_enqueue_script( 'selectWoo' ); + wp_enqueue_style( 'select2' ); + + wc_enqueue_js( + " + jQuery( '.dropdown_product_cat' ).on( 'change', function() { + if ( jQuery(this).val() != '' ) { + var this_page = ''; + var home_url = '" . esc_js( home_url( '/' ) ) . "'; + if ( home_url.indexOf( '?' ) > 0 ) { + this_page = home_url + '&product_cat=' + jQuery(this).val(); + } else { + this_page = home_url + '?product_cat=' + jQuery(this).val(); + } + location.href = this_page; + } else { + location.href = '" . esc_js( wc_get_page_permalink( 'shop' ) ) . "'; + } + }); + + if ( jQuery().selectWoo ) { + var wc_product_cat_select = function() { + jQuery( '.dropdown_product_cat' ).selectWoo( { + placeholder: '" . esc_js( __( 'Select a category', 'woocommerce' ) ) . "', + minimumResultsForSearch: 5, + width: '100%', + allowClear: true, + language: { + noResults: function() { + return '" . esc_js( _x( 'No matches found', 'enhanced select', 'woocommerce' ) ) . "'; + } + } + } ); + }; + wc_product_cat_select(); + } + " + ); + } else { + include_once WC()->plugin_path() . '/includes/walkers/class-wc-product-cat-list-walker.php'; + + $list_args['walker'] = new WC_Product_Cat_List_Walker(); + $list_args['title_li'] = ''; + $list_args['pad_counts'] = 1; + $list_args['show_option_none'] = __( 'No product categories exist.', 'woocommerce' ); + $list_args['current_category'] = ( $this->current_cat ) ? $this->current_cat->term_id : ''; + $list_args['current_category_ancestors'] = $this->cat_ancestors; + $list_args['max_depth'] = $max_depth; + + echo '
      '; + + wp_list_categories( apply_filters( 'woocommerce_product_categories_widget_args', $list_args ) ); + + echo '
    '; + } + + $this->widget_end( $args ); + } +} diff --git a/includes/widgets/class-wc-widget-product-search.php b/includes/widgets/class-wc-widget-product-search.php new file mode 100644 index 0000000..c27debb --- /dev/null +++ b/includes/widgets/class-wc-widget-product-search.php @@ -0,0 +1,50 @@ +widget_cssclass = 'woocommerce widget_product_search'; + $this->widget_description = __( 'A search form for your store.', 'woocommerce' ); + $this->widget_id = 'woocommerce_product_search'; + $this->widget_name = __( 'Product Search', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => '', + 'label' => __( 'Title', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + $this->widget_start( $args, $instance ); + + get_product_search_form(); + + $this->widget_end( $args ); + } +} diff --git a/includes/widgets/class-wc-widget-product-tag-cloud.php b/includes/widgets/class-wc-widget-product-tag-cloud.php new file mode 100644 index 0000000..4d85bc0 --- /dev/null +++ b/includes/widgets/class-wc-widget-product-tag-cloud.php @@ -0,0 +1,121 @@ +widget_cssclass = 'woocommerce widget_product_tag_cloud'; + $this->widget_description = __( 'A cloud of your most used product tags.', 'woocommerce' ); + $this->widget_id = 'woocommerce_product_tag_cloud'; + $this->widget_name = __( 'Product Tag Cloud', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Product tags', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + $current_taxonomy = $this->get_current_taxonomy( $instance ); + + if ( empty( $instance['title'] ) ) { + $taxonomy = get_taxonomy( $current_taxonomy ); + $instance['title'] = $taxonomy->labels->name; + } + + $this->widget_start( $args, $instance ); + + echo '
    '; + + wp_tag_cloud( + apply_filters( + 'woocommerce_product_tag_cloud_widget_args', + array( + 'taxonomy' => $current_taxonomy, + 'topic_count_text_callback' => array( $this, 'topic_count_text' ), + ) + ) + ); + + echo '
    '; + + $this->widget_end( $args ); + } + + /** + * Return the taxonomy being displayed. + * + * @param object $instance Widget instance. + * @return string + */ + public function get_current_taxonomy( $instance ) { + return 'product_tag'; + } + + /** + * Returns topic count text. + * + * @since 3.4.0 + * @param int $count Count text. + * @return string + */ + public function topic_count_text( $count ) { + /* translators: %s: product count */ + return sprintf( _n( '%s product', '%s products', $count, 'woocommerce' ), number_format_i18n( $count ) ); + } + + // Ignore whole block to avoid warnings about PSR2.Methods.MethodDeclaration.Underscore violation. + // @codingStandardsIgnoreStart + /** + * Return the taxonomy being displayed. + * + * @deprecated 3.4.0 + * @param object $instance Widget instance. + * @return string + */ + public function _get_current_taxonomy( $instance ) { + wc_deprecated_function( '_get_current_taxonomy', '3.4.0', 'WC_Widget_Product_Tag_Cloud->get_current_taxonomy' ); + return $this->get_current_taxonomy( $instance ); + } + + /** + * Returns topic count text. + * + * @deprecated 3.4.0 + * @since 2.6.0 + * @param int $count Count text. + * @return string + */ + public function _topic_count_text( $count ) { + wc_deprecated_function( '_topic_count_text', '3.4.0', 'WC_Widget_Product_Tag_Cloud->topic_count_text' ); + return $this->topic_count_text( $count ); + } + // @codingStandardsIgnoreEnd +} diff --git a/includes/widgets/class-wc-widget-products.php b/includes/widgets/class-wc-widget-products.php new file mode 100644 index 0000000..a40c702 --- /dev/null +++ b/includes/widgets/class-wc-widget-products.php @@ -0,0 +1,216 @@ +widget_cssclass = 'woocommerce widget_products'; + $this->widget_description = __( "A list of your store's products.", 'woocommerce' ); + $this->widget_id = 'woocommerce_products'; + $this->widget_name = __( 'Products list', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Products', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'number' => array( + 'type' => 'number', + 'step' => 1, + 'min' => 1, + 'max' => '', + 'std' => 5, + 'label' => __( 'Number of products to show', 'woocommerce' ), + ), + 'show' => array( + 'type' => 'select', + 'std' => '', + 'label' => __( 'Show', 'woocommerce' ), + 'options' => array( + '' => __( 'All products', 'woocommerce' ), + 'featured' => __( 'Featured products', 'woocommerce' ), + 'onsale' => __( 'On-sale products', 'woocommerce' ), + ), + ), + 'orderby' => array( + 'type' => 'select', + 'std' => 'date', + 'label' => __( 'Order by', 'woocommerce' ), + 'options' => array( + 'date' => __( 'Date', 'woocommerce' ), + 'price' => __( 'Price', 'woocommerce' ), + 'rand' => __( 'Random', 'woocommerce' ), + 'sales' => __( 'Sales', 'woocommerce' ), + ), + ), + 'order' => array( + 'type' => 'select', + 'std' => 'desc', + 'label' => _x( 'Order', 'Sorting order', 'woocommerce' ), + 'options' => array( + 'asc' => __( 'ASC', 'woocommerce' ), + 'desc' => __( 'DESC', 'woocommerce' ), + ), + ), + 'hide_free' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Hide free products', 'woocommerce' ), + ), + 'show_hidden' => array( + 'type' => 'checkbox', + 'std' => 0, + 'label' => __( 'Show hidden products', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Query the products and return them. + * + * @param array $args Arguments. + * @param array $instance Widget instance. + * + * @return WP_Query + */ + public function get_products( $args, $instance ) { + $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; + $show = ! empty( $instance['show'] ) ? sanitize_title( $instance['show'] ) : $this->settings['show']['std']; + $orderby = ! empty( $instance['orderby'] ) ? sanitize_title( $instance['orderby'] ) : $this->settings['orderby']['std']; + $order = ! empty( $instance['order'] ) ? sanitize_title( $instance['order'] ) : $this->settings['order']['std']; + $product_visibility_term_ids = wc_get_product_visibility_term_ids(); + + $query_args = array( + 'posts_per_page' => $number, + 'post_status' => 'publish', + 'post_type' => 'product', + 'no_found_rows' => 1, + 'order' => $order, + 'meta_query' => array(), + 'tax_query' => array( + 'relation' => 'AND', + ), + ); // WPCS: slow query ok. + + if ( empty( $instance['show_hidden'] ) ) { + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => is_search() ? $product_visibility_term_ids['exclude-from-search'] : $product_visibility_term_ids['exclude-from-catalog'], + 'operator' => 'NOT IN', + ); + $query_args['post_parent'] = 0; + } + + if ( ! empty( $instance['hide_free'] ) ) { + $query_args['meta_query'][] = array( + 'key' => '_price', + 'value' => 0, + 'compare' => '>', + 'type' => 'DECIMAL', + ); + } + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $query_args['tax_query'][] = array( + array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_term_ids['outofstock'], + 'operator' => 'NOT IN', + ), + ); // WPCS: slow query ok. + } + + switch ( $show ) { + case 'featured': + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_term_ids['featured'], + ); + break; + case 'onsale': + $product_ids_on_sale = wc_get_product_ids_on_sale(); + $product_ids_on_sale[] = 0; + $query_args['post__in'] = $product_ids_on_sale; + break; + } + + switch ( $orderby ) { + case 'price': + $query_args['meta_key'] = '_price'; // WPCS: slow query ok. + $query_args['orderby'] = 'meta_value_num'; + break; + case 'rand': + $query_args['orderby'] = 'rand'; + break; + case 'sales': + $query_args['meta_key'] = 'total_sales'; // WPCS: slow query ok. + $query_args['orderby'] = 'meta_value_num'; + break; + default: + $query_args['orderby'] = 'date'; + } + + return new WP_Query( apply_filters( 'woocommerce_products_widget_query_args', $query_args ) ); + } + + /** + * Output widget. + * + * @param array $args Arguments. + * @param array $instance Widget instance. + * + * @see WP_Widget + */ + public function widget( $args, $instance ) { + if ( $this->get_cached_widget( $args ) ) { + return; + } + + ob_start(); + + wc_set_loop_prop( 'name', 'widget' ); + + $products = $this->get_products( $args, $instance ); + if ( $products && $products->have_posts() ) { + $this->widget_start( $args, $instance ); + + echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); + + $template_args = array( + 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, + 'show_rating' => true, + ); + + while ( $products->have_posts() ) { + $products->the_post(); + wc_get_template( 'content-widget-product.php', $template_args ); + } + + echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); + + $this->widget_end( $args ); + } + + wp_reset_postdata(); + + echo $this->cache_widget( $args, ob_get_clean() ); // WPCS: XSS ok. + } +} diff --git a/includes/widgets/class-wc-widget-rating-filter.php b/includes/widgets/class-wc-widget-rating-filter.php new file mode 100644 index 0000000..6690388 --- /dev/null +++ b/includes/widgets/class-wc-widget-rating-filter.php @@ -0,0 +1,147 @@ +widget_cssclass = 'woocommerce widget_rating_filter'; + $this->widget_description = __( 'Display a list of star ratings to filter products in your store.', 'woocommerce' ); + $this->widget_id = 'woocommerce_rating_filter'; + $this->widget_name = __( 'Filter Products by Rating', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Average rating', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + ); + parent::__construct(); + } + + /** + * Count products after other filters have occurred by adjusting the main query. + * + * @param int $rating Rating. + * @return int + */ + protected function get_filtered_product_count( $rating ) { + global $wpdb; + + $tax_query = WC_Query::get_main_tax_query(); + $meta_query = WC_Query::get_main_meta_query(); + + // Unset current rating filter. + foreach ( $tax_query as $key => $query ) { + if ( ! empty( $query['rating_filter'] ) ) { + unset( $tax_query[ $key ] ); + break; + } + } + + // Set new rating filter. + $product_visibility_terms = wc_get_product_visibility_term_ids(); + $tax_query[] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_terms[ 'rated-' . $rating ], + 'operator' => 'IN', + 'rating_filter' => true, + ); + + $meta_query = new WP_Meta_Query( $meta_query ); + $tax_query = new WP_Tax_Query( $tax_query ); + $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); + $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); + + $sql = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) FROM {$wpdb->posts} "; + $sql .= $tax_query_sql['join'] . $meta_query_sql['join']; + $sql .= " WHERE {$wpdb->posts}.post_type = 'product' AND {$wpdb->posts}.post_status = 'publish' "; + $sql .= $tax_query_sql['where'] . $meta_query_sql['where']; + + $search = WC_Query::get_main_search_query_sql(); + if ( $search ) { + $sql .= ' AND ' . $search; + } + + return absint( $wpdb->get_var( $sql ) ); // WPCS: unprepared SQL ok. + } + + /** + * Widget function. + * + * @see WP_Widget + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + if ( ! is_shop() && ! is_product_taxonomy() ) { + return; + } + + if ( ! WC()->query->get_main_query()->post_count ) { + return; + } + + ob_start(); + + $found = false; + $rating_filter = isset( $_GET['rating_filter'] ) ? array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) ) : array(); // WPCS: input var ok, CSRF ok, sanitization ok. + $base_link = remove_query_arg( 'paged', $this->get_current_page_url() ); + + $this->widget_start( $args, $instance ); + + echo '
      '; + + for ( $rating = 5; $rating >= 1; $rating-- ) { + $count = $this->get_filtered_product_count( $rating ); + if ( empty( $count ) ) { + continue; + } + $found = true; + $link = $base_link; + + if ( in_array( $rating, $rating_filter, true ) ) { + $link_ratings = implode( ',', array_diff( $rating_filter, array( $rating ) ) ); + } else { + $link_ratings = implode( ',', array_merge( $rating_filter, array( $rating ) ) ); + } + + $class = in_array( $rating, $rating_filter, true ) ? 'wc-layered-nav-rating chosen' : 'wc-layered-nav-rating'; + $link = apply_filters( 'woocommerce_rating_filter_link', $link_ratings ? add_query_arg( 'rating_filter', $link_ratings, $link ) : remove_query_arg( 'rating_filter' ) ); + $rating_html = wc_get_star_rating_html( $rating ); + $count_html = wp_kses( + apply_filters( 'woocommerce_rating_filter_count', "({$count})", $count, $rating ), + array( + 'em' => array(), + 'span' => array(), + 'strong' => array(), + ) + ); + + printf( '
    • %s %s
    • ', esc_attr( $class ), esc_url( $link ), $rating_html, $count_html ); // WPCS: XSS ok. + } + + echo '
    '; + + $this->widget_end( $args ); + + if ( ! $found ) { + ob_end_clean(); + } else { + echo ob_get_clean(); // WPCS: XSS ok. + } + } +} diff --git a/includes/widgets/class-wc-widget-recent-reviews.php b/includes/widgets/class-wc-widget-recent-reviews.php new file mode 100644 index 0000000..635e269 --- /dev/null +++ b/includes/widgets/class-wc-widget-recent-reviews.php @@ -0,0 +1,97 @@ +widget_cssclass = 'woocommerce widget_recent_reviews'; + $this->widget_description = __( 'Display a list of recent reviews from your store.', 'woocommerce' ); + $this->widget_id = 'woocommerce_recent_reviews'; + $this->widget_name = __( 'Recent Product Reviews', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Recent reviews', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'number' => array( + 'type' => 'number', + 'step' => 1, + 'min' => 1, + 'max' => '', + 'std' => 10, + 'label' => __( 'Number of reviews to show', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + global $comments, $comment; + + if ( $this->get_cached_widget( $args ) ) { + return; + } + + ob_start(); + + $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; + $comments = get_comments( + array( + 'number' => $number, + 'status' => 'approve', + 'post_status' => 'publish', + 'post_type' => 'product', + 'parent' => 0, + ) + ); // WPCS: override ok. + + if ( $comments ) { + $this->widget_start( $args, $instance ); + + echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_review_list', '
      ' ) ); + + foreach ( (array) $comments as $comment ) { + wc_get_template( + 'content-widget-reviews.php', + array( + 'comment' => $comment, + 'product' => wc_get_product( $comment->comment_post_ID ), + ) + ); + } + + echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_review_list', '
    ' ) ); + + $this->widget_end( $args ); + + } + + $content = ob_get_clean(); + + echo $content; // WPCS: XSS ok. + + $this->cache_widget( $args, $content ); + } +} diff --git a/includes/widgets/class-wc-widget-recently-viewed.php b/includes/widgets/class-wc-widget-recently-viewed.php new file mode 100644 index 0000000..e2b50ce --- /dev/null +++ b/includes/widgets/class-wc-widget-recently-viewed.php @@ -0,0 +1,110 @@ +widget_cssclass = 'woocommerce widget_recently_viewed_products'; + $this->widget_description = __( "Display a list of a customer's recently viewed products.", 'woocommerce' ); + $this->widget_id = 'woocommerce_recently_viewed_products'; + $this->widget_name = __( 'Recently Viewed Products list', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Recently Viewed Products', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'number' => array( + 'type' => 'number', + 'step' => 1, + 'min' => 1, + 'max' => 15, + 'std' => 10, + 'label' => __( 'Number of products to show', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + $viewed_products = ! empty( $_COOKIE['woocommerce_recently_viewed'] ) ? (array) explode( '|', wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) ) : array(); // @codingStandardsIgnoreLine + $viewed_products = array_reverse( array_filter( array_map( 'absint', $viewed_products ) ) ); + + if ( empty( $viewed_products ) ) { + return; + } + + ob_start(); + + $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; + + $query_args = array( + 'posts_per_page' => $number, + 'no_found_rows' => 1, + 'post_status' => 'publish', + 'post_type' => 'product', + 'post__in' => $viewed_products, + 'orderby' => 'post__in', + ); + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $query_args['tax_query'] = array( + array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'outofstock', + 'operator' => 'NOT IN', + ), + ); // WPCS: slow query ok. + } + + $r = new WP_Query( apply_filters( 'woocommerce_recently_viewed_products_widget_query_args', $query_args ) ); + + if ( $r->have_posts() ) { + + $this->widget_start( $args, $instance ); + + echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); + + $template_args = array( + 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, + ); + + while ( $r->have_posts() ) { + $r->the_post(); + wc_get_template( 'content-widget-product.php', $template_args ); + } + + echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); + + $this->widget_end( $args ); + } + + wp_reset_postdata(); + + $content = ob_get_clean(); + + echo $content; // WPCS: XSS ok. + } +} diff --git a/includes/widgets/class-wc-widget-top-rated-products.php b/includes/widgets/class-wc-widget-top-rated-products.php new file mode 100644 index 0000000..e6d95bf --- /dev/null +++ b/includes/widgets/class-wc-widget-top-rated-products.php @@ -0,0 +1,107 @@ +widget_cssclass = 'woocommerce widget_top_rated_products'; + $this->widget_description = __( "A list of your store's top-rated products.", 'woocommerce' ); + $this->widget_id = 'woocommerce_top_rated_products'; + $this->widget_name = __( 'Products by Rating list', 'woocommerce' ); + $this->settings = array( + 'title' => array( + 'type' => 'text', + 'std' => __( 'Top rated products', 'woocommerce' ), + 'label' => __( 'Title', 'woocommerce' ), + ), + 'number' => array( + 'type' => 'number', + 'step' => 1, + 'min' => 1, + 'max' => '', + 'std' => 5, + 'label' => __( 'Number of products to show', 'woocommerce' ), + ), + ); + + parent::__construct(); + } + + /** + * Output widget. + * + * @see WP_Widget + * @param array $args Arguments. + * @param array $instance Widget instance. + */ + public function widget( $args, $instance ) { + + if ( $this->get_cached_widget( $args ) ) { + return; + } + + ob_start(); + + $number = ! empty( $instance['number'] ) ? absint( $instance['number'] ) : $this->settings['number']['std']; + + $query_args = apply_filters( + 'woocommerce_top_rated_products_widget_args', + array( + 'posts_per_page' => $number, + 'no_found_rows' => 1, + 'post_status' => 'publish', + 'post_type' => 'product', + 'meta_key' => '_wc_average_rating', + 'orderby' => 'meta_value_num', + 'order' => 'DESC', + 'meta_query' => WC()->query->get_meta_query(), + 'tax_query' => WC()->query->get_tax_query(), + ) + ); // WPCS: slow query ok. + + $r = new WP_Query( $query_args ); + + if ( $r->have_posts() ) { + + $this->widget_start( $args, $instance ); + + echo wp_kses_post( apply_filters( 'woocommerce_before_widget_product_list', '
      ' ) ); + + $template_args = array( + 'widget_id' => isset( $args['widget_id'] ) ? $args['widget_id'] : $this->widget_id, + 'show_rating' => true, + ); + + while ( $r->have_posts() ) { + $r->the_post(); + wc_get_template( 'content-widget-product.php', $template_args ); + } + + echo wp_kses_post( apply_filters( 'woocommerce_after_widget_product_list', '
    ' ) ); + + $this->widget_end( $args ); + } + + wp_reset_postdata(); + + $content = ob_get_clean(); + + echo $content; // WPCS: XSS ok. + + $this->cache_widget( $args, $content ); + } +} diff --git a/lib/packages/League/Container/Argument/ArgumentResolverInterface.php b/lib/packages/League/Container/Argument/ArgumentResolverInterface.php new file mode 100644 index 0000000..9c7393f --- /dev/null +++ b/lib/packages/League/Container/Argument/ArgumentResolverInterface.php @@ -0,0 +1,28 @@ +getValue(); + } elseif ($argument instanceof ClassNameInterface) { + $id = $argument->getClassName(); + } elseif (!is_string($argument)) { + return $argument; + } else { + $justStringValue = true; + $id = $argument; + } + + $container = null; + + try { + $container = $this->getLeagueContainer(); + } catch (ContainerException $e) { + if ($this instanceof ReflectionContainer) { + $container = $this; + } + } + + if ($container !== null) { + try { + return $container->get($id); + } catch (NotFoundException $exception) { + if ($argument instanceof ClassNameWithOptionalValue) { + return $argument->getOptionalValue(); + } + + if ($justStringValue) { + return $id; + } + + throw $exception; + } + } + + if ($argument instanceof ClassNameWithOptionalValue) { + return $argument->getOptionalValue(); + } + + // Just a string value. + return $id; + }, $arguments); + } + + /** + * {@inheritdoc} + */ + public function reflectArguments(ReflectionFunctionAbstract $method, array $args = []) : array + { + $arguments = array_map(function (ReflectionParameter $param) use ($method, $args) { + $name = $param->getName(); + $type = $param->getType(); + + if (array_key_exists($name, $args)) { + return new RawArgument($args[$name]); + } + + if ($type) { + if (PHP_VERSION_ID >= 70200) { + $typeName = $type->getName(); + } else { + $typeName = (string) $type; + } + + $typeName = ltrim($typeName, '?'); + + if ($param->isDefaultValueAvailable()) { + return new ClassNameWithOptionalValue($typeName, $param->getDefaultValue()); + } + + return new ClassName($typeName); + } + + if ($param->isDefaultValueAvailable()) { + return new RawArgument($param->getDefaultValue()); + } + + throw new NotFoundException(sprintf( + 'Unable to resolve a value for parameter (%s) in the function/method (%s)', + $name, + $method->getName() + )); + }, $method->getParameters()); + + return $this->resolveArguments($arguments); + } + + /** + * @return ContainerInterface + */ + abstract public function getContainer() : ContainerInterface; + + /** + * @return Container + */ + abstract public function getLeagueContainer() : Container; +} diff --git a/lib/packages/League/Container/Argument/ClassName.php b/lib/packages/League/Container/Argument/ClassName.php new file mode 100644 index 0000000..bab358f --- /dev/null +++ b/lib/packages/League/Container/Argument/ClassName.php @@ -0,0 +1,29 @@ +value = $value; + } + + /** + * {@inheritdoc} + */ + public function getClassName() : string + { + return $this->value; + } +} diff --git a/lib/packages/League/Container/Argument/ClassNameInterface.php b/lib/packages/League/Container/Argument/ClassNameInterface.php new file mode 100644 index 0000000..ab2708e --- /dev/null +++ b/lib/packages/League/Container/Argument/ClassNameInterface.php @@ -0,0 +1,13 @@ +className = $className; + $this->optionalValue = $optionalValue; + } + + /** + * @inheritDoc + */ + public function getClassName(): string + { + return $this->className; + } + + public function getOptionalValue() + { + return $this->optionalValue; + } +} diff --git a/lib/packages/League/Container/Argument/RawArgument.php b/lib/packages/League/Container/Argument/RawArgument.php new file mode 100644 index 0000000..fe0ddd0 --- /dev/null +++ b/lib/packages/League/Container/Argument/RawArgument.php @@ -0,0 +1,29 @@ +value = $value; + } + + /** + * {@inheritdoc} + */ + public function getValue() + { + return $this->value; + } +} diff --git a/lib/packages/League/Container/Argument/RawArgumentInterface.php b/lib/packages/League/Container/Argument/RawArgumentInterface.php new file mode 100644 index 0000000..8730cac --- /dev/null +++ b/lib/packages/League/Container/Argument/RawArgumentInterface.php @@ -0,0 +1,13 @@ +definitions = $definitions ?? new DefinitionAggregate; + $this->providers = $providers ?? new ServiceProviderAggregate; + $this->inflectors = $inflectors ?? new InflectorAggregate; + + if ($this->definitions instanceof ContainerAwareInterface) { + $this->definitions->setLeagueContainer($this); + } + + if ($this->providers instanceof ContainerAwareInterface) { + $this->providers->setLeagueContainer($this); + } + + if ($this->inflectors instanceof ContainerAwareInterface) { + $this->inflectors->setLeagueContainer($this); + } + } + + /** + * Add an item to the container. + * + * @param string $id + * @param mixed $concrete + * @param boolean $shared + * + * @return DefinitionInterface + */ + public function add(string $id, $concrete = null, bool $shared = null) : DefinitionInterface + { + $concrete = $concrete ?? $id; + $shared = $shared ?? $this->defaultToShared; + + return $this->definitions->add($id, $concrete, $shared); + } + + /** + * Proxy to add with shared as true. + * + * @param string $id + * @param mixed $concrete + * + * @return DefinitionInterface + */ + public function share(string $id, $concrete = null) : DefinitionInterface + { + return $this->add($id, $concrete, true); + } + + /** + * Whether the container should default to defining shared definitions. + * + * @param boolean $shared + * + * @return self + */ + public function defaultToShared(bool $shared = true) : ContainerInterface + { + $this->defaultToShared = $shared; + + return $this; + } + + /** + * Get a definition to extend. + * + * @param string $id [description] + * + * @return DefinitionInterface + */ + public function extend(string $id) : DefinitionInterface + { + if ($this->providers->provides($id)) { + $this->providers->register($id); + } + + if ($this->definitions->has($id)) { + return $this->definitions->getDefinition($id); + } + + throw new NotFoundException( + sprintf('Unable to extend alias (%s) as it is not being managed as a definition', $id) + ); + } + + /** + * Add a service provider. + * + * @param ServiceProviderInterface|string $provider + * + * @return self + */ + public function addServiceProvider($provider) : self + { + $this->providers->add($provider); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get($id, bool $new = false) + { + if ($this->definitions->has($id)) { + $resolved = $this->definitions->resolve($id, $new); + return $this->inflectors->inflect($resolved); + } + + if ($this->definitions->hasTag($id)) { + $arrayOf = $this->definitions->resolveTagged($id, $new); + + array_walk($arrayOf, function (&$resolved) { + $resolved = $this->inflectors->inflect($resolved); + }); + + return $arrayOf; + } + + if ($this->providers->provides($id)) { + $this->providers->register($id); + + if (!$this->definitions->has($id) && !$this->definitions->hasTag($id)) { + throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id)); + } + + return $this->get($id, $new); + } + + foreach ($this->delegates as $delegate) { + if ($delegate->has($id)) { + $resolved = $delegate->get($id); + return $this->inflectors->inflect($resolved); + } + } + + throw new NotFoundException(sprintf('Alias (%s) is not being managed by the container or delegates', $id)); + } + + /** + * {@inheritdoc} + */ + public function has($id) : bool + { + if ($this->definitions->has($id)) { + return true; + } + + if ($this->definitions->hasTag($id)) { + return true; + } + + if ($this->providers->provides($id)) { + return true; + } + + foreach ($this->delegates as $delegate) { + if ($delegate->has($id)) { + return true; + } + } + + return false; + } + + /** + * Allows for manipulation of specific types on resolution. + * + * @param string $type + * @param callable|null $callback + * + * @return InflectorInterface + */ + public function inflector(string $type, callable $callback = null) : InflectorInterface + { + return $this->inflectors->add($type, $callback); + } + + /** + * Delegate a backup container to be checked for services if it + * cannot be resolved via this container. + * + * @param ContainerInterface $container + * + * @return self + */ + public function delegate(ContainerInterface $container) : self + { + $this->delegates[] = $container; + + if ($container instanceof ContainerAwareInterface) { + $container->setLeagueContainer($this); + } + + return $this; + } +} diff --git a/lib/packages/League/Container/ContainerAwareInterface.php b/lib/packages/League/Container/ContainerAwareInterface.php new file mode 100644 index 0000000..b0a4f91 --- /dev/null +++ b/lib/packages/League/Container/ContainerAwareInterface.php @@ -0,0 +1,40 @@ +container = $container; + + return $this; + } + + /** + * Get the container. + * + * @return ContainerInterface + */ + public function getContainer() : ContainerInterface + { + if ($this->container instanceof ContainerInterface) { + return $this->container; + } + + throw new ContainerException('No container implementation has been set.'); + } + + /** + * Set a container. + * + * @param Container $container + * + * @return self + */ + public function setLeagueContainer(Container $container) : ContainerAwareInterface + { + $this->container = $container; + $this->leagueContainer = $container; + + return $this; + } + + /** + * Get the container. + * + * @return Container + */ + public function getLeagueContainer() : Container + { + if ($this->leagueContainer instanceof Container) { + return $this->leagueContainer; + } + + throw new ContainerException('No container implementation has been set.'); + } +} diff --git a/lib/packages/League/Container/Definition/Definition.php b/lib/packages/League/Container/Definition/Definition.php new file mode 100644 index 0000000..c357080 --- /dev/null +++ b/lib/packages/League/Container/Definition/Definition.php @@ -0,0 +1,274 @@ +alias = $id; + $this->concrete = $concrete; + } + + /** + * {@inheritdoc} + */ + public function addTag(string $tag) : DefinitionInterface + { + $this->tags[$tag] = true; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasTag(string $tag) : bool + { + return isset($this->tags[$tag]); + } + + /** + * {@inheritdoc} + */ + public function setAlias(string $id) : DefinitionInterface + { + $this->alias = $id; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAlias() : string + { + return $this->alias; + } + + /** + * {@inheritdoc} + */ + public function setShared(bool $shared = true) : DefinitionInterface + { + $this->shared = $shared; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function isShared() : bool + { + return $this->shared; + } + + /** + * {@inheritdoc} + */ + public function getConcrete() + { + return $this->concrete; + } + + /** + * {@inheritdoc} + */ + public function setConcrete($concrete) : DefinitionInterface + { + $this->concrete = $concrete; + $this->resolved = null; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addArgument($arg) : DefinitionInterface + { + $this->arguments[] = $arg; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addArguments(array $args) : DefinitionInterface + { + foreach ($args as $arg) { + $this->addArgument($arg); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addMethodCall(string $method, array $args = []) : DefinitionInterface + { + $this->methods[] = [ + 'method' => $method, + 'arguments' => $args + ]; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addMethodCalls(array $methods = []) : DefinitionInterface + { + foreach ($methods as $method => $args) { + $this->addMethodCall($method, $args); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function resolve(bool $new = false) + { + $concrete = $this->concrete; + + if ($this->isShared() && $this->resolved !== null && $new === false) { + return $this->resolved; + } + + if (is_callable($concrete)) { + $concrete = $this->resolveCallable($concrete); + } + + if ($concrete instanceof RawArgumentInterface) { + $this->resolved = $concrete->getValue(); + + return $concrete->getValue(); + } + + if ($concrete instanceof ClassNameInterface) { + $concrete = $concrete->getClassName(); + } + + if (is_string($concrete) && class_exists($concrete)) { + $concrete = $this->resolveClass($concrete); + } + + if (is_object($concrete)) { + $concrete = $this->invokeMethods($concrete); + } + + $this->resolved = $concrete; + + return $concrete; + } + + /** + * Resolve a callable. + * + * @param callable $concrete + * + * @return mixed + */ + protected function resolveCallable(callable $concrete) + { + $resolved = $this->resolveArguments($this->arguments); + + return call_user_func_array($concrete, $resolved); + } + + /** + * Resolve a class. + * + * @param string $concrete + * + * @return object + * + * @throws ReflectionException + */ + protected function resolveClass(string $concrete) + { + $resolved = $this->resolveArguments($this->arguments); + $reflection = new ReflectionClass($concrete); + + return $reflection->newInstanceArgs($resolved); + } + + /** + * Invoke methods on resolved instance. + * + * @param object $instance + * + * @return object + */ + protected function invokeMethods($instance) + { + foreach ($this->methods as $method) { + $args = $this->resolveArguments($method['arguments']); + + /** @var callable $callable */ + $callable = [$instance, $method['method']]; + call_user_func_array($callable, $args); + } + + return $instance; + } +} diff --git a/lib/packages/League/Container/Definition/DefinitionAggregate.php b/lib/packages/League/Container/Definition/DefinitionAggregate.php new file mode 100644 index 0000000..3e39b2f --- /dev/null +++ b/lib/packages/League/Container/Definition/DefinitionAggregate.php @@ -0,0 +1,124 @@ +definitions = array_filter($definitions, function ($definition) { + return ($definition instanceof DefinitionInterface); + }); + } + + /** + * {@inheritdoc} + */ + public function add(string $id, $definition, bool $shared = false) : DefinitionInterface + { + if (!$definition instanceof DefinitionInterface) { + $definition = new Definition($id, $definition); + } + + $this->definitions[] = $definition + ->setAlias($id) + ->setShared($shared) + ; + + return $definition; + } + + /** + * {@inheritdoc} + */ + public function has(string $id) : bool + { + foreach ($this->getIterator() as $definition) { + if ($id === $definition->getAlias()) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function hasTag(string $tag) : bool + { + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getDefinition(string $id) : DefinitionInterface + { + foreach ($this->getIterator() as $definition) { + if ($id === $definition->getAlias()) { + return $definition->setLeagueContainer($this->getLeagueContainer()); + } + } + + throw new NotFoundException(sprintf('Alias (%s) is not being handled as a definition.', $id)); + } + + /** + * {@inheritdoc} + */ + public function resolve(string $id, bool $new = false) + { + return $this->getDefinition($id)->resolve($new); + } + + /** + * {@inheritdoc} + */ + public function resolveTagged(string $tag, bool $new = false) : array + { + $arrayOf = []; + + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + $arrayOf[] = $definition->setLeagueContainer($this->getLeagueContainer())->resolve($new); + } + } + + return $arrayOf; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->definitions); + + for ($i = 0; $i < $count; $i++) { + yield $this->definitions[$i]; + } + } +} diff --git a/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php b/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php new file mode 100644 index 0000000..2d5842f --- /dev/null +++ b/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php @@ -0,0 +1,67 @@ +type = $type; + $this->callback = $callback; + } + + /** + * {@inheritdoc} + */ + public function getType() : string + { + return $this->type; + } + + /** + * {@inheritdoc} + */ + public function invokeMethod(string $name, array $args) : InflectorInterface + { + $this->methods[$name] = $args; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function invokeMethods(array $methods) : InflectorInterface + { + foreach ($methods as $name => $args) { + $this->invokeMethod($name, $args); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setProperty(string $property, $value) : InflectorInterface + { + $this->properties[$property] = $this->resolveArguments([$value])[0]; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setProperties(array $properties) : InflectorInterface + { + foreach ($properties as $property => $value) { + $this->setProperty($property, $value); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function inflect($object) + { + $properties = $this->resolveArguments(array_values($this->properties)); + $properties = array_combine(array_keys($this->properties), $properties); + + // array_combine() can technically return false + foreach ($properties ?: [] as $property => $value) { + $object->{$property} = $value; + } + + foreach ($this->methods as $method => $args) { + $args = $this->resolveArguments($args); + + /** @var callable $callable */ + $callable = [$object, $method]; + call_user_func_array($callable, $args); + } + + if ($this->callback !== null) { + call_user_func($this->callback, $object); + } + } +} diff --git a/lib/packages/League/Container/Inflector/InflectorAggregate.php b/lib/packages/League/Container/Inflector/InflectorAggregate.php new file mode 100644 index 0000000..4db8471 --- /dev/null +++ b/lib/packages/League/Container/Inflector/InflectorAggregate.php @@ -0,0 +1,58 @@ +inflectors[] = $inflector; + + return $inflector; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->inflectors); + + for ($i = 0; $i < $count; $i++) { + yield $this->inflectors[$i]; + } + } + + /** + * {@inheritdoc} + */ + public function inflect($object) + { + foreach ($this->getIterator() as $inflector) { + $type = $inflector->getType(); + + if (! $object instanceof $type) { + continue; + } + + $inflector->setLeagueContainer($this->getLeagueContainer()); + $inflector->inflect($object); + } + + return $object; + } +} diff --git a/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php b/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php new file mode 100644 index 0000000..aac6455 --- /dev/null +++ b/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php @@ -0,0 +1,27 @@ +cacheResolutions === true && array_key_exists($id, $this->cache)) { + return $this->cache[$id]; + } + + if (! $this->has($id)) { + throw new NotFoundException( + sprintf('Alias (%s) is not an existing class and therefore cannot be resolved', $id) + ); + } + + $reflector = new ReflectionClass($id); + $construct = $reflector->getConstructor(); + + $resolution = $construct === null + ? new $id + : $resolution = $reflector->newInstanceArgs($this->reflectArguments($construct, $args)) + ; + + if ($this->cacheResolutions === true) { + $this->cache[$id] = $resolution; + } + + return $resolution; + } + + /** + * {@inheritdoc} + */ + public function has($id) : bool + { + return class_exists($id); + } + + /** + * Invoke a callable via the container. + * + * @param callable $callable + * @param array $args + * + * @return mixed + * + * @throws ReflectionException + */ + public function call(callable $callable, array $args = []) + { + if (is_string($callable) && strpos($callable, '::') !== false) { + $callable = explode('::', $callable); + } + + if (is_array($callable)) { + if (is_string($callable[0])) { + $callable[0] = $this->getContainer()->get($callable[0]); + } + + $reflection = new ReflectionMethod($callable[0], $callable[1]); + + if ($reflection->isStatic()) { + $callable[0] = null; + } + + return $reflection->invokeArgs($callable[0], $this->reflectArguments($reflection, $args)); + } + + if (is_object($callable)) { + $reflection = new ReflectionMethod($callable, '__invoke'); + + return $reflection->invokeArgs($callable, $this->reflectArguments($reflection, $args)); + } + + $reflection = new ReflectionFunction(\Closure::fromCallable($callable)); + + return $reflection->invokeArgs($this->reflectArguments($reflection, $args)); + } + + /** + * Whether the container should default to caching resolutions and returning + * the cache on following calls. + * + * @param boolean $option + * + * @return self + */ + public function cacheResolutions(bool $option = true) : ContainerInterface + { + $this->cacheResolutions = $option; + + return $this; + } +} diff --git a/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php b/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php new file mode 100644 index 0000000..ed6af44 --- /dev/null +++ b/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php @@ -0,0 +1,46 @@ +provides, true); + } + + /** + * {@inheritdoc} + */ + public function setIdentifier(string $id) : ServiceProviderInterface + { + $this->identifier = $id; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getIdentifier() : string + { + return $this->identifier ?? get_class($this); + } +} diff --git a/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php b/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php new file mode 100644 index 0000000..195b48a --- /dev/null +++ b/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php @@ -0,0 +1,14 @@ +getContainer()->has($provider)) { + $provider = $this->getContainer()->get($provider); + } elseif (is_string($provider) && class_exists($provider)) { + $provider = new $provider; + } + + if (in_array($provider, $this->providers, true)) { + return $this; + } + + if ($provider instanceof ContainerAwareInterface) { + $provider->setLeagueContainer($this->getLeagueContainer()); + } + + if ($provider instanceof BootableServiceProviderInterface) { + $provider->boot(); + } + + if ($provider instanceof ServiceProviderInterface) { + $this->providers[] = $provider; + + return $this; + } + + throw new ContainerException( + 'A service provider must be a fully qualified class name or instance ' . + 'of (\Automattic\WooCommerce\Vendor\League\Container\ServiceProvider\ServiceProviderInterface)' + ); + } + + /** + * {@inheritdoc} + */ + public function provides(string $service) : bool + { + foreach ($this->getIterator() as $provider) { + if ($provider->provides($service)) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function getIterator() : Generator + { + $count = count($this->providers); + + for ($i = 0; $i < $count; $i++) { + yield $this->providers[$i]; + } + } + + /** + * {@inheritdoc} + */ + public function register(string $service) + { + if (false === $this->provides($service)) { + throw new ContainerException( + sprintf('(%s) is not provided by a service provider', $service) + ); + } + + foreach ($this->getIterator() as $provider) { + if (in_array($provider->getIdentifier(), $this->registered, true)) { + continue; + } + + if ($provider->provides($service)) { + $provider->register(); + $this->registered[] = $provider->getIdentifier(); + } + } + } +} diff --git a/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php b/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php new file mode 100644 index 0000000..c2f61d6 --- /dev/null +++ b/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php @@ -0,0 +1,36 @@ +leagueContainer property or the `getLeagueContainer` method + * from the ContainerAwareTrait. + * + * @return void + */ + public function register(); + + /** + * Set a custom id for the service provider. This enables + * registering the same service provider multiple times. + * + * @param string $id + * + * @return self + */ + public function setIdentifier(string $id) : ServiceProviderInterface; + + /** + * The id of the service provider uniquely identifies it, so + * that we can quickly determine if it has already been registered. + * Defaults to get_class($provider). + * + * @return string + */ + public function getIdentifier() : string; +} diff --git a/license.txt b/license.txt new file mode 100644 index 0000000..35db42d --- /dev/null +++ b/license.txt @@ -0,0 +1,710 @@ +WooCommerce - eCommerce for WordPress + +Copyright 2015 by the contributors + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +This program incorporates work covered by the following copyright and +permission notices: + + Jigoshop is Copyright (c) 2011 Jigowatt Ltd. + http://jigowatt.com - http://jigoshop.com + + Jigoshop is released under the GPL + +and + + WooCommerce - eCommerce for WordPress + + WooCommerce is Copyright (c) 2015 WooThemes + + WooCommerce is released under the GPL + +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright © 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright © + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright © + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/packages/action-scheduler/action-scheduler.php b/packages/action-scheduler/action-scheduler.php new file mode 100644 index 0000000..a6f0c6e --- /dev/null +++ b/packages/action-scheduler/action-scheduler.php @@ -0,0 +1,65 @@ +. + * + * @package ActionScheduler + */ + +if ( ! function_exists( 'action_scheduler_register_3_dot_3_dot_0' ) && function_exists( 'add_action' ) ) { + + if ( ! class_exists( 'ActionScheduler_Versions' ) ) { + require_once __DIR__ . '/classes/ActionScheduler_Versions.php'; + add_action( 'plugins_loaded', array( 'ActionScheduler_Versions', 'initialize_latest_version' ), 1, 0 ); + } + + add_action( 'plugins_loaded', 'action_scheduler_register_3_dot_3_dot_0', 0, 0 ); + + /** + * Registers this version of Action Scheduler. + */ + function action_scheduler_register_3_dot_3_dot_0() { + $versions = ActionScheduler_Versions::instance(); + $versions->register( '3.3.0', 'action_scheduler_initialize_3_dot_3_dot_0' ); + } + + /** + * Initializes this version of Action Scheduler. + */ + function action_scheduler_initialize_3_dot_3_dot_0() { + // A final safety check is required even here, because historic versions of Action Scheduler + // followed a different pattern (in some unusual cases, we could reach this point and the + // ActionScheduler class is already defined—so we need to guard against that). + if ( ! class_exists( 'ActionScheduler' ) ) { + require_once __DIR__ . '/classes/abstracts/ActionScheduler.php'; + ActionScheduler::init( __FILE__ ); + } + } + + // Support usage in themes - load this version if no plugin has loaded a version yet. + if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) && ! class_exists( 'ActionScheduler' ) ) { + action_scheduler_initialize_3_dot_3_dot_0(); + do_action( 'action_scheduler_pre_theme_init' ); + ActionScheduler_Versions::initialize_latest_version(); + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_ActionClaim.php b/packages/action-scheduler/classes/ActionScheduler_ActionClaim.php new file mode 100644 index 0000000..8b56816 --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_ActionClaim.php @@ -0,0 +1,23 @@ +id = $id; + $this->action_ids = $action_ids; + } + + public function get_id() { + return $this->id; + } + + public function get_actions() { + return $this->action_ids; + } +} + \ No newline at end of file diff --git a/packages/action-scheduler/classes/ActionScheduler_ActionFactory.php b/packages/action-scheduler/classes/ActionScheduler_ActionFactory.php new file mode 100644 index 0000000..592dbc5 --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_ActionFactory.php @@ -0,0 +1,179 @@ +get_date() ); + } + break; + default : + $action_class = 'ActionScheduler_FinishedAction'; + break; + } + + $action_class = apply_filters( 'action_scheduler_stored_action_class', $action_class, $status, $hook, $args, $schedule, $group ); + + $action = new $action_class( $hook, $args, $schedule, $group ); + + /** + * Allow 3rd party code to change the instantiated action for a given hook, args, schedule and group. + * + * @param ActionScheduler_Action $action The instantiated action. + * @param string $hook The instantiated action's hook. + * @param array $args The instantiated action's args. + * @param ActionScheduler_Schedule $schedule The instantiated action's schedule. + * @param string $group The instantiated action's group. + */ + return apply_filters( 'action_scheduler_stored_action_instance', $action, $hook, $args, $schedule, $group ); + } + + /** + * Enqueue an action to run one time, as soon as possible (rather a specific scheduled time). + * + * This method creates a new action with the NULLSchedule. This schedule maps to a MySQL datetime string of + * 0000-00-00 00:00:00. This is done to create a psuedo "async action" type that is fully backward compatible. + * Existing queries to claim actions claim by date, meaning actions scheduled for 0000-00-00 00:00:00 will + * always be claimed prior to actions scheduled for a specific date. This makes sure that any async action is + * given priority in queue processing. This has the added advantage of making sure async actions can be + * claimed by both the existing WP Cron and WP CLI runners, as well as a new async request runner. + * + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass when the hook is triggered + * @param string $group A group to put the action in + * + * @return int The ID of the stored action + */ + public function async( $hook, $args = array(), $group = '' ) { + $schedule = new ActionScheduler_NullSchedule(); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $this->store( $action ); + } + + /** + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass when the hook is triggered + * @param int $when Unix timestamp when the action will run + * @param string $group A group to put the action in + * + * @return int The ID of the stored action + */ + public function single( $hook, $args = array(), $when = null, $group = '' ) { + $date = as_get_datetime_object( $when ); + $schedule = new ActionScheduler_SimpleSchedule( $date ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $this->store( $action ); + } + + /** + * Create the first instance of an action recurring on a given interval. + * + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass when the hook is triggered + * @param int $first Unix timestamp for the first run + * @param int $interval Seconds between runs + * @param string $group A group to put the action in + * + * @return int The ID of the stored action + */ + public function recurring( $hook, $args = array(), $first = null, $interval = null, $group = '' ) { + if ( empty($interval) ) { + return $this->single( $hook, $args, $first, $group ); + } + $date = as_get_datetime_object( $first ); + $schedule = new ActionScheduler_IntervalSchedule( $date, $interval ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $this->store( $action ); + } + + /** + * Create the first instance of an action recurring on a Cron schedule. + * + * @param string $hook The hook to trigger when this action runs + * @param array $args Args to pass when the hook is triggered + * @param int $base_timestamp The first instance of the action will be scheduled + * to run at a time calculated after this timestamp matching the cron + * expression. This can be used to delay the first instance of the action. + * @param int $schedule A cron definition string + * @param string $group A group to put the action in + * + * @return int The ID of the stored action + */ + public function cron( $hook, $args = array(), $base_timestamp = null, $schedule = null, $group = '' ) { + if ( empty($schedule) ) { + return $this->single( $hook, $args, $base_timestamp, $group ); + } + $date = as_get_datetime_object( $base_timestamp ); + $cron = CronExpression::factory( $schedule ); + $schedule = new ActionScheduler_CronSchedule( $date, $cron ); + $action = new ActionScheduler_Action( $hook, $args, $schedule, $group ); + return $this->store( $action ); + } + + /** + * Create a successive instance of a recurring or cron action. + * + * Importantly, the action will be rescheduled to run based on the current date/time. + * That means when the action is scheduled to run in the past, the next scheduled date + * will be pushed forward. For example, if a recurring action set to run every hour + * was scheduled to run 5 seconds ago, it will be next scheduled for 1 hour in the + * future, which is 1 hour and 5 seconds from when it was last scheduled to run. + * + * Alternatively, if the action is scheduled to run in the future, and is run early, + * likely via manual intervention, then its schedule will change based on the time now. + * For example, if a recurring action set to run every day, and is run 12 hours early, + * it will run again in 24 hours, not 36 hours. + * + * This slippage is less of an issue with Cron actions, as the specific run time can + * be set for them to run, e.g. 1am each day. In those cases, and entire period would + * need to be missed before there was any change is scheduled, e.g. in the case of an + * action scheduled for 1am each day, the action would need to run an entire day late. + * + * @param ActionScheduler_Action $action The existing action. + * + * @return string The ID of the stored action + * @throws InvalidArgumentException If $action is not a recurring action. + */ + public function repeat( $action ) { + $schedule = $action->get_schedule(); + $next = $schedule->get_next( as_get_datetime_object() ); + + if ( is_null( $next ) || ! $schedule->is_recurring() ) { + throw new InvalidArgumentException( __( 'Invalid action - must be a recurring action.', 'woocommerce' ) ); + } + + $schedule_class = get_class( $schedule ); + $new_schedule = new $schedule( $next, $schedule->get_recurrence(), $schedule->get_first_date() ); + $new_action = new ActionScheduler_Action( $action->get_hook(), $action->get_args(), $new_schedule, $action->get_group() ); + return $this->store( $new_action ); + } + + /** + * @param ActionScheduler_Action $action + * + * @return int The ID of the stored action + */ + protected function store( ActionScheduler_Action $action ) { + $store = ActionScheduler_Store::instance(); + return $store->save_action( $action ); + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_AdminView.php b/packages/action-scheduler/classes/ActionScheduler_AdminView.php new file mode 100644 index 0000000..d4f25fd --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_AdminView.php @@ -0,0 +1,154 @@ +render(); + } + + /** + * Registers action-scheduler into WooCommerce > System status. + * + * @param array $tabs An associative array of tab key => label. + * @return array $tabs An associative array of tab key => label, including Action Scheduler's tabs + */ + public function register_system_status_tab( array $tabs ) { + $tabs['action-scheduler'] = __( 'Scheduled Actions', 'woocommerce' ); + + return $tabs; + } + + /** + * Include Action Scheduler's administration under the Tools menu. + * + * A menu under the Tools menu is important for backward compatibility (as that's + * where it started), and also provides more convenient access than the WooCommerce + * System Status page, and for sites where WooCommerce isn't active. + */ + public function register_menu() { + $hook_suffix = add_submenu_page( + 'tools.php', + __( 'Scheduled Actions', 'woocommerce' ), + __( 'Scheduled Actions', 'woocommerce' ), + 'manage_options', + 'action-scheduler', + array( $this, 'render_admin_ui' ) + ); + add_action( 'load-' . $hook_suffix , array( $this, 'process_admin_ui' ) ); + } + + /** + * Triggers processing of any pending actions. + */ + public function process_admin_ui() { + $this->get_list_table(); + } + + /** + * Renders the Admin UI + */ + public function render_admin_ui() { + $table = $this->get_list_table(); + $table->display_page(); + } + + /** + * Get the admin UI object and process any requested actions. + * + * @return ActionScheduler_ListTable + */ + protected function get_list_table() { + if ( null === $this->list_table ) { + $this->list_table = new ActionScheduler_ListTable( ActionScheduler::store(), ActionScheduler::logger(), ActionScheduler::runner() ); + $this->list_table->process_actions(); + } + + return $this->list_table; + } + + /** + * Provide more information about the screen and its data in the help tab. + */ + public function add_help_tabs() { + $screen = get_current_screen(); + + if ( ! $screen || self::$screen_id != $screen->id ) { + return; + } + + $as_version = ActionScheduler_Versions::instance()->latest_version(); + $screen->add_help_tab( + array( + 'id' => 'action_scheduler_about', + 'title' => __( 'About', 'woocommerce' ), + 'content' => + '

    ' . sprintf( __( 'About Action Scheduler %s', 'woocommerce' ), $as_version ) . '

    ' . + '

    ' . + __( 'Action Scheduler is a scalable, traceable job queue for background processing large sets of actions. Action Scheduler works by triggering an action hook to run at some time in the future. Scheduled actions can also be scheduled to run on a recurring schedule.', 'woocommerce' ) . + '

    ', + ) + ); + + $screen->add_help_tab( + array( + 'id' => 'action_scheduler_columns', + 'title' => __( 'Columns', 'woocommerce' ), + 'content' => + '

    ' . __( 'Scheduled Action Columns', 'woocommerce' ) . '

    ' . + '
      ' . + sprintf( '
    • %1$s: %2$s
    • ', __( 'Hook', 'woocommerce' ), __( 'Name of the action hook that will be triggered.', 'woocommerce' ) ) . + sprintf( '
    • %1$s: %2$s
    • ', __( 'Status', 'woocommerce' ), __( 'Action statuses are Pending, Complete, Canceled, Failed', 'woocommerce' ) ) . + sprintf( '
    • %1$s: %2$s
    • ', __( 'Arguments', 'woocommerce' ), __( 'Optional data array passed to the action hook.', 'woocommerce' ) ) . + sprintf( '
    • %1$s: %2$s
    • ', __( 'Group', 'woocommerce' ), __( 'Optional action group.', 'woocommerce' ) ) . + sprintf( '
    • %1$s: %2$s
    • ', __( 'Recurrence', 'woocommerce' ), __( 'The action\'s schedule frequency.', 'woocommerce' ) ) . + sprintf( '
    • %1$s: %2$s
    • ', __( 'Scheduled', 'woocommerce' ), __( 'The date/time the action is/was scheduled to run.', 'woocommerce' ) ) . + sprintf( '
    • %1$s: %2$s
    • ', __( 'Log', 'woocommerce' ), __( 'Activity log for the action.', 'woocommerce' ) ) . + '
    ', + ) + ); + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php b/packages/action-scheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php new file mode 100644 index 0000000..57706a2 --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_AsyncRequest_QueueRunner.php @@ -0,0 +1,97 @@ +store = $store; + } + + /** + * Handle async requests + * + * Run a queue, and maybe dispatch another async request to run another queue + * if there are still pending actions after completing a queue in this request. + */ + protected function handle() { + do_action( 'action_scheduler_run_queue', 'Async Request' ); // run a queue in the same way as WP Cron, but declare the Async Request context + + $sleep_seconds = $this->get_sleep_seconds(); + + if ( $sleep_seconds ) { + sleep( $sleep_seconds ); + } + + $this->maybe_dispatch(); + } + + /** + * If the async request runner is needed and allowed to run, dispatch a request. + */ + public function maybe_dispatch() { + if ( ! $this->allow() ) { + return; + } + + $this->dispatch(); + ActionScheduler_QueueRunner::instance()->unhook_dispatch_async_request(); + } + + /** + * Only allow async requests when needed. + * + * Also allow 3rd party code to disable running actions via async requests. + */ + protected function allow() { + + if ( ! has_action( 'action_scheduler_run_queue' ) || ActionScheduler::runner()->has_maximum_concurrent_batches() || ! $this->store->has_pending_actions_due() ) { + $allow = false; + } else { + $allow = true; + } + + return apply_filters( 'action_scheduler_allow_async_request_runner', $allow ); + } + + /** + * Chaining async requests can crash MySQL. A brief sleep call in PHP prevents that. + */ + protected function get_sleep_seconds() { + return apply_filters( 'action_scheduler_async_request_sleep_seconds', 5, $this ); + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_Compatibility.php b/packages/action-scheduler/classes/ActionScheduler_Compatibility.php new file mode 100644 index 0000000..387ee7f --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_Compatibility.php @@ -0,0 +1,99 @@ + $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) { + if ( false !== @ini_set( 'memory_limit', $filtered_limit ) ) { + return $filtered_limit; + } else { + return false; + } + } elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) { + if ( false !== @ini_set( 'memory_limit', $wp_max_limit ) ) { + return $wp_max_limit; + } else { + return false; + } + } + return false; + } + + /** + * Attempts to raise the PHP timeout for time intensive processes. + * + * Only allows raising the existing limit and prevents lowering it. Wrapper for wc_set_time_limit(), when available. + * + * @param int The time limit in seconds. + */ + public static function raise_time_limit( $limit = 0 ) { + if ( $limit < ini_get( 'max_execution_time' ) ) { + return; + } + + if ( function_exists( 'wc_set_time_limit' ) ) { + wc_set_time_limit( $limit ); + } elseif ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved + @set_time_limit( $limit ); + } + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_DataController.php b/packages/action-scheduler/classes/ActionScheduler_DataController.php new file mode 100644 index 0000000..a9f5434 --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_DataController.php @@ -0,0 +1,187 @@ +=' ); + return $php_support && apply_filters( 'action_scheduler_migration_dependencies_met', true ); + } + + /** + * Get a flag indicating whether the migration is complete. + * + * @return bool Whether the flag has been set marking the migration as complete + */ + public static function is_migration_complete() { + return get_option( self::STATUS_FLAG ) === self::STATUS_COMPLETE; + } + + /** + * Mark the migration as complete. + */ + public static function mark_migration_complete() { + update_option( self::STATUS_FLAG, self::STATUS_COMPLETE ); + } + + /** + * Unmark migration when a plugin is de-activated. Will not work in case of silent activation, for example in an update. + * We do this to mitigate the bug of lost actions which happens if there was an AS 2.x to AS 3.x migration in the past, but that plugin is now + * deactivated and the site was running on AS 2.x again. + */ + public static function mark_migration_incomplete() { + delete_option( self::STATUS_FLAG ); + } + + /** + * Set the action store class name. + * + * @param string $class Classname of the store class. + * + * @return string + */ + public static function set_store_class( $class ) { + return self::DATASTORE_CLASS; + } + + /** + * Set the action logger class name. + * + * @param string $class Classname of the logger class. + * + * @return string + */ + public static function set_logger_class( $class ) { + return self::LOGGER_CLASS; + } + + /** + * Set the sleep time in seconds. + * + * @param integer $sleep_time The number of seconds to pause before resuming operation. + */ + public static function set_sleep_time( $sleep_time ) { + self::$sleep_time = (int) $sleep_time; + } + + /** + * Set the tick count required for freeing memory. + * + * @param integer $free_ticks The number of ticks to free memory on. + */ + public static function set_free_ticks( $free_ticks ) { + self::$free_ticks = (int) $free_ticks; + } + + /** + * Free memory if conditions are met. + * + * @param int $ticks Current tick count. + */ + public static function maybe_free_memory( $ticks ) { + if ( self::$free_ticks && 0 === $ticks % self::$free_ticks ) { + self::free_memory(); + } + } + + /** + * Reduce memory footprint by clearing the database query and object caches. + */ + public static function free_memory() { + if ( 0 < self::$sleep_time ) { + /* translators: %d: amount of time */ + \WP_CLI::warning( sprintf( _n( 'Stopped the insanity for %d second', 'Stopped the insanity for %d seconds', self::$sleep_time, 'woocommerce' ), self::$sleep_time ) ); + sleep( self::$sleep_time ); + } + + \WP_CLI::warning( __( 'Attempting to reduce used memory...', 'woocommerce' ) ); + + /** + * @var $wpdb \wpdb + * @var $wp_object_cache \WP_Object_Cache + */ + global $wpdb, $wp_object_cache; + + $wpdb->queries = array(); + + if ( ! is_a( $wp_object_cache, 'WP_Object_Cache' ) ) { + return; + } + + $wp_object_cache->group_ops = array(); + $wp_object_cache->stats = array(); + $wp_object_cache->memcache_debug = array(); + $wp_object_cache->cache = array(); + + if ( is_callable( array( $wp_object_cache, '__remoteset' ) ) ) { + call_user_func( array( $wp_object_cache, '__remoteset' ) ); // important + } + } + + /** + * Connect to table datastores if migration is complete. + * Otherwise, proceed with the migration if the dependencies have been met. + */ + public static function init() { + if ( self::is_migration_complete() ) { + add_filter( 'action_scheduler_store_class', array( 'ActionScheduler_DataController', 'set_store_class' ), 100 ); + add_filter( 'action_scheduler_logger_class', array( 'ActionScheduler_DataController', 'set_logger_class' ), 100 ); + add_action( 'deactivate_plugin', array( 'ActionScheduler_DataController', 'mark_migration_incomplete' ) ); + } elseif ( self::dependencies_met() ) { + Controller::init(); + } + + add_action( 'action_scheduler/progress_tick', array( 'ActionScheduler_DataController', 'maybe_free_memory' ) ); + } + + /** + * Singleton factory. + */ + public static function instance() { + if ( ! isset( self::$instance ) ) { + self::$instance = new static(); + } + + return self::$instance; + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_DateTime.php b/packages/action-scheduler/classes/ActionScheduler_DateTime.php new file mode 100644 index 0000000..5e8743c --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_DateTime.php @@ -0,0 +1,76 @@ +format( 'U' ); + } + + /** + * Set the UTC offset. + * + * This represents a fixed offset instead of a timezone setting. + * + * @param $offset + */ + public function setUtcOffset( $offset ) { + $this->utcOffset = intval( $offset ); + } + + /** + * Returns the timezone offset. + * + * @return int + * @link http://php.net/manual/en/datetime.getoffset.php + */ + public function getOffset() { + return $this->utcOffset ? $this->utcOffset : parent::getOffset(); + } + + /** + * Set the TimeZone associated with the DateTime + * + * @param DateTimeZone $timezone + * + * @return static + * @link http://php.net/manual/en/datetime.settimezone.php + */ + public function setTimezone( $timezone ) { + $this->utcOffset = 0; + parent::setTimezone( $timezone ); + + return $this; + } + + /** + * Get the timestamp with the WordPress timezone offset added or subtracted. + * + * @since 3.0.0 + * @return int + */ + public function getOffsetTimestamp() { + return $this->getTimestamp() + $this->getOffset(); + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_Exception.php b/packages/action-scheduler/classes/ActionScheduler_Exception.php new file mode 100644 index 0000000..353d3c0 --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_Exception.php @@ -0,0 +1,11 @@ +store = $store; + } + + public function attach( ActionScheduler_ActionClaim $claim ) { + $this->claim = $claim; + add_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); + add_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0, 1 ); + add_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0, 0 ); + add_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0, 0 ); + add_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0, 0 ); + } + + public function detach() { + $this->claim = NULL; + $this->untrack_action(); + remove_action( 'shutdown', array( $this, 'handle_unexpected_shutdown' ) ); + remove_action( 'action_scheduler_before_execute', array( $this, 'track_current_action' ), 0 ); + remove_action( 'action_scheduler_after_execute', array( $this, 'untrack_action' ), 0 ); + remove_action( 'action_scheduler_execution_ignored', array( $this, 'untrack_action' ), 0 ); + remove_action( 'action_scheduler_failed_execution', array( $this, 'untrack_action' ), 0 ); + } + + public function track_current_action( $action_id ) { + $this->action_id = $action_id; + } + + public function untrack_action() { + $this->action_id = 0; + } + + public function handle_unexpected_shutdown() { + if ( $error = error_get_last() ) { + if ( in_array( $error['type'], array( E_ERROR, E_PARSE, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ) ) ) { + if ( !empty($this->action_id) ) { + $this->store->mark_failure( $this->action_id ); + do_action( 'action_scheduler_unexpected_shutdown', $this->action_id, $error ); + } + } + $this->store->release_claim( $this->claim ); + } + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_InvalidActionException.php b/packages/action-scheduler/classes/ActionScheduler_InvalidActionException.php new file mode 100644 index 0000000..0a9b5bd --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_InvalidActionException.php @@ -0,0 +1,47 @@ + label). + * + * @var array + */ + protected $columns = array(); + + /** + * Actions (name => label). + * + * @var array + */ + protected $row_actions = array(); + + /** + * The active data stores + * + * @var ActionScheduler_Store + */ + protected $store; + + /** + * A logger to use for getting action logs to display + * + * @var ActionScheduler_Logger + */ + protected $logger; + + /** + * A ActionScheduler_QueueRunner runner instance (or child class) + * + * @var ActionScheduler_QueueRunner + */ + protected $runner; + + /** + * Bulk actions. The key of the array is the method name of the implementation: + * + * bulk_(array $ids, string $sql_in). + * + * See the comments in the parent class for further details + * + * @var array + */ + protected $bulk_actions = array(); + + /** + * Flag variable to render our notifications, if any, once. + * + * @var bool + */ + protected static $did_notification = false; + + /** + * Array of seconds for common time periods, like week or month, alongside an internationalised string representation, i.e. "Day" or "Days" + * + * @var array + */ + private static $time_periods; + + /** + * Sets the current data store object into `store->action` and initialises the object. + * + * @param ActionScheduler_Store $store + * @param ActionScheduler_Logger $logger + * @param ActionScheduler_QueueRunner $runner + */ + public function __construct( ActionScheduler_Store $store, ActionScheduler_Logger $logger, ActionScheduler_QueueRunner $runner ) { + + $this->store = $store; + $this->logger = $logger; + $this->runner = $runner; + + $this->table_header = __( 'Scheduled Actions', 'woocommerce' ); + + $this->bulk_actions = array( + 'delete' => __( 'Delete', 'woocommerce' ), + ); + + $this->columns = array( + 'hook' => __( 'Hook', 'woocommerce' ), + 'status' => __( 'Status', 'woocommerce' ), + 'args' => __( 'Arguments', 'woocommerce' ), + 'group' => __( 'Group', 'woocommerce' ), + 'recurrence' => __( 'Recurrence', 'woocommerce' ), + 'schedule' => __( 'Scheduled Date', 'woocommerce' ), + 'log_entries' => __( 'Log', 'woocommerce' ), + ); + + $this->sort_by = array( + 'schedule', + 'hook', + 'group', + ); + + $this->search_by = array( + 'hook', + 'args', + 'claim_id', + ); + + $request_status = $this->get_request_status(); + + if ( empty( $request_status ) ) { + $this->sort_by[] = 'status'; + } elseif ( in_array( $request_status, array( 'in-progress', 'failed' ) ) ) { + $this->columns += array( 'claim_id' => __( 'Claim ID', 'woocommerce' ) ); + $this->sort_by[] = 'claim_id'; + } + + $this->row_actions = array( + 'hook' => array( + 'run' => array( + 'name' => __( 'Run', 'woocommerce' ), + 'desc' => __( 'Process the action now as if it were run as part of a queue', 'woocommerce' ), + ), + 'cancel' => array( + 'name' => __( 'Cancel', 'woocommerce' ), + 'desc' => __( 'Cancel the action now to avoid it being run in future', 'woocommerce' ), + 'class' => 'cancel trash', + ), + ), + ); + + self::$time_periods = array( + array( + 'seconds' => YEAR_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s year', '%s years', 'woocommerce' ), + ), + array( + 'seconds' => MONTH_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s month', '%s months', 'woocommerce' ), + ), + array( + 'seconds' => WEEK_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s week', '%s weeks', 'woocommerce' ), + ), + array( + 'seconds' => DAY_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s day', '%s days', 'woocommerce' ), + ), + array( + 'seconds' => HOUR_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s hour', '%s hours', 'woocommerce' ), + ), + array( + 'seconds' => MINUTE_IN_SECONDS, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s minute', '%s minutes', 'woocommerce' ), + ), + array( + 'seconds' => 1, + /* translators: %s: amount of time */ + 'names' => _n_noop( '%s second', '%s seconds', 'woocommerce' ), + ), + ); + + parent::__construct( array( + 'singular' => 'action-scheduler', + 'plural' => 'action-scheduler', + 'ajax' => false, + ) ); + } + + /** + * Convert an interval of seconds into a two part human friendly string. + * + * The WordPress human_time_diff() function only calculates the time difference to one degree, meaning + * even if an action is 1 day and 11 hours away, it will display "1 day". This function goes one step + * further to display two degrees of accuracy. + * + * Inspired by the Crontrol::interval() function by Edward Dale: https://wordpress.org/plugins/wp-crontrol/ + * + * @param int $interval A interval in seconds. + * @param int $periods_to_include Depth of time periods to include, e.g. for an interval of 70, and $periods_to_include of 2, both minutes and seconds would be included. With a value of 1, only minutes would be included. + * @return string A human friendly string representation of the interval. + */ + private static function human_interval( $interval, $periods_to_include = 2 ) { + + if ( $interval <= 0 ) { + return __( 'Now!', 'woocommerce' ); + } + + $output = ''; + + for ( $time_period_index = 0, $periods_included = 0, $seconds_remaining = $interval; $time_period_index < count( self::$time_periods ) && $seconds_remaining > 0 && $periods_included < $periods_to_include; $time_period_index++ ) { + + $periods_in_interval = floor( $seconds_remaining / self::$time_periods[ $time_period_index ]['seconds'] ); + + if ( $periods_in_interval > 0 ) { + if ( ! empty( $output ) ) { + $output .= ' '; + } + $output .= sprintf( _n( self::$time_periods[ $time_period_index ]['names'][0], self::$time_periods[ $time_period_index ]['names'][1], $periods_in_interval, 'woocommerce' ), $periods_in_interval ); + $seconds_remaining -= $periods_in_interval * self::$time_periods[ $time_period_index ]['seconds']; + $periods_included++; + } + } + + return $output; + } + + /** + * Returns the recurrence of an action or 'Non-repeating'. The output is human readable. + * + * @param ActionScheduler_Action $action + * + * @return string + */ + protected function get_recurrence( $action ) { + $schedule = $action->get_schedule(); + if ( $schedule->is_recurring() ) { + $recurrence = $schedule->get_recurrence(); + + if ( is_numeric( $recurrence ) ) { + /* translators: %s: time interval */ + return sprintf( __( 'Every %s', 'woocommerce' ), self::human_interval( $recurrence ) ); + } else { + return $recurrence; + } + } + + return __( 'Non-repeating', 'woocommerce' ); + } + + /** + * Serializes the argument of an action to render it in a human friendly format. + * + * @param array $row The array representation of the current row of the table + * + * @return string + */ + public function column_args( array $row ) { + if ( empty( $row['args'] ) ) { + return apply_filters( 'action_scheduler_list_table_column_args', '', $row ); + } + + $row_html = '
      '; + foreach ( $row['args'] as $key => $value ) { + $row_html .= sprintf( '
    • %s => %s
    • ', esc_html( var_export( $key, true ) ), esc_html( var_export( $value, true ) ) ); + } + $row_html .= '
    '; + + return apply_filters( 'action_scheduler_list_table_column_args', $row_html, $row ); + } + + /** + * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal. + * + * @param array $row Action array. + * @return string + */ + public function column_log_entries( array $row ) { + + $log_entries_html = '
      '; + + $timezone = new DateTimezone( 'UTC' ); + + foreach ( $row['log_entries'] as $log_entry ) { + $log_entries_html .= $this->get_log_entry_html( $log_entry, $timezone ); + } + + $log_entries_html .= '
    '; + + return $log_entries_html; + } + + /** + * Prints the logs entries inline. We do so to avoid loading Javascript and other hacks to show it in a modal. + * + * @param ActionScheduler_LogEntry $log_entry + * @param DateTimezone $timezone + * @return string + */ + protected function get_log_entry_html( ActionScheduler_LogEntry $log_entry, DateTimezone $timezone ) { + $date = $log_entry->get_date(); + $date->setTimezone( $timezone ); + return sprintf( '
  • %s
    %s
  • ', esc_html( $date->format( 'Y-m-d H:i:s O' ) ), esc_html( $log_entry->get_message() ) ); + } + + /** + * Only display row actions for pending actions. + * + * @param array $row Row to render + * @param string $column_name Current row + * + * @return string + */ + protected function maybe_render_actions( $row, $column_name ) { + if ( 'pending' === strtolower( $row[ 'status_name' ] ) ) { + return parent::maybe_render_actions( $row, $column_name ); + } + + return ''; + } + + /** + * Renders admin notifications + * + * Notifications: + * 1. When the maximum number of tasks are being executed simultaneously. + * 2. Notifications when a task is manually executed. + * 3. Tables are missing. + */ + public function display_admin_notices() { + global $wpdb; + + if ( ( is_a( $this->store, 'ActionScheduler_HybridStore' ) || is_a( $this->store, 'ActionScheduler_DBStore' ) ) && apply_filters( 'action_scheduler_enable_recreate_data_store', true ) ) { + $table_list = array( + 'actionscheduler_actions', + 'actionscheduler_logs', + 'actionscheduler_groups', + 'actionscheduler_claims', + ); + + $found_tables = $wpdb->get_col( "SHOW TABLES LIKE '{$wpdb->prefix}actionscheduler%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + foreach ( $table_list as $table_name ) { + if ( ! in_array( $wpdb->prefix . $table_name, $found_tables ) ) { + $this->admin_notices[] = array( + 'class' => 'error', + 'message' => __( 'It appears one or more database tables were missing. Attempting to re-create the missing table(s).' , 'woocommerce' ), + ); + $this->recreate_tables(); + parent::display_admin_notices(); + + return; + } + } + } + + if ( $this->runner->has_maximum_concurrent_batches() ) { + $claim_count = $this->store->get_claim_count(); + $this->admin_notices[] = array( + 'class' => 'updated', + 'message' => sprintf( + /* translators: %s: amount of claims */ + _n( + 'Maximum simultaneous queues already in progress (%s queue). No additional queues will begin processing until the current queues are complete.', + 'Maximum simultaneous queues already in progress (%s queues). No additional queues will begin processing until the current queues are complete.', + $claim_count, + 'woocommerce' + ), + $claim_count + ), + ); + } elseif ( $this->store->has_pending_actions_due() ) { + + $async_request_lock_expiration = ActionScheduler::lock()->get_expiration( 'async-request-runner' ); + + // No lock set or lock expired + if ( false === $async_request_lock_expiration || $async_request_lock_expiration < time() ) { + $in_progress_url = add_query_arg( 'status', 'in-progress', remove_query_arg( 'status' ) ); + /* translators: %s: process URL */ + $async_request_message = sprintf( __( 'A new queue has begun processing. View actions in-progress »', 'woocommerce' ), esc_url( $in_progress_url ) ); + } else { + /* translators: %d: seconds */ + $async_request_message = sprintf( __( 'The next queue will begin processing in approximately %d seconds.', 'woocommerce' ), $async_request_lock_expiration - time() ); + } + + $this->admin_notices[] = array( + 'class' => 'notice notice-info', + 'message' => $async_request_message, + ); + } + + $notification = get_transient( 'action_scheduler_admin_notice' ); + + if ( is_array( $notification ) ) { + delete_transient( 'action_scheduler_admin_notice' ); + + $action = $this->store->fetch_action( $notification['action_id'] ); + $action_hook_html = '' . $action->get_hook() . ''; + if ( 1 == $notification['success'] ) { + $class = 'updated'; + switch ( $notification['row_action_type'] ) { + case 'run' : + /* translators: %s: action HTML */ + $action_message_html = sprintf( __( 'Successfully executed action: %s', 'woocommerce' ), $action_hook_html ); + break; + case 'cancel' : + /* translators: %s: action HTML */ + $action_message_html = sprintf( __( 'Successfully canceled action: %s', 'woocommerce' ), $action_hook_html ); + break; + default : + /* translators: %s: action HTML */ + $action_message_html = sprintf( __( 'Successfully processed change for action: %s', 'woocommerce' ), $action_hook_html ); + break; + } + } else { + $class = 'error'; + /* translators: 1: action HTML 2: action ID 3: error message */ + $action_message_html = sprintf( __( 'Could not process change for action: "%1$s" (ID: %2$d). Error: %3$s', 'woocommerce' ), $action_hook_html, esc_html( $notification['action_id'] ), esc_html( $notification['error_message'] ) ); + } + + $action_message_html = apply_filters( 'action_scheduler_admin_notice_html', $action_message_html, $action, $notification ); + + $this->admin_notices[] = array( + 'class' => $class, + 'message' => $action_message_html, + ); + } + + parent::display_admin_notices(); + } + + /** + * Prints the scheduled date in a human friendly format. + * + * @param array $row The array representation of the current row of the table + * + * @return string + */ + public function column_schedule( $row ) { + return $this->get_schedule_display_string( $row['schedule'] ); + } + + /** + * Get the scheduled date in a human friendly format. + * + * @param ActionScheduler_Schedule $schedule + * @return string + */ + protected function get_schedule_display_string( ActionScheduler_Schedule $schedule ) { + + $schedule_display_string = ''; + + if ( ! $schedule->get_date() ) { + return '0000-00-00 00:00:00'; + } + + $next_timestamp = $schedule->get_date()->getTimestamp(); + + $schedule_display_string .= $schedule->get_date()->format( 'Y-m-d H:i:s O' ); + $schedule_display_string .= '
    '; + + if ( gmdate( 'U' ) > $next_timestamp ) { + /* translators: %s: date interval */ + $schedule_display_string .= sprintf( __( ' (%s ago)', 'woocommerce' ), self::human_interval( gmdate( 'U' ) - $next_timestamp ) ); + } else { + /* translators: %s: date interval */ + $schedule_display_string .= sprintf( __( ' (%s)', 'woocommerce' ), self::human_interval( $next_timestamp - gmdate( 'U' ) ) ); + } + + return $schedule_display_string; + } + + /** + * Bulk delete + * + * Deletes actions based on their ID. This is the handler for the bulk delete. It assumes the data + * properly validated by the callee and it will delete the actions without any extra validation. + * + * @param array $ids + * @param string $ids_sql Inherited and unused + */ + protected function bulk_delete( array $ids, $ids_sql ) { + foreach ( $ids as $id ) { + $this->store->delete_action( $id ); + } + } + + /** + * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their + * parameters are valid. + * + * @param int $action_id + */ + protected function row_action_cancel( $action_id ) { + $this->process_row_action( $action_id, 'cancel' ); + } + + /** + * Implements the logic behind running an action. ActionScheduler_Abstract_ListTable validates the request and their + * parameters are valid. + * + * @param int $action_id + */ + protected function row_action_run( $action_id ) { + $this->process_row_action( $action_id, 'run' ); + } + + /** + * Force the data store schema updates. + */ + protected function recreate_tables() { + if ( is_a( $this->store, 'ActionScheduler_HybridStore' ) ) { + $store = $this->store; + } else { + $store = new ActionScheduler_HybridStore(); + } + add_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10, 2 ); + + $store_schema = new ActionScheduler_StoreSchema(); + $logger_schema = new ActionScheduler_LoggerSchema(); + $store_schema->register_tables( true ); + $logger_schema->register_tables( true ); + + remove_action( 'action_scheduler/created_table', array( $store, 'set_autoincrement' ), 10 ); + } + /** + * Implements the logic behind processing an action once an action link is clicked on the list table. + * + * @param int $action_id + * @param string $row_action_type The type of action to perform on the action. + */ + protected function process_row_action( $action_id, $row_action_type ) { + try { + switch ( $row_action_type ) { + case 'run' : + $this->runner->process_action( $action_id, 'Admin List Table' ); + break; + case 'cancel' : + $this->store->cancel_action( $action_id ); + break; + } + $success = 1; + $error_message = ''; + } catch ( Exception $e ) { + $success = 0; + $error_message = $e->getMessage(); + } + + set_transient( 'action_scheduler_admin_notice', compact( 'action_id', 'success', 'error_message', 'row_action_type' ), 30 ); + } + + /** + * {@inheritDoc} + */ + public function prepare_items() { + $this->prepare_column_headers(); + + $per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page ); + $query = array( + 'per_page' => $per_page, + 'offset' => $this->get_items_offset(), + 'status' => $this->get_request_status(), + 'orderby' => $this->get_request_orderby(), + 'order' => $this->get_request_order(), + 'search' => $this->get_request_search_query(), + ); + + $this->items = array(); + + $total_items = $this->store->query_actions( $query, 'count' ); + + $status_labels = $this->store->get_status_labels(); + + foreach ( $this->store->query_actions( $query ) as $action_id ) { + try { + $action = $this->store->fetch_action( $action_id ); + } catch ( Exception $e ) { + continue; + } + if ( is_a( $action, 'ActionScheduler_NullAction' ) ) { + continue; + } + $this->items[ $action_id ] = array( + 'ID' => $action_id, + 'hook' => $action->get_hook(), + 'status_name' => $this->store->get_status( $action_id ), + 'status' => $status_labels[ $this->store->get_status( $action_id ) ], + 'args' => $action->get_args(), + 'group' => $action->get_group(), + 'log_entries' => $this->logger->get_logs( $action_id ), + 'claim_id' => $this->store->get_claim_id( $action_id ), + 'recurrence' => $this->get_recurrence( $action ), + 'schedule' => $action->get_schedule(), + ); + } + + $this->set_pagination_args( array( + 'total_items' => $total_items, + 'per_page' => $per_page, + 'total_pages' => ceil( $total_items / $per_page ), + ) ); + } + + /** + * Prints the available statuses so the user can click to filter. + */ + protected function display_filter_by_status() { + $this->status_counts = $this->store->action_counts(); + parent::display_filter_by_status(); + } + + /** + * Get the text to display in the search box on the list table. + */ + protected function get_search_box_button_text() { + return __( 'Search hook, args and claim ID', 'woocommerce' ); + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_LogEntry.php b/packages/action-scheduler/classes/ActionScheduler_LogEntry.php new file mode 100644 index 0000000..649636d --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_LogEntry.php @@ -0,0 +1,67 @@ +comment_type + * to ActionScheduler_LogEntry::__construct(), goodness knows why, and the Follow-up Emails plugin + * hard-codes loading its own version of ActionScheduler_wpCommentLogger with that out-dated method, + * goodness knows why, so we need to guard against that here instead of using a DateTime type declaration + * for the constructor's 3rd param of $date and causing a fatal error with older versions of FUE. + */ + if ( null !== $date && ! is_a( $date, 'DateTime' ) ) { + _doing_it_wrong( __METHOD__, 'The third parameter must be a valid DateTime instance, or null.', '2.0.0' ); + $date = null; + } + + $this->action_id = $action_id; + $this->message = $message; + $this->date = $date ? $date : new Datetime; + } + + /** + * Returns the date when this log entry was created + * + * @return Datetime + */ + public function get_date() { + return $this->date; + } + + public function get_action_id() { + return $this->action_id; + } + + public function get_message() { + return $this->message; + } +} + diff --git a/packages/action-scheduler/classes/ActionScheduler_NullLogEntry.php b/packages/action-scheduler/classes/ActionScheduler_NullLogEntry.php new file mode 100644 index 0000000..6f8f218 --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_NullLogEntry.php @@ -0,0 +1,11 @@ +maybe_dispatch_async_request() uses a lock to avoid + * calling ActionScheduler_QueueRunner->has_maximum_concurrent_batches() every time the 'shutdown', + * hook is triggered, because that method calls ActionScheduler_QueueRunner->store->get_claim_count() + * to find the current number of claims in the database. + * + * @param string $lock_type A string to identify different lock types. + * @bool True if lock value has changed, false if not or if set failed. + */ + public function set( $lock_type ) { + return update_option( $this->get_key( $lock_type ), time() + $this->get_duration( $lock_type ) ); + } + + /** + * If a lock is set, return the timestamp it was set to expiry. + * + * @param string $lock_type A string to identify different lock types. + * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. + */ + public function get_expiration( $lock_type ) { + return get_option( $this->get_key( $lock_type ) ); + } + + /** + * Get the key to use for storing the lock in the transient + * + * @param string $lock_type A string to identify different lock types. + * @return string + */ + protected function get_key( $lock_type ) { + return sprintf( 'action_scheduler_lock_%s', $lock_type ); + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_QueueCleaner.php b/packages/action-scheduler/classes/ActionScheduler_QueueCleaner.php new file mode 100644 index 0000000..49cd44b --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_QueueCleaner.php @@ -0,0 +1,158 @@ +store = $store ? $store : ActionScheduler_Store::instance(); + $this->batch_size = $batch_size; + } + + public function delete_old_actions() { + $lifespan = apply_filters( 'action_scheduler_retention_period', $this->month_in_seconds ); + $cutoff = as_get_datetime_object($lifespan.' seconds ago'); + + $statuses_to_purge = array( + ActionScheduler_Store::STATUS_COMPLETE, + ActionScheduler_Store::STATUS_CANCELED, + ); + + foreach ( $statuses_to_purge as $status ) { + $actions_to_delete = $this->store->query_actions( array( + 'status' => $status, + 'modified' => $cutoff, + 'modified_compare' => '<=', + 'per_page' => $this->get_batch_size(), + 'orderby' => 'none', + ) ); + + foreach ( $actions_to_delete as $action_id ) { + try { + $this->store->delete_action( $action_id ); + } catch ( Exception $e ) { + + /** + * Notify 3rd party code of exceptions when deleting a completed action older than the retention period + * + * This hook provides a way for 3rd party code to log or otherwise handle exceptions relating to their + * actions. + * + * @since 2.0.0 + * + * @param int $action_id The scheduled actions ID in the data store + * @param Exception $e The exception thrown when attempting to delete the action from the data store + * @param int $lifespan The retention period, in seconds, for old actions + * @param int $count_of_actions_to_delete The number of old actions being deleted in this batch + */ + do_action( 'action_scheduler_failed_old_action_deletion', $action_id, $e, $lifespan, count( $actions_to_delete ) ); + } + } + } + } + + /** + * Unclaim pending actions that have not been run within a given time limit. + * + * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed + * as a parameter is 10x the time limit used for queue processing. + * + * @param int $time_limit The number of seconds to allow a queue to run before unclaiming its pending actions. Default 300 (5 minutes). + */ + public function reset_timeouts( $time_limit = 300 ) { + $timeout = apply_filters( 'action_scheduler_timeout_period', $time_limit ); + if ( $timeout < 0 ) { + return; + } + $cutoff = as_get_datetime_object($timeout.' seconds ago'); + $actions_to_reset = $this->store->query_actions( array( + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'modified' => $cutoff, + 'modified_compare' => '<=', + 'claimed' => true, + 'per_page' => $this->get_batch_size(), + 'orderby' => 'none', + ) ); + + foreach ( $actions_to_reset as $action_id ) { + $this->store->unclaim_action( $action_id ); + do_action( 'action_scheduler_reset_action', $action_id ); + } + } + + /** + * Mark actions that have been running for more than a given time limit as failed, based on + * the assumption some uncatachable and unloggable fatal error occurred during processing. + * + * When called by ActionScheduler_Abstract_QueueRunner::run_cleanup(), the time limit passed + * as a parameter is 10x the time limit used for queue processing. + * + * @param int $time_limit The number of seconds to allow an action to run before it is considered to have failed. Default 300 (5 minutes). + */ + public function mark_failures( $time_limit = 300 ) { + $timeout = apply_filters( 'action_scheduler_failure_period', $time_limit ); + if ( $timeout < 0 ) { + return; + } + $cutoff = as_get_datetime_object($timeout.' seconds ago'); + $actions_to_reset = $this->store->query_actions( array( + 'status' => ActionScheduler_Store::STATUS_RUNNING, + 'modified' => $cutoff, + 'modified_compare' => '<=', + 'per_page' => $this->get_batch_size(), + 'orderby' => 'none', + ) ); + + foreach ( $actions_to_reset as $action_id ) { + $this->store->mark_failure( $action_id ); + do_action( 'action_scheduler_failed_action', $action_id, $timeout ); + } + } + + /** + * Do all of the cleaning actions. + * + * @param int $time_limit The number of seconds to use as the timeout and failure period. Default 300 (5 minutes). + * @author Jeremy Pry + */ + public function clean( $time_limit = 300 ) { + $this->delete_old_actions(); + $this->reset_timeouts( $time_limit ); + $this->mark_failures( $time_limit ); + } + + /** + * Get the batch size for cleaning the queue. + * + * @author Jeremy Pry + * @return int + */ + protected function get_batch_size() { + /** + * Filter the batch size when cleaning the queue. + * + * @param int $batch_size The number of actions to clean in one batch. + */ + return absint( apply_filters( 'action_scheduler_cleanup_batch_size', $this->batch_size ) ); + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_QueueRunner.php b/packages/action-scheduler/classes/ActionScheduler_QueueRunner.php new file mode 100644 index 0000000..d2ac090 --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_QueueRunner.php @@ -0,0 +1,197 @@ +store ); + } + + $this->async_request = $async_request; + } + + /** + * @codeCoverageIgnore + */ + public function init() { + + add_filter( 'cron_schedules', array( self::instance(), 'add_wp_cron_schedule' ) ); + + // Check for and remove any WP Cron hook scheduled by Action Scheduler < 3.0.0, which didn't include the $context param + $next_timestamp = wp_next_scheduled( self::WP_CRON_HOOK ); + if ( $next_timestamp ) { + wp_unschedule_event( $next_timestamp, self::WP_CRON_HOOK ); + } + + $cron_context = array( 'WP Cron' ); + + if ( ! wp_next_scheduled( self::WP_CRON_HOOK, $cron_context ) ) { + $schedule = apply_filters( 'action_scheduler_run_schedule', self::WP_CRON_SCHEDULE ); + wp_schedule_event( time(), $schedule, self::WP_CRON_HOOK, $cron_context ); + } + + add_action( self::WP_CRON_HOOK, array( self::instance(), 'run' ) ); + $this->hook_dispatch_async_request(); + } + + /** + * Hook check for dispatching an async request. + */ + public function hook_dispatch_async_request() { + add_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); + } + + /** + * Unhook check for dispatching an async request. + */ + public function unhook_dispatch_async_request() { + remove_action( 'shutdown', array( $this, 'maybe_dispatch_async_request' ) ); + } + + /** + * Check if we should dispatch an async request to process actions. + * + * This method is attached to 'shutdown', so is called frequently. To avoid slowing down + * the site, it mitigates the work performed in each request by: + * 1. checking if it's in the admin context and then + * 2. haven't run on the 'shutdown' hook within the lock time (60 seconds by default) + * 3. haven't exceeded the number of allowed batches. + * + * The order of these checks is important, because they run from a check on a value: + * 1. in memory - is_admin() maps to $GLOBALS or the WP_ADMIN constant + * 2. in memory - transients use autoloaded options by default + * 3. from a database query - has_maximum_concurrent_batches() run the query + * $this->store->get_claim_count() to find the current number of claims in the DB. + * + * If all of these conditions are met, then we request an async runner check whether it + * should dispatch a request to process pending actions. + */ + public function maybe_dispatch_async_request() { + if ( is_admin() && ! ActionScheduler::lock()->is_locked( 'async-request-runner' ) ) { + // Only start an async queue at most once every 60 seconds + ActionScheduler::lock()->set( 'async-request-runner' ); + $this->async_request->maybe_dispatch(); + } + } + + /** + * Process actions in the queue. Attached to self::WP_CRON_HOOK i.e. 'action_scheduler_run_queue' + * + * The $context param of this method defaults to 'WP Cron', because prior to Action Scheduler 3.0.0 + * that was the only context in which this method was run, and the self::WP_CRON_HOOK hook had no context + * passed along with it. New code calling this method directly, or by triggering the self::WP_CRON_HOOK, + * should set a context as the first parameter. For an example of this, refer to the code seen in + * @see ActionScheduler_AsyncRequest_QueueRunner::handle() + * + * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * Generally, this should be capitalised and not localised as it's a proper noun. + * @return int The number of actions processed. + */ + public function run( $context = 'WP Cron' ) { + ActionScheduler_Compatibility::raise_memory_limit(); + ActionScheduler_Compatibility::raise_time_limit( $this->get_time_limit() ); + do_action( 'action_scheduler_before_process_queue' ); + $this->run_cleanup(); + $processed_actions = 0; + if ( false === $this->has_maximum_concurrent_batches() ) { + $batch_size = apply_filters( 'action_scheduler_queue_runner_batch_size', 25 ); + do { + $processed_actions_in_batch = $this->do_batch( $batch_size, $context ); + $processed_actions += $processed_actions_in_batch; + } while ( $processed_actions_in_batch > 0 && ! $this->batch_limits_exceeded( $processed_actions ) ); // keep going until we run out of actions, time, or memory + } + + do_action( 'action_scheduler_after_process_queue' ); + return $processed_actions; + } + + /** + * Process a batch of actions pending in the queue. + * + * Actions are processed by claiming a set of pending actions then processing each one until either the batch + * size is completed, or memory or time limits are reached, defined by @see $this->batch_limits_exceeded(). + * + * @param int $size The maximum number of actions to process in the batch. + * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * Generally, this should be capitalised and not localised as it's a proper noun. + * @return int The number of actions processed. + */ + protected function do_batch( $size = 100, $context = '' ) { + $claim = $this->store->stake_claim($size); + $this->monitor->attach($claim); + $processed_actions = 0; + + foreach ( $claim->get_actions() as $action_id ) { + // bail if we lost the claim + if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $claim->get_id() ) ) ) { + break; + } + $this->process_action( $action_id, $context ); + $processed_actions++; + + if ( $this->batch_limits_exceeded( $processed_actions ) ) { + break; + } + } + $this->store->release_claim($claim); + $this->monitor->detach(); + $this->clear_caches(); + return $processed_actions; + } + + /** + * Running large batches can eat up memory, as WP adds data to its object cache. + * + * If using a persistent object store, this has the side effect of flushing that + * as well, so this is disabled by default. To enable: + * + * add_filter( 'action_scheduler_queue_runner_flush_cache', '__return_true' ); + */ + protected function clear_caches() { + if ( ! wp_using_ext_object_cache() || apply_filters( 'action_scheduler_queue_runner_flush_cache', false ) ) { + wp_cache_flush(); + } + } + + public function add_wp_cron_schedule( $schedules ) { + $schedules['every_minute'] = array( + 'interval' => 60, // in seconds + 'display' => __( 'Every minute', 'woocommerce' ), + ); + + return $schedules; + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_Versions.php b/packages/action-scheduler/classes/ActionScheduler_Versions.php new file mode 100644 index 0000000..915c2e6 --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_Versions.php @@ -0,0 +1,62 @@ +versions[$version_string]) ) { + return FALSE; + } + $this->versions[$version_string] = $initialization_callback; + return TRUE; + } + + public function get_versions() { + return $this->versions; + } + + public function latest_version() { + $keys = array_keys($this->versions); + if ( empty($keys) ) { + return false; + } + uasort( $keys, 'version_compare' ); + return end($keys); + } + + public function latest_version_callback() { + $latest = $this->latest_version(); + if ( empty($latest) || !isset($this->versions[$latest]) ) { + return '__return_null'; + } + return $this->versions[$latest]; + } + + /** + * @return ActionScheduler_Versions + * @codeCoverageIgnore + */ + public static function instance() { + if ( empty(self::$instance) ) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * @codeCoverageIgnore + */ + public static function initialize_latest_version() { + $self = self::instance(); + call_user_func($self->latest_version_callback()); + } +} + \ No newline at end of file diff --git a/packages/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php b/packages/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php new file mode 100644 index 0000000..bad2c2c --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_WPCommentCleaner.php @@ -0,0 +1,115 @@ + Status administration screen + add_action( 'load-tools_page_action-scheduler', array( __CLASS__, 'register_admin_notice' ) ); + add_action( 'load-woocommerce_page_wc-status', array( __CLASS__, 'register_admin_notice' ) ); + } + + /** + * Determines if there are log entries in the wp comments table. + * + * Uses the flag set on migration completion set by @see self::maybe_schedule_cleanup(). + * + * @return boolean Whether there are scheduled action comments in the comments table. + */ + public static function has_logs() { + return 'yes' === get_option( self::$has_logs_option_key ); + } + + /** + * Schedules the WP Post comment table cleanup to run in 6 months if it's not already scheduled. + * Attached to the migration complete hook 'action_scheduler/migration_complete'. + */ + public static function maybe_schedule_cleanup() { + if ( (bool) get_comments( array( 'type' => ActionScheduler_wpCommentLogger::TYPE, 'number' => 1, 'fields' => 'ids' ) ) ) { + update_option( self::$has_logs_option_key, 'yes' ); + + if ( ! as_next_scheduled_action( self::$cleanup_hook ) ) { + as_schedule_single_action( gmdate( 'U' ) + ( 6 * MONTH_IN_SECONDS ), self::$cleanup_hook ); + } + } + } + + /** + * Delete all action comments from the WP Comments table. + */ + public static function delete_all_action_comments() { + global $wpdb; + $wpdb->delete( $wpdb->comments, array( 'comment_type' => ActionScheduler_wpCommentLogger::TYPE, 'comment_agent' => ActionScheduler_wpCommentLogger::AGENT ) ); + delete_option( self::$has_logs_option_key ); + } + + /** + * Registers admin notices about the orphaned action logs. + */ + public static function register_admin_notice() { + add_action( 'admin_notices', array( __CLASS__, 'print_admin_notice' ) ); + } + + /** + * Prints details about the orphaned action logs and includes information on where to learn more. + */ + public static function print_admin_notice() { + $next_cleanup_message = ''; + $next_scheduled_cleanup_hook = as_next_scheduled_action( self::$cleanup_hook ); + + if ( $next_scheduled_cleanup_hook ) { + /* translators: %s: date interval */ + $next_cleanup_message = sprintf( __( 'This data will be deleted in %s.', 'woocommerce' ), human_time_diff( gmdate( 'U' ), $next_scheduled_cleanup_hook ) ); + } + + $notice = sprintf( + /* translators: 1: next cleanup message 2: github issue URL */ + __( 'Action Scheduler has migrated data to custom tables; however, orphaned log entries exist in the WordPress Comments table. %1$s Learn more »', 'woocommerce' ), + $next_cleanup_message, + 'https://github.com/woocommerce/action-scheduler/issues/368' + ); + + echo '

    ' . wp_kses_post( $notice ) . '

    '; + } +} diff --git a/packages/action-scheduler/classes/ActionScheduler_wcSystemStatus.php b/packages/action-scheduler/classes/ActionScheduler_wcSystemStatus.php new file mode 100644 index 0000000..92c6cae --- /dev/null +++ b/packages/action-scheduler/classes/ActionScheduler_wcSystemStatus.php @@ -0,0 +1,157 @@ +store = $store; + } + + /** + * Display action data, including number of actions grouped by status and the oldest & newest action in each status. + * + * Helpful to identify issues, like a clogged queue. + */ + public function render() { + $action_counts = $this->store->action_counts(); + $status_labels = $this->store->get_status_labels(); + $oldest_and_newest = $this->get_oldest_and_newest( array_keys( $status_labels ) ); + + $this->get_template( $status_labels, $action_counts, $oldest_and_newest ); + } + + /** + * Get oldest and newest scheduled dates for a given set of statuses. + * + * @param array $status_keys Set of statuses to find oldest & newest action for. + * @return array + */ + protected function get_oldest_and_newest( $status_keys ) { + + $oldest_and_newest = array(); + + foreach ( $status_keys as $status ) { + $oldest_and_newest[ $status ] = array( + 'oldest' => '–', + 'newest' => '–', + ); + + if ( 'in-progress' === $status ) { + continue; + } + + $oldest_and_newest[ $status ]['oldest'] = $this->get_action_status_date( $status, 'oldest' ); + $oldest_and_newest[ $status ]['newest'] = $this->get_action_status_date( $status, 'newest' ); + } + + return $oldest_and_newest; + } + + /** + * Get oldest or newest scheduled date for a given status. + * + * @param string $status Action status label/name string. + * @param string $date_type Oldest or Newest. + * @return DateTime + */ + protected function get_action_status_date( $status, $date_type = 'oldest' ) { + + $order = 'oldest' === $date_type ? 'ASC' : 'DESC'; + + $action = $this->store->query_actions( array( + 'claimed' => false, + 'status' => $status, + 'per_page' => 1, + 'order' => $order, + ) ); + + if ( ! empty( $action ) ) { + $date_object = $this->store->get_date( $action[0] ); + $action_date = $date_object->format( 'Y-m-d H:i:s O' ); + } else { + $action_date = '–'; + } + + return $action_date; + } + + /** + * Get oldest or newest scheduled date for a given status. + * + * @param array $status_labels Set of statuses to find oldest & newest action for. + * @param array $action_counts Number of actions grouped by status. + * @param array $oldest_and_newest Date of the oldest and newest action with each status. + */ + protected function get_template( $status_labels, $action_counts, $oldest_and_newest ) { + $as_version = ActionScheduler_Versions::instance()->latest_version(); + $as_datastore = get_class( ActionScheduler_Store::instance() ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + $count ) { + // WC uses the 3rd column for export, so we need to display more data in that (hidden when viewed as part of the table) and add an empty 2nd column. + printf( + '', + esc_html( $status_labels[ $status ] ), + number_format_i18n( $count ), + $oldest_and_newest[ $status ]['oldest'], + $oldest_and_newest[ $status ]['newest'] + ); + } + ?> + +

     
    %1$s %2$s, Oldest: %3$s, Newest: %4$s%3$s%4$s
    + + run_cleanup(); + $this->add_hooks(); + + // Check to make sure there aren't too many concurrent processes running. + if ( $this->has_maximum_concurrent_batches() ) { + if ( $force ) { + WP_CLI::warning( __( 'There are too many concurrent batches, but the run is forced to continue.', 'woocommerce' ) ); + } else { + WP_CLI::error( __( 'There are too many concurrent batches.', 'woocommerce' ) ); + } + } + + // Stake a claim and store it. + $this->claim = $this->store->stake_claim( $batch_size, null, $hooks, $group ); + $this->monitor->attach( $this->claim ); + $this->actions = $this->claim->get_actions(); + + return count( $this->actions ); + } + + /** + * Add our hooks to the appropriate actions. + * + * @author Jeremy Pry + */ + protected function add_hooks() { + add_action( 'action_scheduler_before_execute', array( $this, 'before_execute' ) ); + add_action( 'action_scheduler_after_execute', array( $this, 'after_execute' ), 10, 2 ); + add_action( 'action_scheduler_failed_execution', array( $this, 'action_failed' ), 10, 2 ); + } + + /** + * Set up the WP CLI progress bar. + * + * @author Jeremy Pry + */ + protected function setup_progress_bar() { + $count = count( $this->actions ); + $this->progress_bar = new ProgressBar( + /* translators: %d: amount of actions */ + sprintf( _n( 'Running %d action', 'Running %d actions', $count, 'woocommerce' ), number_format_i18n( $count ) ), + $count + ); + } + + /** + * Process actions in the queue. + * + * @author Jeremy Pry + * + * @param string $context Optional runner context. Default 'WP CLI'. + * + * @return int The number of actions processed. + */ + public function run( $context = 'WP CLI' ) { + do_action( 'action_scheduler_before_process_queue' ); + $this->setup_progress_bar(); + foreach ( $this->actions as $action_id ) { + // Error if we lost the claim. + if ( ! in_array( $action_id, $this->store->find_actions_by_claim_id( $this->claim->get_id() ) ) ) { + WP_CLI::warning( __( 'The claim has been lost. Aborting current batch.', 'woocommerce' ) ); + break; + } + + $this->process_action( $action_id, $context ); + $this->progress_bar->tick(); + } + + $completed = $this->progress_bar->current(); + $this->progress_bar->finish(); + $this->store->release_claim( $this->claim ); + do_action( 'action_scheduler_after_process_queue' ); + + return $completed; + } + + /** + * Handle WP CLI message when the action is starting. + * + * @author Jeremy Pry + * + * @param $action_id + */ + public function before_execute( $action_id ) { + /* translators: %s refers to the action ID */ + WP_CLI::log( sprintf( __( 'Started processing action %s', 'woocommerce' ), $action_id ) ); + } + + /** + * Handle WP CLI message when the action has completed. + * + * @author Jeremy Pry + * + * @param int $action_id + * @param null|ActionScheduler_Action $action The instance of the action. Default to null for backward compatibility. + */ + public function after_execute( $action_id, $action = null ) { + // backward compatibility + if ( null === $action ) { + $action = $this->store->fetch_action( $action_id ); + } + /* translators: 1: action ID 2: hook name */ + WP_CLI::log( sprintf( __( 'Completed processing action %1$s with hook: %2$s', 'woocommerce' ), $action_id, $action->get_hook() ) ); + } + + /** + * Handle WP CLI message when the action has failed. + * + * @author Jeremy Pry + * + * @param int $action_id + * @param Exception $exception + * @throws \WP_CLI\ExitException With failure message. + */ + public function action_failed( $action_id, $exception ) { + WP_CLI::error( + /* translators: 1: action ID 2: exception message */ + sprintf( __( 'Error processing action %1$s: %2$s', 'woocommerce' ), $action_id, $exception->getMessage() ), + false + ); + } + + /** + * Sleep and help avoid hitting memory limit + * + * @param int $sleep_time Amount of seconds to sleep + * @deprecated 3.0.0 + */ + protected function stop_the_insanity( $sleep_time = 0 ) { + _deprecated_function( 'ActionScheduler_WPCLI_QueueRunner::stop_the_insanity', '3.0.0', 'ActionScheduler_DataController::free_memory' ); + + ActionScheduler_DataController::free_memory(); + } + + /** + * Maybe trigger the stop_the_insanity() method to free up memory. + */ + protected function maybe_stop_the_insanity() { + // The value returned by progress_bar->current() might be padded. Remove padding, and convert to int. + $current_iteration = intval( trim( $this->progress_bar->current() ) ); + if ( 0 === $current_iteration % 50 ) { + $this->stop_the_insanity(); + } + } +} diff --git a/packages/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php b/packages/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php new file mode 100644 index 0000000..e66e7c6 --- /dev/null +++ b/packages/action-scheduler/classes/WP_CLI/ActionScheduler_WPCLI_Scheduler_command.php @@ -0,0 +1,158 @@ +] + * : The maximum number of actions to run. Defaults to 100. + * + * [--batches=] + * : Limit execution to a number of batches. Defaults to 0, meaning batches will continue being executed until all actions are complete. + * + * [--cleanup-batch-size=] + * : The maximum number of actions to clean up. Defaults to the value of --batch-size. + * + * [--hooks=] + * : Only run actions with the specified hook. Omitting this option runs actions with any hook. Define multiple hooks as a comma separated string (without spaces), e.g. `--hooks=hook_one,hook_two,hook_three` + * + * [--group=] + * : Only run actions from the specified group. Omitting this option runs actions from all groups. + * + * [--free-memory-on=] + * : The number of actions to process between freeing memory. 0 disables freeing memory. Default 50. + * + * [--pause=] + * : The number of seconds to pause when freeing memory. Default no pause. + * + * [--force] + * : Whether to force execution despite the maximum number of concurrent processes being exceeded. + * + * @param array $args Positional arguments. + * @param array $assoc_args Keyed arguments. + * @throws \WP_CLI\ExitException When an error occurs. + * + * @subcommand run + */ + public function run( $args, $assoc_args ) { + // Handle passed arguments. + $batch = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batch-size', 100 ) ); + $batches = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'batches', 0 ) ); + $clean = absint( \WP_CLI\Utils\get_flag_value( $assoc_args, 'cleanup-batch-size', $batch ) ); + $hooks = explode( ',', WP_CLI\Utils\get_flag_value( $assoc_args, 'hooks', '' ) ); + $hooks = array_filter( array_map( 'trim', $hooks ) ); + $group = \WP_CLI\Utils\get_flag_value( $assoc_args, 'group', '' ); + $free_on = \WP_CLI\Utils\get_flag_value( $assoc_args, 'free-memory-on', 50 ); + $sleep = \WP_CLI\Utils\get_flag_value( $assoc_args, 'pause', 0 ); + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force', false ); + + ActionScheduler_DataController::set_free_ticks( $free_on ); + ActionScheduler_DataController::set_sleep_time( $sleep ); + + $batches_completed = 0; + $actions_completed = 0; + $unlimited = $batches === 0; + + try { + // Custom queue cleaner instance. + $cleaner = new ActionScheduler_QueueCleaner( null, $clean ); + + // Get the queue runner instance + $runner = new ActionScheduler_WPCLI_QueueRunner( null, null, $cleaner ); + + // Determine how many tasks will be run in the first batch. + $total = $runner->setup( $batch, $hooks, $group, $force ); + + // Run actions for as long as possible. + while ( $total > 0 ) { + $this->print_total_actions( $total ); + $actions_completed += $runner->run(); + $batches_completed++; + + // Maybe set up tasks for the next batch. + $total = ( $unlimited || $batches_completed < $batches ) ? $runner->setup( $batch, $hooks, $group, $force ) : 0; + } + } catch ( Exception $e ) { + $this->print_error( $e ); + } + + $this->print_total_batches( $batches_completed ); + $this->print_success( $actions_completed ); + } + + /** + * Print WP CLI message about how many actions are about to be processed. + * + * @author Jeremy Pry + * + * @param int $total + */ + protected function print_total_actions( $total ) { + WP_CLI::log( + sprintf( + /* translators: %d refers to how many scheduled taks were found to run */ + _n( 'Found %d scheduled task', 'Found %d scheduled tasks', $total, 'woocommerce' ), + number_format_i18n( $total ) + ) + ); + } + + /** + * Print WP CLI message about how many batches of actions were processed. + * + * @author Jeremy Pry + * + * @param int $batches_completed + */ + protected function print_total_batches( $batches_completed ) { + WP_CLI::log( + sprintf( + /* translators: %d refers to the total number of batches executed */ + _n( '%d batch executed.', '%d batches executed.', $batches_completed, 'woocommerce' ), + number_format_i18n( $batches_completed ) + ) + ); + } + + /** + * Convert an exception into a WP CLI error. + * + * @author Jeremy Pry + * + * @param Exception $e The error object. + * + * @throws \WP_CLI\ExitException + */ + protected function print_error( Exception $e ) { + WP_CLI::error( + sprintf( + /* translators: %s refers to the exception error message */ + __( 'There was an error running the action scheduler: %s', 'woocommerce' ), + $e->getMessage() + ) + ); + } + + /** + * Print a success message with the number of completed actions. + * + * @author Jeremy Pry + * + * @param int $actions_completed + */ + protected function print_success( $actions_completed ) { + WP_CLI::success( + sprintf( + /* translators: %d refers to the total number of taskes completed */ + _n( '%d scheduled task completed.', '%d scheduled tasks completed.', $actions_completed, 'woocommerce' ), + number_format_i18n( $actions_completed ) + ) + ); + } +} diff --git a/packages/action-scheduler/classes/WP_CLI/Migration_Command.php b/packages/action-scheduler/classes/WP_CLI/Migration_Command.php new file mode 100644 index 0000000..066697e --- /dev/null +++ b/packages/action-scheduler/classes/WP_CLI/Migration_Command.php @@ -0,0 +1,148 @@ + 'Migrates actions to the DB tables store', + 'synopsis' => [ + [ + 'type' => 'assoc', + 'name' => 'batch-size', + 'optional' => true, + 'default' => 100, + 'description' => 'The number of actions to process in each batch', + ], + [ + 'type' => 'assoc', + 'name' => 'free-memory-on', + 'optional' => true, + 'default' => 50, + 'description' => 'The number of actions to process between freeing memory. 0 disables freeing memory', + ], + [ + 'type' => 'assoc', + 'name' => 'pause', + 'optional' => true, + 'default' => 0, + 'description' => 'The number of seconds to pause when freeing memory', + ], + [ + 'type' => 'flag', + 'name' => 'dry-run', + 'optional' => true, + 'description' => 'Reports on the actions that would have been migrated, but does not change any data', + ], + ], + ] ); + } + + /** + * Process the data migration. + * + * @param array $positional_args Required for WP CLI. Not used in migration. + * @param array $assoc_args Optional arguments. + * + * @return void + */ + public function migrate( $positional_args, $assoc_args ) { + $this->init_logging(); + + $config = $this->get_migration_config( $assoc_args ); + $runner = new Runner( $config ); + $runner->init_destination(); + + $batch_size = isset( $assoc_args[ 'batch-size' ] ) ? (int) $assoc_args[ 'batch-size' ] : 100; + $free_on = isset( $assoc_args[ 'free-memory-on' ] ) ? (int) $assoc_args[ 'free-memory-on' ] : 50; + $sleep = isset( $assoc_args[ 'pause' ] ) ? (int) $assoc_args[ 'pause' ] : 0; + \ActionScheduler_DataController::set_free_ticks( $free_on ); + \ActionScheduler_DataController::set_sleep_time( $sleep ); + + do { + $actions_processed = $runner->run( $batch_size ); + $this->total_processed += $actions_processed; + } while ( $actions_processed > 0 ); + + if ( ! $config->get_dry_run() ) { + // let the scheduler know that there's nothing left to do + $scheduler = new Scheduler(); + $scheduler->mark_complete(); + } + + WP_CLI::success( sprintf( '%s complete. %d actions processed.', $config->get_dry_run() ? 'Dry run' : 'Migration', $this->total_processed ) ); + } + + /** + * Build the config object used to create the Runner + * + * @param array $args Optional arguments. + * + * @return ActionScheduler\Migration\Config + */ + private function get_migration_config( $args ) { + $args = wp_parse_args( $args, [ + 'dry-run' => false, + ] ); + + $config = Controller::instance()->get_migration_config_object(); + $config->set_dry_run( ! empty( $args[ 'dry-run' ] ) ); + + return $config; + } + + /** + * Hook command line logging into migration actions. + */ + private function init_logging() { + add_action( 'action_scheduler/migrate_action_dry_run', function ( $action_id ) { + WP_CLI::debug( sprintf( 'Dry-run: migrated action %d', $action_id ) ); + }, 10, 1 ); + add_action( 'action_scheduler/no_action_to_migrate', function ( $action_id ) { + WP_CLI::debug( sprintf( 'No action found to migrate for ID %d', $action_id ) ); + }, 10, 1 ); + add_action( 'action_scheduler/migrate_action_failed', function ( $action_id ) { + WP_CLI::warning( sprintf( 'Failed migrating action with ID %d', $action_id ) ); + }, 10, 1 ); + add_action( 'action_scheduler/migrate_action_incomplete', function ( $source_id, $destination_id ) { + WP_CLI::warning( sprintf( 'Unable to remove source action with ID %d after migrating to new ID %d', $source_id, $destination_id ) ); + }, 10, 2 ); + add_action( 'action_scheduler/migrated_action', function ( $source_id, $destination_id ) { + WP_CLI::debug( sprintf( 'Migrated source action with ID %d to new store with ID %d', $source_id, $destination_id ) ); + }, 10, 2 ); + add_action( 'action_scheduler/migration_batch_starting', function ( $batch ) { + WP_CLI::debug( 'Beginning migration of batch: ' . print_r( $batch, true ) ); + }, 10, 1 ); + add_action( 'action_scheduler/migration_batch_complete', function ( $batch ) { + WP_CLI::log( sprintf( 'Completed migration of %d actions', count( $batch ) ) ); + }, 10, 1 ); + } +} diff --git a/packages/action-scheduler/classes/WP_CLI/ProgressBar.php b/packages/action-scheduler/classes/WP_CLI/ProgressBar.php new file mode 100644 index 0000000..dcb6e8d --- /dev/null +++ b/packages/action-scheduler/classes/WP_CLI/ProgressBar.php @@ -0,0 +1,119 @@ +total_ticks = 0; + $this->message = $message; + $this->count = $count; + $this->interval = $interval; + } + + /** + * Increment the progress bar ticks. + */ + public function tick() { + if ( null === $this->progress_bar ) { + $this->setup_progress_bar(); + } + + $this->progress_bar->tick(); + $this->total_ticks++; + + do_action( 'action_scheduler/progress_tick', $this->total_ticks ); + } + + /** + * Get the progress bar tick count. + * + * @return int + */ + public function current() { + return $this->progress_bar ? $this->progress_bar->current() : 0; + } + + /** + * Finish the current progress bar. + */ + public function finish() { + if ( null !== $this->progress_bar ) { + $this->progress_bar->finish(); + } + + $this->progress_bar = null; + } + + /** + * Set the message used when creating the progress bar. + * + * @param string $message The message to be used when the next progress bar is created. + */ + public function set_message( $message ) { + $this->message = $message; + } + + /** + * Set the count for a new progress bar. + * + * @param integer $count The total number of ticks expected to complete. + */ + public function set_count( $count ) { + $this->count = $count; + $this->finish(); + } + + /** + * Set up the progress bar. + */ + protected function setup_progress_bar() { + $this->progress_bar = \WP_CLI\Utils\make_progress_bar( + $this->message, + $this->count, + $this->interval + ); + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler.php b/packages/action-scheduler/classes/abstracts/ActionScheduler.php new file mode 100644 index 0000000..755b167 --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler.php @@ -0,0 +1,304 @@ +init(); + $store->init(); + $logger->init(); + $runner->init(); + } + + if ( apply_filters( 'action_scheduler_load_deprecated_functions', true ) ) { + require_once( self::plugin_path( 'deprecated/functions.php' ) ); + } + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + WP_CLI::add_command( 'action-scheduler', 'ActionScheduler_WPCLI_Scheduler_command' ); + if ( ! ActionScheduler_DataController::is_migration_complete() && Controller::instance()->allow_migration() ) { + $command = new Migration_Command(); + $command->register(); + } + } + + self::$data_store_initialized = true; + + /** + * Handle WP comment cleanup after migration. + */ + if ( is_a( $logger, 'ActionScheduler_DBLogger' ) && ActionScheduler_DataController::is_migration_complete() && ActionScheduler_WPCommentCleaner::has_logs() ) { + ActionScheduler_WPCommentCleaner::init(); + } + + add_action( 'action_scheduler/migration_complete', 'ActionScheduler_WPCommentCleaner::maybe_schedule_cleanup' ); + } + + /** + * Check whether the AS data store has been initialized. + * + * @param string $function_name The name of the function being called. Optional. Default `null`. + * @return bool + */ + public static function is_initialized( $function_name = null ) { + if ( ! self::$data_store_initialized && ! empty( $function_name ) ) { + $message = sprintf( __( '%s() was called before the Action Scheduler data store was initialized', 'woocommerce' ), esc_attr( $function_name ) ); + error_log( $message, E_WARNING ); + } + + return self::$data_store_initialized; + } + + /** + * Determine if the class is one of our abstract classes. + * + * @since 3.0.0 + * + * @param string $class The class name. + * + * @return bool + */ + protected static function is_class_abstract( $class ) { + static $abstracts = array( + 'ActionScheduler' => true, + 'ActionScheduler_Abstract_ListTable' => true, + 'ActionScheduler_Abstract_QueueRunner' => true, + 'ActionScheduler_Abstract_Schedule' => true, + 'ActionScheduler_Abstract_RecurringSchedule' => true, + 'ActionScheduler_Lock' => true, + 'ActionScheduler_Logger' => true, + 'ActionScheduler_Abstract_Schema' => true, + 'ActionScheduler_Store' => true, + 'ActionScheduler_TimezoneHelper' => true, + ); + + return isset( $abstracts[ $class ] ) && $abstracts[ $class ]; + } + + /** + * Determine if the class is one of our migration classes. + * + * @since 3.0.0 + * + * @param string $class The class name. + * + * @return bool + */ + protected static function is_class_migration( $class ) { + static $migration_segments = array( + 'ActionMigrator' => true, + 'BatchFetcher' => true, + 'DBStoreMigrator' => true, + 'DryRun' => true, + 'LogMigrator' => true, + 'Config' => true, + 'Controller' => true, + 'Runner' => true, + 'Scheduler' => true, + ); + + $segments = explode( '_', $class ); + $segment = isset( $segments[ 1 ] ) ? $segments[ 1 ] : $class; + + return isset( $migration_segments[ $segment ] ) && $migration_segments[ $segment ]; + } + + /** + * Determine if the class is one of our WP CLI classes. + * + * @since 3.0.0 + * + * @param string $class The class name. + * + * @return bool + */ + protected static function is_class_cli( $class ) { + static $cli_segments = array( + 'QueueRunner' => true, + 'Command' => true, + 'ProgressBar' => true, + ); + + $segments = explode( '_', $class ); + $segment = isset( $segments[ 1 ] ) ? $segments[ 1 ] : $class; + + return isset( $cli_segments[ $segment ] ) && $cli_segments[ $segment ]; + } + + final public function __clone() { + trigger_error("Singleton. No cloning allowed!", E_USER_ERROR); + } + + final public function __wakeup() { + trigger_error("Singleton. No serialization allowed!", E_USER_ERROR); + } + + final private function __construct() {} + + /** Deprecated **/ + + public static function get_datetime_object( $when = null, $timezone = 'UTC' ) { + _deprecated_function( __METHOD__, '2.0', 'wcs_add_months()' ); + return as_get_datetime_object( $when, $timezone ); + } + + /** + * Issue deprecated warning if an Action Scheduler function is called in the shutdown hook. + * + * @param string $function_name The name of the function being called. + * @deprecated 3.1.6. + */ + public static function check_shutdown_hook( $function_name ) { + _deprecated_function( __FUNCTION__, '3.1.6' ); + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php new file mode 100644 index 0000000..dd7f66a --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_ListTable.php @@ -0,0 +1,674 @@ + value pair. The + * key must much the table column name and the value is the label, which is + * automatically translated. + */ + protected $columns = array(); + + /** + * Defines the row-actions. It expects an array where the key + * is the column name and the value is an array of actions. + * + * The array of actions are key => value, where key is the method name + * (with the prefix row_action_) and the value is the label + * and title. + */ + protected $row_actions = array(); + + /** + * The Primary key of our table + */ + protected $ID = 'ID'; + + /** + * Enables sorting, it expects an array + * of columns (the column names are the values) + */ + protected $sort_by = array(); + + protected $filter_by = array(); + + /** + * @var array The status name => count combinations for this table's items. Used to display status filters. + */ + protected $status_counts = array(); + + /** + * @var array Notices to display when loading the table. Array of arrays of form array( 'class' => {updated|error}, 'message' => 'This is the notice text display.' ). + */ + protected $admin_notices = array(); + + /** + * @var string Localised string displayed in the

    element above the able. + */ + protected $table_header; + + /** + * Enables bulk actions. It must be an array where the key is the action name + * and the value is the label (which is translated automatically). It is important + * to notice that it will check that the method exists (`bulk_$name`) and will throw + * an exception if it does not exists. + * + * This class will automatically check if the current request has a bulk action, will do the + * validations and afterwards will execute the bulk method, with two arguments. The first argument + * is the array with primary keys, the second argument is a string with a list of the primary keys, + * escaped and ready to use (with `IN`). + */ + protected $bulk_actions = array(); + + /** + * Makes translation easier, it basically just wraps + * `_x` with some default (the package name). + * + * @deprecated 3.0.0 + */ + protected function translate( $text, $context = '' ) { + return $text; + } + + /** + * Reads `$this->bulk_actions` and returns an array that WP_List_Table understands. It + * also validates that the bulk method handler exists. It throws an exception because + * this is a library meant for developers and missing a bulk method is a development-time error. + */ + protected function get_bulk_actions() { + $actions = array(); + + foreach ( $this->bulk_actions as $action => $label ) { + if ( ! is_callable( array( $this, 'bulk_' . $action ) ) ) { + throw new RuntimeException( "The bulk action $action does not have a callback method" ); + } + + $actions[ $action ] = $label; + } + + return $actions; + } + + /** + * Checks if the current request has a bulk action. If that is the case it will validate and will + * execute the bulk method handler. Regardless if the action is valid or not it will redirect to + * the previous page removing the current arguments that makes this request a bulk action. + */ + protected function process_bulk_action() { + global $wpdb; + // Detect when a bulk action is being triggered. + $action = $this->current_action(); + if ( ! $action ) { + return; + } + + check_admin_referer( 'bulk-' . $this->_args['plural'] ); + + $method = 'bulk_' . $action; + if ( array_key_exists( $action, $this->bulk_actions ) && is_callable( array( $this, $method ) ) && ! empty( $_GET['ID'] ) && is_array( $_GET['ID'] ) ) { + $ids_sql = '(' . implode( ',', array_fill( 0, count( $_GET['ID'] ), '%s' ) ) . ')'; + $this->$method( $_GET['ID'], $wpdb->prepare( $ids_sql, $_GET['ID'] ) ); + } + + wp_redirect( remove_query_arg( + array( '_wp_http_referer', '_wpnonce', 'ID', 'action', 'action2' ), + wp_unslash( $_SERVER['REQUEST_URI'] ) + ) ); + exit; + } + + /** + * Default code for deleting entries. + * validated already by process_bulk_action() + */ + protected function bulk_delete( array $ids, $ids_sql ) { + $store = ActionScheduler::store(); + foreach ( $ids as $action_id ) { + $store->delete( $action_id ); + } + } + + /** + * Prepares the _column_headers property which is used by WP_Table_List at rendering. + * It merges the columns and the sortable columns. + */ + protected function prepare_column_headers() { + $this->_column_headers = array( + $this->get_columns(), + get_hidden_columns( $this->screen ), + $this->get_sortable_columns(), + ); + } + + /** + * Reads $this->sort_by and returns the columns name in a format that WP_Table_List + * expects + */ + public function get_sortable_columns() { + $sort_by = array(); + foreach ( $this->sort_by as $column ) { + $sort_by[ $column ] = array( $column, true ); + } + return $sort_by; + } + + /** + * Returns the columns names for rendering. It adds a checkbox for selecting everything + * as the first column + */ + public function get_columns() { + $columns = array_merge( + array( 'cb' => '' ), + $this->columns + ); + + return $columns; + } + + /** + * Get prepared LIMIT clause for items query + * + * @global wpdb $wpdb + * + * @return string Prepared LIMIT clause for items query. + */ + protected function get_items_query_limit() { + global $wpdb; + + $per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page ); + return $wpdb->prepare( 'LIMIT %d', $per_page ); + } + + /** + * Returns the number of items to offset/skip for this current view. + * + * @return int + */ + protected function get_items_offset() { + $per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page ); + $current_page = $this->get_pagenum(); + if ( 1 < $current_page ) { + $offset = $per_page * ( $current_page - 1 ); + } else { + $offset = 0; + } + + return $offset; + } + + /** + * Get prepared OFFSET clause for items query + * + * @global wpdb $wpdb + * + * @return string Prepared OFFSET clause for items query. + */ + protected function get_items_query_offset() { + global $wpdb; + + return $wpdb->prepare( 'OFFSET %d', $this->get_items_offset() ); + } + + /** + * Prepares the ORDER BY sql statement. It uses `$this->sort_by` to know which + * columns are sortable. This requests validates the orderby $_GET parameter is a valid + * column and sortable. It will also use order (ASC|DESC) using DESC by default. + */ + protected function get_items_query_order() { + if ( empty( $this->sort_by ) ) { + return ''; + } + + $orderby = esc_sql( $this->get_request_orderby() ); + $order = esc_sql( $this->get_request_order() ); + + return "ORDER BY {$orderby} {$order}"; + } + + /** + * Return the sortable column specified for this request to order the results by, if any. + * + * @return string + */ + protected function get_request_orderby() { + + $valid_sortable_columns = array_values( $this->sort_by ); + + if ( ! empty( $_GET['orderby'] ) && in_array( $_GET['orderby'], $valid_sortable_columns ) ) { + $orderby = sanitize_text_field( $_GET['orderby'] ); + } else { + $orderby = $valid_sortable_columns[0]; + } + + return $orderby; + } + + /** + * Return the sortable column order specified for this request. + * + * @return string + */ + protected function get_request_order() { + + if ( ! empty( $_GET['order'] ) && 'desc' === strtolower( $_GET['order'] ) ) { + $order = 'DESC'; + } else { + $order = 'ASC'; + } + + return $order; + } + + /** + * Return the status filter for this request, if any. + * + * @return string + */ + protected function get_request_status() { + $status = ( ! empty( $_GET['status'] ) ) ? $_GET['status'] : ''; + return $status; + } + + /** + * Return the search filter for this request, if any. + * + * @return string + */ + protected function get_request_search_query() { + $search_query = ( ! empty( $_GET['s'] ) ) ? $_GET['s'] : ''; + return $search_query; + } + + /** + * Process and return the columns name. This is meant for using with SQL, this means it + * always includes the primary key. + * + * @return array + */ + protected function get_table_columns() { + $columns = array_keys( $this->columns ); + if ( ! in_array( $this->ID, $columns ) ) { + $columns[] = $this->ID; + } + + return $columns; + } + + /** + * Check if the current request is doing a "full text" search. If that is the case + * prepares the SQL to search texts using LIKE. + * + * If the current request does not have any search or if this list table does not support + * that feature it will return an empty string. + * + * TODO: + * - Improve search doing LIKE by word rather than by phrases. + * + * @return string + */ + protected function get_items_query_search() { + global $wpdb; + + if ( empty( $_GET['s'] ) || empty( $this->search_by ) ) { + return ''; + } + + $filter = array(); + foreach ( $this->search_by as $column ) { + $filter[] = $wpdb->prepare('`' . $column . '` like "%%s%"', $wpdb->esc_like( $_GET['s'] )); + } + return implode( ' OR ', $filter ); + } + + /** + * Prepares the SQL to filter rows by the options defined at `$this->filter_by`. Before trusting + * any data sent by the user it validates that it is a valid option. + */ + protected function get_items_query_filters() { + global $wpdb; + + if ( ! $this->filter_by || empty( $_GET['filter_by'] ) || ! is_array( $_GET['filter_by'] ) ) { + return ''; + } + + $filter = array(); + + foreach ( $this->filter_by as $column => $options ) { + if ( empty( $_GET['filter_by'][ $column ] ) || empty( $options[ $_GET['filter_by'][ $column ] ] ) ) { + continue; + } + + $filter[] = $wpdb->prepare( "`$column` = %s", $_GET['filter_by'][ $column ] ); + } + + return implode( ' AND ', $filter ); + + } + + /** + * Prepares the data to feed WP_Table_List. + * + * This has the core for selecting, sorting and filting data. To keep the code simple + * its logic is split among many methods (get_items_query_*). + * + * Beside populating the items this function will also count all the records that matches + * the filtering criteria and will do fill the pagination variables. + */ + public function prepare_items() { + global $wpdb; + + $this->process_bulk_action(); + + $this->process_row_actions(); + + if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; + } + + $this->prepare_column_headers(); + + $limit = $this->get_items_query_limit(); + $offset = $this->get_items_query_offset(); + $order = $this->get_items_query_order(); + $where = array_filter(array( + $this->get_items_query_search(), + $this->get_items_query_filters(), + )); + $columns = '`' . implode( '`, `', $this->get_table_columns() ) . '`'; + + if ( ! empty( $where ) ) { + $where = 'WHERE ('. implode( ') AND (', $where ) . ')'; + } else { + $where = ''; + } + + $sql = "SELECT $columns FROM {$this->table_name} {$where} {$order} {$limit} {$offset}"; + + $this->set_items( $wpdb->get_results( $sql, ARRAY_A ) ); + + $query_count = "SELECT COUNT({$this->ID}) FROM {$this->table_name} {$where}"; + $total_items = $wpdb->get_var( $query_count ); + $per_page = $this->get_items_per_page( $this->package . '_items_per_page', $this->items_per_page ); + $this->set_pagination_args( array( + 'total_items' => $total_items, + 'per_page' => $per_page, + 'total_pages' => ceil( $total_items / $per_page ), + ) ); + } + + public function extra_tablenav( $which ) { + if ( ! $this->filter_by || 'top' !== $which ) { + return; + } + + echo '
    '; + + foreach ( $this->filter_by as $id => $options ) { + $default = ! empty( $_GET['filter_by'][ $id ] ) ? $_GET['filter_by'][ $id ] : ''; + if ( empty( $options[ $default ] ) ) { + $default = ''; + } + + echo ''; + } + + submit_button( esc_html__( 'Filter', 'woocommerce' ), '', 'filter_action', false, array( 'id' => 'post-query-submit' ) ); + echo '
    '; + } + + /** + * Set the data for displaying. It will attempt to unserialize (There is a chance that some columns + * are serialized). This can be override in child classes for futher data transformation. + */ + protected function set_items( array $items ) { + $this->items = array(); + foreach ( $items as $item ) { + $this->items[ $item[ $this->ID ] ] = array_map( 'maybe_unserialize', $item ); + } + } + + /** + * Renders the checkbox for each row, this is the first column and it is named ID regardless + * of how the primary key is named (to keep the code simpler). The bulk actions will do the proper + * name transformation though using `$this->ID`. + */ + public function column_cb( $row ) { + return ''; + } + + /** + * Renders the row-actions. + * + * This method renders the action menu, it reads the definition from the $row_actions property, + * and it checks that the row action method exists before rendering it. + * + * @param array $row Row to render + * @param $column_name Current row + * @return + */ + protected function maybe_render_actions( $row, $column_name ) { + if ( empty( $this->row_actions[ $column_name ] ) ) { + return; + } + + $row_id = $row[ $this->ID ]; + + $actions = '
    '; + $action_count = 0; + foreach ( $this->row_actions[ $column_name ] as $action_key => $action ) { + + $action_count++; + + if ( ! method_exists( $this, 'row_action_' . $action_key ) ) { + continue; + } + + $action_link = ! empty( $action['link'] ) ? $action['link'] : add_query_arg( array( 'row_action' => $action_key, 'row_id' => $row_id, 'nonce' => wp_create_nonce( $action_key . '::' . $row_id ) ) ); + $span_class = ! empty( $action['class'] ) ? $action['class'] : $action_key; + $separator = ( $action_count < count( $this->row_actions[ $column_name ] ) ) ? ' | ' : ''; + + $actions .= sprintf( '', esc_attr( $span_class ) ); + $actions .= sprintf( '%3$s', esc_url( $action_link ), esc_attr( $action['desc'] ), esc_html( $action['name'] ) ); + $actions .= sprintf( '%s', $separator ); + } + $actions .= '
    '; + return $actions; + } + + protected function process_row_actions() { + $parameters = array( 'row_action', 'row_id', 'nonce' ); + foreach ( $parameters as $parameter ) { + if ( empty( $_REQUEST[ $parameter ] ) ) { + return; + } + } + + $method = 'row_action_' . $_REQUEST['row_action']; + + if ( $_REQUEST['nonce'] === wp_create_nonce( $_REQUEST[ 'row_action' ] . '::' . $_REQUEST[ 'row_id' ] ) && method_exists( $this, $method ) ) { + $this->$method( $_REQUEST['row_id'] ); + } + + wp_redirect( remove_query_arg( + array( 'row_id', 'row_action', 'nonce' ), + wp_unslash( $_SERVER['REQUEST_URI'] ) + ) ); + exit; + } + + /** + * Default column formatting, it will escape everythig for security. + */ + public function column_default( $item, $column_name ) { + $column_html = esc_html( $item[ $column_name ] ); + $column_html .= $this->maybe_render_actions( $item, $column_name ); + return $column_html; + } + + /** + * Display the table heading and search query, if any + */ + protected function display_header() { + echo '

    ' . esc_attr( $this->table_header ) . '

    '; + if ( $this->get_request_search_query() ) { + /* translators: %s: search query */ + echo '' . esc_attr( sprintf( __( 'Search results for "%s"', 'woocommerce' ), $this->get_request_search_query() ) ) . ''; + } + echo '
    '; + } + + /** + * Display the table heading and search query, if any + */ + protected function display_admin_notices() { + foreach ( $this->admin_notices as $notice ) { + echo '
    '; + echo '

    ' . wp_kses_post( $notice['message'] ) . '

    '; + echo '
    '; + } + } + + /** + * Prints the available statuses so the user can click to filter. + */ + protected function display_filter_by_status() { + + $status_list_items = array(); + $request_status = $this->get_request_status(); + + // Helper to set 'all' filter when not set on status counts passed in + if ( ! isset( $this->status_counts['all'] ) ) { + $this->status_counts = array( 'all' => array_sum( $this->status_counts ) ) + $this->status_counts; + } + + foreach ( $this->status_counts as $status_name => $count ) { + + if ( 0 === $count ) { + continue; + } + + if ( $status_name === $request_status || ( empty( $request_status ) && 'all' === $status_name ) ) { + $status_list_item = '
  • %3$s (%4$d)
  • '; + } else { + $status_list_item = '
  • %3$s (%4$d)
  • '; + } + + $status_filter_url = ( 'all' === $status_name ) ? remove_query_arg( 'status' ) : add_query_arg( 'status', $status_name ); + $status_filter_url = remove_query_arg( array( 'paged', 's' ), $status_filter_url ); + $status_list_items[] = sprintf( $status_list_item, esc_attr( $status_name ), esc_url( $status_filter_url ), esc_html( ucfirst( $status_name ) ), absint( $count ) ); + } + + if ( $status_list_items ) { + echo '
      '; + echo implode( " | \n", $status_list_items ); + echo '
    '; + } + } + + /** + * Renders the table list, we override the original class to render the table inside a form + * and to render any needed HTML (like the search box). By doing so the callee of a function can simple + * forget about any extra HTML. + */ + protected function display_table() { + echo '
    '; + foreach ( $_GET as $key => $value ) { + if ( '_' === $key[0] || 'paged' === $key || 'ID' === $key ) { + continue; + } + echo ''; + } + if ( ! empty( $this->search_by ) ) { + echo $this->search_box( $this->get_search_box_button_text(), 'plugin' ); // WPCS: XSS OK + } + parent::display(); + echo '
    '; + } + + /** + * Process any pending actions. + */ + public function process_actions() { + $this->process_bulk_action(); + $this->process_row_actions(); + + if ( ! empty( $_REQUEST['_wp_http_referer'] ) ) { + // _wp_http_referer is used only on bulk actions, we remove it to keep the $_GET shorter + wp_redirect( remove_query_arg( array( '_wp_http_referer', '_wpnonce' ), wp_unslash( $_SERVER['REQUEST_URI'] ) ) ); + exit; + } + } + + /** + * Render the list table page, including header, notices, status filters and table. + */ + public function display_page() { + $this->prepare_items(); + + echo '
    '; + $this->display_header(); + $this->display_admin_notices(); + $this->display_filter_by_status(); + $this->display_table(); + echo '
    '; + } + + /** + * Get the text to display in the search box on the list table. + */ + protected function get_search_box_placeholder() { + return esc_html__( 'Search', 'woocommerce' ); + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php new file mode 100644 index 0000000..82ecbc6 --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_QueueRunner.php @@ -0,0 +1,240 @@ +created_time = microtime( true ); + + $this->store = $store ? $store : ActionScheduler_Store::instance(); + $this->monitor = $monitor ? $monitor : new ActionScheduler_FatalErrorMonitor( $this->store ); + $this->cleaner = $cleaner ? $cleaner : new ActionScheduler_QueueCleaner( $this->store ); + } + + /** + * Process an individual action. + * + * @param int $action_id The action ID to process. + * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * Generally, this should be capitalised and not localised as it's a proper noun. + */ + public function process_action( $action_id, $context = '' ) { + try { + $valid_action = false; + do_action( 'action_scheduler_before_execute', $action_id, $context ); + + if ( ActionScheduler_Store::STATUS_PENDING !== $this->store->get_status( $action_id ) ) { + do_action( 'action_scheduler_execution_ignored', $action_id, $context ); + return; + } + + $valid_action = true; + do_action( 'action_scheduler_begin_execute', $action_id, $context ); + + $action = $this->store->fetch_action( $action_id ); + $this->store->log_execution( $action_id ); + $action->execute(); + do_action( 'action_scheduler_after_execute', $action_id, $action, $context ); + $this->store->mark_complete( $action_id ); + } catch ( Exception $e ) { + if ( $valid_action ) { + $this->store->mark_failure( $action_id ); + do_action( 'action_scheduler_failed_execution', $action_id, $e, $context ); + } else { + do_action( 'action_scheduler_failed_validation', $action_id, $e, $context ); + } + } + + if ( isset( $action ) && is_a( $action, 'ActionScheduler_Action' ) && $action->get_schedule()->is_recurring() ) { + $this->schedule_next_instance( $action, $action_id ); + } + } + + /** + * Schedule the next instance of the action if necessary. + * + * @param ActionScheduler_Action $action + * @param int $action_id + */ + protected function schedule_next_instance( ActionScheduler_Action $action, $action_id ) { + try { + ActionScheduler::factory()->repeat( $action ); + } catch ( Exception $e ) { + do_action( 'action_scheduler_failed_to_schedule_next_instance', $action_id, $e, $action ); + } + } + + /** + * Run the queue cleaner. + * + * @author Jeremy Pry + */ + protected function run_cleanup() { + $this->cleaner->clean( 10 * $this->get_time_limit() ); + } + + /** + * Get the number of concurrent batches a runner allows. + * + * @return int + */ + public function get_allowed_concurrent_batches() { + return apply_filters( 'action_scheduler_queue_runner_concurrent_batches', 1 ); + } + + /** + * Check if the number of allowed concurrent batches is met or exceeded. + * + * @return bool + */ + public function has_maximum_concurrent_batches() { + return $this->store->get_claim_count() >= $this->get_allowed_concurrent_batches(); + } + + /** + * Get the maximum number of seconds a batch can run for. + * + * @return int The number of seconds. + */ + protected function get_time_limit() { + + $time_limit = 30; + + // Apply deprecated filter from deprecated get_maximum_execution_time() method + if ( has_filter( 'action_scheduler_maximum_execution_time' ) ) { + _deprecated_function( 'action_scheduler_maximum_execution_time', '2.1.1', 'action_scheduler_queue_runner_time_limit' ); + $time_limit = apply_filters( 'action_scheduler_maximum_execution_time', $time_limit ); + } + + return absint( apply_filters( 'action_scheduler_queue_runner_time_limit', $time_limit ) ); + } + + /** + * Get the number of seconds the process has been running. + * + * @return int The number of seconds. + */ + protected function get_execution_time() { + $execution_time = microtime( true ) - $this->created_time; + + // Get the CPU time if the hosting environment uses it rather than wall-clock time to calculate a process's execution time. + if ( function_exists( 'getrusage' ) && apply_filters( 'action_scheduler_use_cpu_execution_time', defined( 'PANTHEON_ENVIRONMENT' ) ) ) { + $resource_usages = getrusage(); + + if ( isset( $resource_usages['ru_stime.tv_usec'], $resource_usages['ru_stime.tv_usec'] ) ) { + $execution_time = $resource_usages['ru_stime.tv_sec'] + ( $resource_usages['ru_stime.tv_usec'] / 1000000 ); + } + } + + return $execution_time; + } + + /** + * Check if the host's max execution time is (likely) to be exceeded if processing more actions. + * + * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action + * @return bool + */ + protected function time_likely_to_be_exceeded( $processed_actions ) { + + $execution_time = $this->get_execution_time(); + $max_execution_time = $this->get_time_limit(); + $time_per_action = $execution_time / $processed_actions; + $estimated_time = $execution_time + ( $time_per_action * 3 ); + $likely_to_be_exceeded = $estimated_time > $max_execution_time; + + return apply_filters( 'action_scheduler_maximum_execution_time_likely_to_be_exceeded', $likely_to_be_exceeded, $this, $processed_actions, $execution_time, $max_execution_time ); + } + + /** + * Get memory limit + * + * Based on WP_Background_Process::get_memory_limit() + * + * @return int + */ + protected function get_memory_limit() { + if ( function_exists( 'ini_get' ) ) { + $memory_limit = ini_get( 'memory_limit' ); + } else { + $memory_limit = '128M'; // Sensible default, and minimum required by WooCommerce + } + + if ( ! $memory_limit || -1 === $memory_limit || '-1' === $memory_limit ) { + // Unlimited, set to 32GB. + $memory_limit = '32G'; + } + + return ActionScheduler_Compatibility::convert_hr_to_bytes( $memory_limit ); + } + + /** + * Memory exceeded + * + * Ensures the batch process never exceeds 90% of the maximum WordPress memory. + * + * Based on WP_Background_Process::memory_exceeded() + * + * @return bool + */ + protected function memory_exceeded() { + + $memory_limit = $this->get_memory_limit() * 0.90; + $current_memory = memory_get_usage( true ); + $memory_exceeded = $current_memory >= $memory_limit; + + return apply_filters( 'action_scheduler_memory_exceeded', $memory_exceeded, $this ); + } + + /** + * See if the batch limits have been exceeded, which is when memory usage is almost at + * the maximum limit, or the time to process more actions will exceed the max time limit. + * + * Based on WC_Background_Process::batch_limits_exceeded() + * + * @param int $processed_actions The number of actions processed so far - used to determine the likelihood of exceeding the time limit if processing another action + * @return bool + */ + protected function batch_limits_exceeded( $processed_actions ) { + return $this->memory_exceeded() || $this->time_likely_to_be_exceeded( $processed_actions ); + } + + /** + * Process actions in the queue. + * + * @author Jeremy Pry + * @param string $context Optional identifer for the context in which this action is being processed, e.g. 'WP CLI' or 'WP Cron' + * Generally, this should be capitalised and not localised as it's a proper noun. + * @return int The number of actions processed. + */ + abstract public function run( $context = '' ); +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php new file mode 100644 index 0000000..131d475 --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_RecurringSchedule.php @@ -0,0 +1,102 @@ +start - and logic to calculate the next run date after + * that - @see $this->calculate_next(). The $first_date property also keeps a record of when the very + * first instance of this chain of schedules ran. + * + * @var DateTime + */ + private $first_date = NULL; + + /** + * Timestamp equivalent of @see $this->first_date + * + * @var int + */ + protected $first_timestamp = NULL; + + /** + * The recurrance between each time an action is run using this schedule. + * Used to calculate the start date & time. Can be a number of seconds, in the + * case of ActionScheduler_IntervalSchedule, or a cron expression, as in the + * case of ActionScheduler_CronSchedule. Or something else. + * + * @var mixed + */ + protected $recurrence; + + /** + * @param DateTime $date The date & time to run the action. + * @param mixed $recurrence The data used to determine the schedule's recurrance. + * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. + */ + public function __construct( DateTime $date, $recurrence, DateTime $first = null ) { + parent::__construct( $date ); + $this->first_date = empty( $first ) ? $date : $first; + $this->recurrence = $recurrence; + } + + /** + * @return bool + */ + public function is_recurring() { + return true; + } + + /** + * Get the date & time of the first schedule in this recurring series. + * + * @return DateTime|null + */ + public function get_first_date() { + return clone $this->first_date; + } + + /** + * @return string + */ + public function get_recurrence() { + return $this->recurrence; + } + + /** + * For PHP 5.2 compat, since DateTime objects can't be serialized + * @return array + */ + public function __sleep() { + $sleep_params = parent::__sleep(); + $this->first_timestamp = $this->first_date->getTimestamp(); + return array_merge( $sleep_params, array( + 'first_timestamp', + 'recurrence' + ) ); + } + + /** + * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, schedules used different property names to refer + * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. This was addressed in + * Action Scheduler 3.0.0, where properties and property names were aligned for better + * inheritance. To maintain backward compatibility with scheduled serialized and stored + * prior to 3.0, we need to correctly map the old property names. + */ + public function __wakeup() { + parent::__wakeup(); + if ( $this->first_timestamp > 0 ) { + $this->first_date = as_get_datetime_object( $this->first_timestamp ); + } else { + $this->first_date = $this->get_date(); + } + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php new file mode 100644 index 0000000..2631ef5 --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schedule.php @@ -0,0 +1,83 @@ +scheduled_date + * + * @var int + */ + protected $scheduled_timestamp = NULL; + + /** + * @param DateTime $date The date & time to run the action. + */ + public function __construct( DateTime $date ) { + $this->scheduled_date = $date; + } + + /** + * Check if a schedule should recur. + * + * @return bool + */ + abstract public function is_recurring(); + + /** + * Calculate when the next instance of this schedule would run based on a given date & time. + * + * @param DateTime $after + * @return DateTime + */ + abstract protected function calculate_next( DateTime $after ); + + /** + * Get the next date & time when this schedule should run after a given date & time. + * + * @param DateTime $after + * @return DateTime|null + */ + public function get_next( DateTime $after ) { + $after = clone $after; + if ( $after > $this->scheduled_date ) { + $after = $this->calculate_next( $after ); + return $after; + } + return clone $this->scheduled_date; + } + + /** + * Get the date & time the schedule is set to run. + * + * @return DateTime|null + */ + public function get_date() { + return $this->scheduled_date; + } + + /** + * For PHP 5.2 compat, since DateTime objects can't be serialized + * @return array + */ + public function __sleep() { + $this->scheduled_timestamp = $this->scheduled_date->getTimestamp(); + return array( + 'scheduled_timestamp', + ); + } + + public function __wakeup() { + $this->scheduled_date = as_get_datetime_object( $this->scheduled_timestamp ); + unset( $this->scheduled_timestamp ); + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php new file mode 100644 index 0000000..2334fda --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_Abstract_Schema.php @@ -0,0 +1,172 @@ +tables as $table ) { + $wpdb->tables[] = $table; + $name = $this->get_full_table_name( $table ); + $wpdb->$table = $name; + } + + // create the tables + if ( $this->schema_update_required() || $force_update ) { + foreach ( $this->tables as $table ) { + /** + * Allow custom processing before updating a table schema. + * + * @param string $table Name of table being updated. + * @param string $db_version Existing version of the table being updated. + */ + do_action( 'action_scheduler_before_schema_update', $table, $this->db_version ); + $this->update_table( $table ); + } + $this->mark_schema_update_complete(); + } + } + + /** + * @param string $table The name of the table + * + * @return string The CREATE TABLE statement, suitable for passing to dbDelta + */ + abstract protected function get_table_definition( $table ); + + /** + * Determine if the database schema is out of date + * by comparing the integer found in $this->schema_version + * with the option set in the WordPress options table + * + * @return bool + */ + private function schema_update_required() { + $option_name = 'schema-' . static::class; + $this->db_version = get_option( $option_name, 0 ); + + // Check for schema option stored by the Action Scheduler Custom Tables plugin in case site has migrated from that plugin with an older schema + if ( 0 === $this->db_version ) { + + $plugin_option_name = 'schema-'; + + switch ( static::class ) { + case 'ActionScheduler_StoreSchema' : + $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Store_Table_Maker'; + break; + case 'ActionScheduler_LoggerSchema' : + $plugin_option_name .= 'Action_Scheduler\Custom_Tables\DB_Logger_Table_Maker'; + break; + } + + $this->db_version = get_option( $plugin_option_name, 0 ); + + delete_option( $plugin_option_name ); + } + + return version_compare( $this->db_version, $this->schema_version, '<' ); + } + + /** + * Update the option in WordPress to indicate that + * our schema is now up to date + * + * @return void + */ + private function mark_schema_update_complete() { + $option_name = 'schema-' . static::class; + + // work around race conditions and ensure that our option updates + $value_to_save = (string) $this->schema_version . '.0.' . time(); + + update_option( $option_name, $value_to_save ); + } + + /** + * Update the schema for the given table + * + * @param string $table The name of the table to update + * + * @return void + */ + private function update_table( $table ) { + require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); + $definition = $this->get_table_definition( $table ); + if ( $definition ) { + $updated = dbDelta( $definition ); + foreach ( $updated as $updated_table => $update_description ) { + if ( strpos( $update_description, 'Created table' ) === 0 ) { + do_action( 'action_scheduler/created_table', $updated_table, $table ); + } + } + } + } + + /** + * @param string $table + * + * @return string The full name of the table, including the + * table prefix for the current blog + */ + protected function get_full_table_name( $table ) { + return $GLOBALS[ 'wpdb' ]->prefix . $table; + } + + /** + * Confirms that all of the tables registered by this schema class have been created. + * + * @return bool + */ + public function tables_exist() { + global $wpdb; + + $existing_tables = $wpdb->get_col( 'SHOW TABLES' ); + $expected_tables = array_map( + function ( $table_name ) use ( $wpdb ) { + return $wpdb->prefix . $table_name; + }, + $this->tables + ); + + return count( array_intersect( $existing_tables, $expected_tables ) ) === count( $expected_tables ); + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_Lock.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_Lock.php new file mode 100644 index 0000000..86e8528 --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_Lock.php @@ -0,0 +1,62 @@ +get_expiration( $lock_type ) >= time() ); + } + + /** + * Set a lock. + * + * @param string $lock_type A string to identify different lock types. + * @return bool + */ + abstract public function set( $lock_type ); + + /** + * If a lock is set, return the timestamp it was set to expiry. + * + * @param string $lock_type A string to identify different lock types. + * @return bool|int False if no lock is set, otherwise the timestamp for when the lock is set to expire. + */ + abstract public function get_expiration( $lock_type ); + + /** + * Get the amount of time to set for a given lock. 60 seconds by default. + * + * @param string $lock_type A string to identify different lock types. + * @return int + */ + protected function get_duration( $lock_type ) { + return apply_filters( 'action_scheduler_lock_duration', self::$lock_duration, $lock_type ); + } + + /** + * @return ActionScheduler_Lock + */ + public static function instance() { + if ( empty( self::$locker ) ) { + $class = apply_filters( 'action_scheduler_lock_class', 'ActionScheduler_OptionLock' ); + self::$locker = new $class(); + } + return self::$locker; + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_Logger.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_Logger.php new file mode 100644 index 0000000..1d47201 --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_Logger.php @@ -0,0 +1,176 @@ +hook_stored_action(); + add_action( 'action_scheduler_canceled_action', array( $this, 'log_canceled_action' ), 10, 1 ); + add_action( 'action_scheduler_begin_execute', array( $this, 'log_started_action' ), 10, 2 ); + add_action( 'action_scheduler_after_execute', array( $this, 'log_completed_action' ), 10, 3 ); + add_action( 'action_scheduler_failed_execution', array( $this, 'log_failed_action' ), 10, 3 ); + add_action( 'action_scheduler_failed_action', array( $this, 'log_timed_out_action' ), 10, 2 ); + add_action( 'action_scheduler_unexpected_shutdown', array( $this, 'log_unexpected_shutdown' ), 10, 2 ); + add_action( 'action_scheduler_reset_action', array( $this, 'log_reset_action' ), 10, 1 ); + add_action( 'action_scheduler_execution_ignored', array( $this, 'log_ignored_action' ), 10, 2 ); + add_action( 'action_scheduler_failed_fetch_action', array( $this, 'log_failed_fetch_action' ), 10, 2 ); + add_action( 'action_scheduler_failed_to_schedule_next_instance', array( $this, 'log_failed_schedule_next_instance' ), 10, 2 ); + add_action( 'action_scheduler_bulk_cancel_actions', array( $this, 'bulk_log_cancel_actions' ), 10, 1 ); + } + + public function hook_stored_action() { + add_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) ); + } + + public function unhook_stored_action() { + remove_action( 'action_scheduler_stored_action', array( $this, 'log_stored_action' ) ); + } + + public function log_stored_action( $action_id ) { + $this->log( $action_id, __( 'action created', 'woocommerce' ) ); + } + + public function log_canceled_action( $action_id ) { + $this->log( $action_id, __( 'action canceled', 'woocommerce' ) ); + } + + public function log_started_action( $action_id, $context = '' ) { + if ( ! empty( $context ) ) { + /* translators: %s: context */ + $message = sprintf( __( 'action started via %s', 'woocommerce' ), $context ); + } else { + $message = __( 'action started', 'woocommerce' ); + } + $this->log( $action_id, $message ); + } + + public function log_completed_action( $action_id, $action = NULL, $context = '' ) { + if ( ! empty( $context ) ) { + /* translators: %s: context */ + $message = sprintf( __( 'action complete via %s', 'woocommerce' ), $context ); + } else { + $message = __( 'action complete', 'woocommerce' ); + } + $this->log( $action_id, $message ); + } + + public function log_failed_action( $action_id, Exception $exception, $context = '' ) { + if ( ! empty( $context ) ) { + /* translators: 1: context 2: exception message */ + $message = sprintf( __( 'action failed via %1$s: %2$s', 'woocommerce' ), $context, $exception->getMessage() ); + } else { + /* translators: %s: exception message */ + $message = sprintf( __( 'action failed: %s', 'woocommerce' ), $exception->getMessage() ); + } + $this->log( $action_id, $message ); + } + + public function log_timed_out_action( $action_id, $timeout ) { + /* translators: %s: amount of time */ + $this->log( $action_id, sprintf( __( 'action timed out after %s seconds', 'woocommerce' ), $timeout ) ); + } + + public function log_unexpected_shutdown( $action_id, $error ) { + if ( ! empty( $error ) ) { + /* translators: 1: error message 2: filename 3: line */ + $this->log( $action_id, sprintf( __( 'unexpected shutdown: PHP Fatal error %1$s in %2$s on line %3$s', 'woocommerce' ), $error['message'], $error['file'], $error['line'] ) ); + } + } + + public function log_reset_action( $action_id ) { + $this->log( $action_id, __( 'action reset', 'woocommerce' ) ); + } + + public function log_ignored_action( $action_id, $context = '' ) { + if ( ! empty( $context ) ) { + /* translators: %s: context */ + $message = sprintf( __( 'action ignored via %s', 'woocommerce' ), $context ); + } else { + $message = __( 'action ignored', 'woocommerce' ); + } + $this->log( $action_id, $message ); + } + + /** + * @param string $action_id + * @param Exception|NULL $exception The exception which occured when fetching the action. NULL by default for backward compatibility. + * + * @return ActionScheduler_LogEntry[] + */ + public function log_failed_fetch_action( $action_id, Exception $exception = NULL ) { + + if ( ! is_null( $exception ) ) { + /* translators: %s: exception message */ + $log_message = sprintf( __( 'There was a failure fetching this action: %s', 'woocommerce' ), $exception->getMessage() ); + } else { + $log_message = __( 'There was a failure fetching this action', 'woocommerce' ); + } + + $this->log( $action_id, $log_message ); + } + + public function log_failed_schedule_next_instance( $action_id, Exception $exception ) { + /* translators: %s: exception message */ + $this->log( $action_id, sprintf( __( 'There was a failure scheduling the next instance of this action: %s', 'woocommerce' ), $exception->getMessage() ) ); + } + + /** + * Bulk add cancel action log entries. + * + * Implemented here for backward compatibility. Should be implemented in parent loggers + * for more performant bulk logging. + * + * @param array $action_ids List of action ID. + */ + public function bulk_log_cancel_actions( $action_ids ) { + if ( empty( $action_ids ) ) { + return; + } + + foreach ( $action_ids as $action_id ) { + $this->log_canceled_action( $action_id ); + } + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_Store.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_Store.php new file mode 100644 index 0000000..16d7f13 --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_Store.php @@ -0,0 +1,422 @@ + null, + 'status' => self::STATUS_PENDING, + 'group' => '', + ) + ); + + // These params are fixed for this method. + $params['hook'] = $hook; + $params['orderby'] = 'date'; + $params['per_page'] = 1; + + if ( ! empty( $params['status'] ) ) { + if ( self::STATUS_PENDING === $params['status'] ) { + $params['order'] = 'ASC'; // Find the next action that matches. + } else { + $params['order'] = 'DESC'; // Find the most recent action that matches. + } + } + + $results = $this->query_actions( $params ); + + return empty( $results ) ? null : $results[0]; + } + + /** + * Query for action count or list of action IDs. + * + * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * + * @param array $query { + * Query filtering options. + * + * @type string $hook The name of the actions. Optional. + * @type string|array $status The status or statuses of the actions. Optional. + * @type array $args The args array of the actions. Optional. + * @type DateTime $date The scheduled date of the action. Used in UTC timezone. Optional. + * @type string $date_compare Operator for selecting by $date param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='. + * @type DateTime $modified The last modified date of the action. Used in UTC timezone. Optional. + * @type string $modified_compare Operator for comparing $modified param. Accepted values are '!=', '>', '>=', '<', '<=', '='. Defaults to '<='. + * @type string $group The group the action belongs to. Optional. + * @type bool|int $claimed TRUE to find claimed actions, FALSE to find unclaimed actions, an int to find a specific claim ID. Optional. + * @type int $per_page Number of results to return. Defaults to 5. + * @type int $offset The query pagination offset. Defaults to 0. + * @type int $orderby Accepted values are 'hook', 'group', 'modified', 'date' or 'none'. Defaults to 'date'. + * @type string $order Accepted values are 'ASC' or 'DESC'. Defaults to 'ASC'. + * } + * @param string $query_type Whether to select or count the results. Default, select. + * + * @return string|array|null The IDs of actions matching the query. Null on failure. + */ + abstract public function query_actions( $query = array(), $query_type = 'select' ); + + /** + * Run query to get a single action ID. + * + * @since x.x.x + * + * @see ActionScheduler_Store::query_actions for $query arg usage but 'per_page' and 'offset' can't be used. + * + * @param array $query Query parameters. + * + * @return int|null + */ + public function query_action( $query ) { + $query['per_page'] = 1; + $query['offset'] = 0; + $results = $this->query_actions( $query ); + + if ( empty( $results ) ) { + return null; + } else { + return (int) $results[0]; + } + } + + /** + * Get a count of all actions in the store, grouped by status + * + * @return array + */ + abstract public function action_counts(); + + /** + * @param string $action_id + */ + abstract public function cancel_action( $action_id ); + + /** + * @param string $action_id + */ + abstract public function delete_action( $action_id ); + + /** + * @param string $action_id + * + * @return DateTime The date the action is schedule to run, or the date that it ran. + */ + abstract public function get_date( $action_id ); + + + /** + * @param int $max_actions + * @param DateTime $before_date Claim only actions schedule before the given date. Defaults to now. + * @param array $hooks Claim only actions with a hook or hooks. + * @param string $group Claim only actions in the given group. + * + * @return ActionScheduler_ActionClaim + */ + abstract public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ); + + /** + * @return int + */ + abstract public function get_claim_count(); + + /** + * @param ActionScheduler_ActionClaim $claim + */ + abstract public function release_claim( ActionScheduler_ActionClaim $claim ); + + /** + * @param string $action_id + */ + abstract public function unclaim_action( $action_id ); + + /** + * @param string $action_id + */ + abstract public function mark_failure( $action_id ); + + /** + * @param string $action_id + */ + abstract public function log_execution( $action_id ); + + /** + * @param string $action_id + */ + abstract public function mark_complete( $action_id ); + + /** + * @param string $action_id + * + * @return string + */ + abstract public function get_status( $action_id ); + + /** + * @param string $action_id + * @return mixed + */ + abstract public function get_claim_id( $action_id ); + + /** + * @param string $claim_id + * @return array + */ + abstract public function find_actions_by_claim_id( $claim_id ); + + /** + * @param string $comparison_operator + * @return string + */ + protected function validate_sql_comparator( $comparison_operator ) { + if ( in_array( $comparison_operator, array('!=', '>', '>=', '<', '<=', '=') ) ) { + return $comparison_operator; + } + return '='; + } + + /** + * Get the time MySQL formated date/time string for an action's (next) scheduled date. + * + * @param ActionScheduler_Action $action + * @param DateTime $scheduled_date (optional) + * @return string + */ + protected function get_scheduled_date_string( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) { + $next = null === $scheduled_date ? $action->get_schedule()->get_date() : $scheduled_date; + if ( ! $next ) { + return '0000-00-00 00:00:00'; + } + $next->setTimezone( new DateTimeZone( 'UTC' ) ); + + return $next->format( 'Y-m-d H:i:s' ); + } + + /** + * Get the time MySQL formated date/time string for an action's (next) scheduled date. + * + * @param ActionScheduler_Action $action + * @param DateTime $scheduled_date (optional) + * @return string + */ + protected function get_scheduled_date_string_local( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) { + $next = null === $scheduled_date ? $action->get_schedule()->get_date() : $scheduled_date; + if ( ! $next ) { + return '0000-00-00 00:00:00'; + } + + ActionScheduler_TimezoneHelper::set_local_timezone( $next ); + return $next->format( 'Y-m-d H:i:s' ); + } + + /** + * Validate that we could decode action arguments. + * + * @param mixed $args The decoded arguments. + * @param int $action_id The action ID. + * + * @throws ActionScheduler_InvalidActionException When the decoded arguments are invalid. + */ + protected function validate_args( $args, $action_id ) { + // Ensure we have an array of args. + if ( ! is_array( $args ) ) { + throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id ); + } + + // Validate JSON decoding if possible. + if ( function_exists( 'json_last_error' ) && JSON_ERROR_NONE !== json_last_error() ) { + throw ActionScheduler_InvalidActionException::from_decoding_args( $action_id, $args ); + } + } + + /** + * Validate a ActionScheduler_Schedule object. + * + * @param mixed $schedule The unserialized ActionScheduler_Schedule object. + * @param int $action_id The action ID. + * + * @throws ActionScheduler_InvalidActionException When the schedule is invalid. + */ + protected function validate_schedule( $schedule, $action_id ) { + if ( empty( $schedule ) || ! is_a( $schedule, 'ActionScheduler_Schedule' ) ) { + throw ActionScheduler_InvalidActionException::from_schedule( $action_id, $schedule ); + } + } + + /** + * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4. + * + * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However, + * with custom tables, we use an indexed VARCHAR column instead. + * + * @param ActionScheduler_Action $action Action to be validated. + * @throws InvalidArgumentException When json encoded args is too long. + */ + protected function validate_action( ActionScheduler_Action $action ) { + if ( strlen( json_encode( $action->get_args() ) ) > static::$max_args_length ) { + throw new InvalidArgumentException( sprintf( __( 'ActionScheduler_Action::$args too long. To ensure the args column can be indexed, action args should not be more than %d characters when encoded as JSON.', 'woocommerce' ), static::$max_args_length ) ); + } + } + + /** + * Cancel pending actions by hook. + * + * @since 3.0.0 + * + * @param string $hook Hook name. + * + * @return void + */ + public function cancel_actions_by_hook( $hook ) { + $action_ids = true; + while ( ! empty( $action_ids ) ) { + $action_ids = $this->query_actions( + array( + 'hook' => $hook, + 'status' => self::STATUS_PENDING, + 'per_page' => 1000, + 'orderby' => 'action_id', + ) + ); + + $this->bulk_cancel_actions( $action_ids ); + } + } + + /** + * Cancel pending actions by group. + * + * @since 3.0.0 + * + * @param string $group Group slug. + * + * @return void + */ + public function cancel_actions_by_group( $group ) { + $action_ids = true; + while ( ! empty( $action_ids ) ) { + $action_ids = $this->query_actions( + array( + 'group' => $group, + 'status' => self::STATUS_PENDING, + 'per_page' => 1000, + 'orderby' => 'action_id', + ) + ); + + $this->bulk_cancel_actions( $action_ids ); + } + } + + /** + * Cancel a set of action IDs. + * + * @since 3.0.0 + * + * @param array $action_ids List of action IDs. + * + * @return void + */ + private function bulk_cancel_actions( $action_ids ) { + foreach ( $action_ids as $action_id ) { + $this->cancel_action( $action_id ); + } + + do_action( 'action_scheduler_bulk_cancel_actions', $action_ids ); + } + + /** + * @return array + */ + public function get_status_labels() { + return array( + self::STATUS_COMPLETE => __( 'Complete', 'woocommerce' ), + self::STATUS_PENDING => __( 'Pending', 'woocommerce' ), + self::STATUS_RUNNING => __( 'In-progress', 'woocommerce' ), + self::STATUS_FAILED => __( 'Failed', 'woocommerce' ), + self::STATUS_CANCELED => __( 'Canceled', 'woocommerce' ), + ); + } + + /** + * Check if there are any pending scheduled actions due to run. + * + * @param ActionScheduler_Action $action + * @param DateTime $scheduled_date (optional) + * @return string + */ + public function has_pending_actions_due() { + $pending_actions = $this->query_actions( array( + 'date' => as_get_datetime_object(), + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'orderby' => 'none', + ) ); + + return ! empty( $pending_actions ); + } + + /** + * Callable initialization function optionally overridden in derived classes. + */ + public function init() {} + + /** + * Callable function to mark an action as migrated optionally overridden in derived classes. + */ + public function mark_migrated( $action_id ) {} + + /** + * @return ActionScheduler_Store + */ + public static function instance() { + if ( empty( self::$store ) ) { + $class = apply_filters( 'action_scheduler_store_class', self::DEFAULT_CLASS ); + self::$store = new $class(); + } + return self::$store; + } +} diff --git a/packages/action-scheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php b/packages/action-scheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php new file mode 100644 index 0000000..fd01449 --- /dev/null +++ b/packages/action-scheduler/classes/abstracts/ActionScheduler_TimezoneHelper.php @@ -0,0 +1,152 @@ +format( 'U' ) ); + } + + if ( get_option( 'timezone_string' ) ) { + $date->setTimezone( new DateTimeZone( self::get_local_timezone_string() ) ); + } else { + $date->setUtcOffset( self::get_local_timezone_offset() ); + } + + return $date; + } + + /** + * Helper to retrieve the timezone string for a site until a WP core method exists + * (see https://core.trac.wordpress.org/ticket/24730). + * + * Adapted from wc_timezone_string() and https://secure.php.net/manual/en/function.timezone-name-from-abbr.php#89155. + * + * If no timezone string is set, and its not possible to match the UTC offset set for the site to a timezone + * string, then an empty string will be returned, and the UTC offset should be used to set a DateTime's + * timezone. + * + * @since 2.1.0 + * @return string PHP timezone string for the site or empty if no timezone string is available. + */ + protected static function get_local_timezone_string( $reset = false ) { + // If site timezone string exists, return it. + $timezone = get_option( 'timezone_string' ); + if ( $timezone ) { + return $timezone; + } + + // Get UTC offset, if it isn't set then return UTC. + $utc_offset = intval( get_option( 'gmt_offset', 0 ) ); + if ( 0 === $utc_offset ) { + return 'UTC'; + } + + // Adjust UTC offset from hours to seconds. + $utc_offset *= 3600; + + // Attempt to guess the timezone string from the UTC offset. + $timezone = timezone_name_from_abbr( '', $utc_offset ); + if ( $timezone ) { + return $timezone; + } + + // Last try, guess timezone string manually. + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + if ( (bool) date( 'I' ) === (bool) $city['dst'] && $city['timezone_id'] && intval( $city['offset'] ) === $utc_offset ) { + return $city['timezone_id']; + } + } + } + + // No timezone string + return ''; + } + + /** + * Get timezone offset in seconds. + * + * @since 2.1.0 + * @return float + */ + protected static function get_local_timezone_offset() { + $timezone = get_option( 'timezone_string' ); + + if ( $timezone ) { + $timezone_object = new DateTimeZone( $timezone ); + return $timezone_object->getOffset( new DateTime( 'now' ) ); + } else { + return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS; + } + } + + /** + * @deprecated 2.1.0 + */ + public static function get_local_timezone( $reset = FALSE ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); + if ( $reset ) { + self::$local_timezone = NULL; + } + if ( !isset(self::$local_timezone) ) { + $tzstring = get_option('timezone_string'); + + if ( empty($tzstring) ) { + $gmt_offset = get_option('gmt_offset'); + if ( $gmt_offset == 0 ) { + $tzstring = 'UTC'; + } else { + $gmt_offset *= HOUR_IN_SECONDS; + $tzstring = timezone_name_from_abbr( '', $gmt_offset, 1 ); + + // If there's no timezone string, try again with no DST. + if ( false === $tzstring ) { + $tzstring = timezone_name_from_abbr( '', $gmt_offset, 0 ); + } + + // Try mapping to the first abbreviation we can find. + if ( false === $tzstring ) { + $is_dst = date( 'I' ); + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + if ( $city['dst'] == $is_dst && $city['offset'] == $gmt_offset ) { + // If there's no valid timezone ID, keep looking. + if ( null === $city['timezone_id'] ) { + continue; + } + + $tzstring = $city['timezone_id']; + break 2; + } + } + } + } + + // If we still have no valid string, then fall back to UTC. + if ( false === $tzstring ) { + $tzstring = 'UTC'; + } + } + } + + self::$local_timezone = new DateTimeZone($tzstring); + } + return self::$local_timezone; + } +} diff --git a/packages/action-scheduler/classes/actions/ActionScheduler_Action.php b/packages/action-scheduler/classes/actions/ActionScheduler_Action.php new file mode 100644 index 0000000..520f932 --- /dev/null +++ b/packages/action-scheduler/classes/actions/ActionScheduler_Action.php @@ -0,0 +1,75 @@ +set_hook($hook); + $this->set_schedule($schedule); + $this->set_args($args); + $this->set_group($group); + } + + public function execute() { + return do_action_ref_array( $this->get_hook(), array_values( $this->get_args() ) ); + } + + /** + * @param string $hook + */ + protected function set_hook( $hook ) { + $this->hook = $hook; + } + + public function get_hook() { + return $this->hook; + } + + protected function set_schedule( ActionScheduler_Schedule $schedule ) { + $this->schedule = $schedule; + } + + /** + * @return ActionScheduler_Schedule + */ + public function get_schedule() { + return $this->schedule; + } + + protected function set_args( array $args ) { + $this->args = $args; + } + + public function get_args() { + return $this->args; + } + + /** + * @param string $group + */ + protected function set_group( $group ) { + $this->group = $group; + } + + /** + * @return string + */ + public function get_group() { + return $this->group; + } + + /** + * @return bool If the action has been finished + */ + public function is_finished() { + return FALSE; + } +} diff --git a/packages/action-scheduler/classes/actions/ActionScheduler_CanceledAction.php b/packages/action-scheduler/classes/actions/ActionScheduler_CanceledAction.php new file mode 100644 index 0000000..8bbc5d1 --- /dev/null +++ b/packages/action-scheduler/classes/actions/ActionScheduler_CanceledAction.php @@ -0,0 +1,23 @@ +set_schedule( new ActionScheduler_NullSchedule() ); + } + } +} diff --git a/packages/action-scheduler/classes/actions/ActionScheduler_FinishedAction.php b/packages/action-scheduler/classes/actions/ActionScheduler_FinishedAction.php new file mode 100644 index 0000000..b23a56c --- /dev/null +++ b/packages/action-scheduler/classes/actions/ActionScheduler_FinishedAction.php @@ -0,0 +1,16 @@ +set_schedule( new ActionScheduler_NullSchedule() ); + } + + public function execute() { + // don't execute + } +} + \ No newline at end of file diff --git a/packages/action-scheduler/classes/data-stores/ActionScheduler_DBLogger.php b/packages/action-scheduler/classes/data-stores/ActionScheduler_DBLogger.php new file mode 100644 index 0000000..8424f0d --- /dev/null +++ b/packages/action-scheduler/classes/data-stores/ActionScheduler_DBLogger.php @@ -0,0 +1,150 @@ +format( 'Y-m-d H:i:s' ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + $date_local = $date->format( 'Y-m-d H:i:s' ); + + /** @var \wpdb $wpdb */ + global $wpdb; + $wpdb->insert( $wpdb->actionscheduler_logs, [ + 'action_id' => $action_id, + 'message' => $message, + 'log_date_gmt' => $date_gmt, + 'log_date_local' => $date_local, + ], [ '%d', '%s', '%s', '%s' ] ); + + return $wpdb->insert_id; + } + + /** + * Retrieve an action log entry. + * + * @param int $entry_id Log entry ID. + * + * @return ActionScheduler_LogEntry + */ + public function get_entry( $entry_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $entry = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE log_id=%d", $entry_id ) ); + + return $this->create_entry_from_db_record( $entry ); + } + + /** + * Create an action log entry from a database record. + * + * @param object $record Log entry database record object. + * + * @return ActionScheduler_LogEntry + */ + private function create_entry_from_db_record( $record ) { + if ( empty( $record ) ) { + return new ActionScheduler_NullLogEntry(); + } + + if ( is_null( $record->log_date_gmt ) ) { + $date = as_get_datetime_object( ActionScheduler_StoreSchema::DEFAULT_DATE ); + } else { + $date = as_get_datetime_object( $record->log_date_gmt ); + } + + return new ActionScheduler_LogEntry( $record->action_id, $record->message, $date ); + } + + /** + * Retrieve the an action's log entries from the database. + * + * @param int $action_id Action ID. + * + * @return ActionScheduler_LogEntry[] + */ + public function get_logs( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $records = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_logs} WHERE action_id=%d", $action_id ) ); + + return array_map( [ $this, 'create_entry_from_db_record' ], $records ); + } + + /** + * Initialize the data store. + * + * @codeCoverageIgnore + */ + public function init() { + $table_maker = new ActionScheduler_LoggerSchema(); + $table_maker->init(); + $table_maker->register_tables(); + + parent::init(); + + add_action( 'action_scheduler_deleted_action', [ $this, 'clear_deleted_action_logs' ], 10, 1 ); + } + + /** + * Delete the action logs for an action. + * + * @param int $action_id Action ID. + */ + public function clear_deleted_action_logs( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $wpdb->delete( $wpdb->actionscheduler_logs, [ 'action_id' => $action_id, ], [ '%d' ] ); + } + + /** + * Bulk add cancel action log entries. + * + * @param array $action_ids List of action ID. + */ + public function bulk_log_cancel_actions( $action_ids ) { + if ( empty( $action_ids ) ) { + return; + } + + /** @var \wpdb $wpdb */ + global $wpdb; + $date = as_get_datetime_object(); + $date_gmt = $date->format( 'Y-m-d H:i:s' ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + $date_local = $date->format( 'Y-m-d H:i:s' ); + $message = __( 'action canceled', 'woocommerce' ); + $format = '(%d, ' . $wpdb->prepare( '%s, %s, %s', $message, $date_gmt, $date_local ) . ')'; + $sql_query = "INSERT {$wpdb->actionscheduler_logs} (action_id, message, log_date_gmt, log_date_local) VALUES "; + $value_rows = []; + + foreach ( $action_ids as $action_id ) { + $value_rows[] = $wpdb->prepare( $format, $action_id ); + } + $sql_query .= implode( ',', $value_rows ); + + $wpdb->query( $sql_query ); + } +} diff --git a/packages/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php b/packages/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php new file mode 100644 index 0000000..22871d3 --- /dev/null +++ b/packages/action-scheduler/classes/data-stores/ActionScheduler_DBStore.php @@ -0,0 +1,845 @@ +init(); + $table_maker->register_tables(); + } + + /** + * Save an action. + * + * @param ActionScheduler_Action $action Action object. + * @param DateTime $date Optional schedule date. Default null. + * + * @return int Action ID. + */ + public function save_action( ActionScheduler_Action $action, \DateTime $date = null ) { + try { + + $this->validate_action( $action ); + + /** @var \wpdb $wpdb */ + global $wpdb; + $data = [ + 'hook' => $action->get_hook(), + 'status' => ( $action->is_finished() ? self::STATUS_COMPLETE : self::STATUS_PENDING ), + 'scheduled_date_gmt' => $this->get_scheduled_date_string( $action, $date ), + 'scheduled_date_local' => $this->get_scheduled_date_string_local( $action, $date ), + 'schedule' => serialize( $action->get_schedule() ), + 'group_id' => $this->get_group_id( $action->get_group() ), + ]; + $args = wp_json_encode( $action->get_args() ); + if ( strlen( $args ) <= static::$max_index_length ) { + $data['args'] = $args; + } else { + $data['args'] = $this->hash_args( $args ); + $data['extended_args'] = $args; + } + + $table_name = ! empty( $wpdb->actionscheduler_actions ) ? $wpdb->actionscheduler_actions : $wpdb->prefix . 'actionscheduler_actions'; + $wpdb->insert( $table_name, $data ); + $action_id = $wpdb->insert_id; + + if ( is_wp_error( $action_id ) ) { + throw new RuntimeException( $action_id->get_error_message() ); + } + elseif ( empty( $action_id ) ) { + throw new RuntimeException( $wpdb->last_error ? $wpdb->last_error : __( 'Database error.', 'woocommerce' ) ); + } + + do_action( 'action_scheduler_stored_action', $action_id ); + + return $action_id; + } catch ( \Exception $e ) { + /* translators: %s: error message */ + throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'woocommerce' ), $e->getMessage() ), 0 ); + } + } + + /** + * Generate a hash from json_encoded $args using MD5 as this isn't for security. + * + * @param string $args JSON encoded action args. + * @return string + */ + protected function hash_args( $args ) { + return md5( $args ); + } + + /** + * Get action args query param value from action args. + * + * @param array $args Action args. + * @return string + */ + protected function get_args_for_query( $args ) { + $encoded = wp_json_encode( $args ); + if ( strlen( $encoded ) <= static::$max_index_length ) { + return $encoded; + } + return $this->hash_args( $encoded ); + } + /** + * Get a group's ID based on its name/slug. + * + * @param string $slug The string name of a group. + * @param bool $create_if_not_exists Whether to create the group if it does not already exist. Default, true - create the group. + * + * @return int The group's ID, if it exists or is created, or 0 if it does not exist and is not created. + */ + protected function get_group_id( $slug, $create_if_not_exists = true ) { + if ( empty( $slug ) ) { + return 0; + } + /** @var \wpdb $wpdb */ + global $wpdb; + $group_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT group_id FROM {$wpdb->actionscheduler_groups} WHERE slug=%s", $slug ) ); + if ( empty( $group_id ) && $create_if_not_exists ) { + $group_id = $this->create_group( $slug ); + } + + return $group_id; + } + + /** + * Create an action group. + * + * @param string $slug Group slug. + * + * @return int Group ID. + */ + protected function create_group( $slug ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $wpdb->insert( $wpdb->actionscheduler_groups, [ 'slug' => $slug ] ); + + return (int) $wpdb->insert_id; + } + + /** + * Retrieve an action. + * + * @param int $action_id Action ID. + * + * @return ActionScheduler_Action + */ + public function fetch_action( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $data = $wpdb->get_row( $wpdb->prepare( + "SELECT a.*, g.slug AS `group` FROM {$wpdb->actionscheduler_actions} a LEFT JOIN {$wpdb->actionscheduler_groups} g ON a.group_id=g.group_id WHERE a.action_id=%d", + $action_id + ) ); + + if ( empty( $data ) ) { + return $this->get_null_action(); + } + + if ( ! empty( $data->extended_args ) ) { + $data->args = $data->extended_args; + unset( $data->extended_args ); + } + + // Convert NULL dates to zero dates. + $date_fields = [ + 'scheduled_date_gmt', + 'scheduled_date_local', + 'last_attempt_gmt', + 'last_attempt_gmt' + ]; + foreach( $date_fields as $date_field ) { + if ( is_null( $data->$date_field ) ) { + $data->$date_field = ActionScheduler_StoreSchema::DEFAULT_DATE; + } + } + + try { + $action = $this->make_action_from_db_record( $data ); + } catch ( ActionScheduler_InvalidActionException $exception ) { + do_action( 'action_scheduler_failed_fetch_action', $action_id, $exception ); + return $this->get_null_action(); + } + + return $action; + } + + /** + * Create a null action. + * + * @return ActionScheduler_NullAction + */ + protected function get_null_action() { + return new ActionScheduler_NullAction(); + } + + /** + * Create an action from a database record. + * + * @param object $data Action database record. + * + * @return ActionScheduler_Action|ActionScheduler_CanceledAction|ActionScheduler_FinishedAction + */ + protected function make_action_from_db_record( $data ) { + + $hook = $data->hook; + $args = json_decode( $data->args, true ); + $schedule = unserialize( $data->schedule ); + + $this->validate_args( $args, $data->action_id ); + $this->validate_schedule( $schedule, $data->action_id ); + + if ( empty( $schedule ) ) { + $schedule = new ActionScheduler_NullSchedule(); + } + $group = $data->group ? $data->group : ''; + + return ActionScheduler::factory()->get_stored_action( $data->status, $data->hook, $args, $schedule, $group ); + } + + /** + * Returns the SQL statement to query (or count) actions. + * + * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * + * @param array $query Filtering options. + * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count. + * + * @return string SQL statement already properly escaped. + */ + protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) { + + if ( ! in_array( $select_or_count, array( 'select', 'count' ) ) ) { + throw new InvalidArgumentException( __( 'Invalid value for select or count parameter. Cannot query actions.', 'woocommerce' ) ); + } + + $query = wp_parse_args( $query, [ + 'hook' => '', + 'args' => null, + 'date' => null, + 'date_compare' => '<=', + 'modified' => null, + 'modified_compare' => '<=', + 'group' => '', + 'status' => '', + 'claimed' => null, + 'per_page' => 5, + 'offset' => 0, + 'orderby' => 'date', + 'order' => 'ASC', + ] ); + + /** @var \wpdb $wpdb */ + global $wpdb; + $sql = ( 'count' === $select_or_count ) ? 'SELECT count(a.action_id)' : 'SELECT a.action_id'; + $sql .= " FROM {$wpdb->actionscheduler_actions} a"; + $sql_params = []; + + if ( ! empty( $query[ 'group' ] ) || 'group' === $query[ 'orderby' ] ) { + $sql .= " LEFT JOIN {$wpdb->actionscheduler_groups} g ON g.group_id=a.group_id"; + } + + $sql .= " WHERE 1=1"; + + if ( ! empty( $query[ 'group' ] ) ) { + $sql .= " AND g.slug=%s"; + $sql_params[] = $query[ 'group' ]; + } + + if ( $query[ 'hook' ] ) { + $sql .= " AND a.hook=%s"; + $sql_params[] = $query[ 'hook' ]; + } + if ( ! is_null( $query[ 'args' ] ) ) { + $sql .= " AND a.args=%s"; + $sql_params[] = $this->get_args_for_query( $query[ 'args' ] ); + } + + if ( $query['status'] ) { + $statuses = (array) $query['status']; + $placeholders = array_fill( 0, count( $statuses ), '%s' ); + $sql .= ' AND a.status IN (' . join( ', ', $placeholders ) . ')'; + $sql_params = array_merge( $sql_params, array_values( $statuses ) ); + } + + if ( $query[ 'date' ] instanceof \DateTime ) { + $date = clone $query[ 'date' ]; + $date->setTimezone( new \DateTimeZone( 'UTC' ) ); + $date_string = $date->format( 'Y-m-d H:i:s' ); + $comparator = $this->validate_sql_comparator( $query[ 'date_compare' ] ); + $sql .= " AND a.scheduled_date_gmt $comparator %s"; + $sql_params[] = $date_string; + } + + if ( $query[ 'modified' ] instanceof \DateTime ) { + $modified = clone $query[ 'modified' ]; + $modified->setTimezone( new \DateTimeZone( 'UTC' ) ); + $date_string = $modified->format( 'Y-m-d H:i:s' ); + $comparator = $this->validate_sql_comparator( $query[ 'modified_compare' ] ); + $sql .= " AND a.last_attempt_gmt $comparator %s"; + $sql_params[] = $date_string; + } + + if ( $query[ 'claimed' ] === true ) { + $sql .= " AND a.claim_id != 0"; + } elseif ( $query[ 'claimed' ] === false ) { + $sql .= " AND a.claim_id = 0"; + } elseif ( ! is_null( $query[ 'claimed' ] ) ) { + $sql .= " AND a.claim_id = %d"; + $sql_params[] = $query[ 'claimed' ]; + } + + if ( ! empty( $query['search'] ) ) { + $sql .= " AND (a.hook LIKE %s OR (a.extended_args IS NULL AND a.args LIKE %s) OR a.extended_args LIKE %s"; + for( $i = 0; $i < 3; $i++ ) { + $sql_params[] = sprintf( '%%%s%%', $query['search'] ); + } + + $search_claim_id = (int) $query['search']; + if ( $search_claim_id ) { + $sql .= ' OR a.claim_id = %d'; + $sql_params[] = $search_claim_id; + } + + $sql .= ')'; + } + + if ( 'select' === $select_or_count ) { + if ( 'ASC' === strtoupper( $query['order'] ) ) { + $order = 'ASC'; + } else { + $order = 'DESC'; + } + switch ( $query['orderby'] ) { + case 'hook': + $sql .= " ORDER BY a.hook $order"; + break; + case 'group': + $sql .= " ORDER BY g.slug $order"; + break; + case 'modified': + $sql .= " ORDER BY a.last_attempt_gmt $order"; + break; + case 'none': + break; + case 'action_id': + $sql .= " ORDER BY a.action_id $order"; + break; + case 'date': + default: + $sql .= " ORDER BY a.scheduled_date_gmt $order"; + break; + } + + if ( $query[ 'per_page' ] > 0 ) { + $sql .= " LIMIT %d, %d"; + $sql_params[] = $query[ 'offset' ]; + $sql_params[] = $query[ 'per_page' ]; + } + } + + if ( ! empty( $sql_params ) ) { + $sql = $wpdb->prepare( $sql, $sql_params ); + } + + return $sql; + } + + /** + * Query for action count or list of action IDs. + * + * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * + * @see ActionScheduler_Store::query_actions for $query arg usage. + * + * @param array $query Query filtering options. + * @param string $query_type Whether to select or count the results. Defaults to select. + * + * @return string|array|null The IDs of actions matching the query. Null on failure. + */ + public function query_actions( $query = [], $query_type = 'select' ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $sql = $this->get_query_actions_sql( $query, $query_type ); + + return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); + } + + /** + * Get a count of all actions in the store, grouped by status. + * + * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status. + */ + public function action_counts() { + global $wpdb; + + $sql = "SELECT a.status, count(a.status) as 'count'"; + $sql .= " FROM {$wpdb->actionscheduler_actions} a"; + $sql .= " GROUP BY a.status"; + + $actions_count_by_status = array(); + $action_stati_and_labels = $this->get_status_labels(); + + foreach ( $wpdb->get_results( $sql ) as $action_data ) { + // Ignore any actions with invalid status + if ( array_key_exists( $action_data->status, $action_stati_and_labels ) ) { + $actions_count_by_status[ $action_data->status ] = $action_data->count; + } + } + + return $actions_count_by_status; + } + + /** + * Cancel an action. + * + * @param int $action_id Action ID. + * + * @return void + */ + public function cancel_action( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $updated = $wpdb->update( + $wpdb->actionscheduler_actions, + [ 'status' => self::STATUS_CANCELED ], + [ 'action_id' => $action_id ], + [ '%s' ], + [ '%d' ] + ); + if ( empty( $updated ) ) { + /* translators: %s: action ID */ + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + do_action( 'action_scheduler_canceled_action', $action_id ); + } + + /** + * Cancel pending actions by hook. + * + * @since 3.0.0 + * + * @param string $hook Hook name. + * + * @return void + */ + public function cancel_actions_by_hook( $hook ) { + $this->bulk_cancel_actions( [ 'hook' => $hook ] ); + } + + /** + * Cancel pending actions by group. + * + * @param string $group Group slug. + * + * @return void + */ + public function cancel_actions_by_group( $group ) { + $this->bulk_cancel_actions( [ 'group' => $group ] ); + } + + /** + * Bulk cancel actions. + * + * @since 3.0.0 + * + * @param array $query_args Query parameters. + */ + protected function bulk_cancel_actions( $query_args ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + if ( ! is_array( $query_args ) ) { + return; + } + + // Don't cancel actions that are already canceled. + if ( isset( $query_args['status'] ) && $query_args['status'] == self::STATUS_CANCELED ) { + return; + } + + $action_ids = true; + $query_args = wp_parse_args( + $query_args, + [ + 'per_page' => 1000, + 'status' => self::STATUS_PENDING, + 'orderby' => 'action_id', + ] + ); + + while ( $action_ids ) { + $action_ids = $this->query_actions( $query_args ); + if ( empty( $action_ids ) ) { + break; + } + + $format = array_fill( 0, count( $action_ids ), '%d' ); + $query_in = '(' . implode( ',', $format ) . ')'; + $parameters = $action_ids; + array_unshift( $parameters, self::STATUS_CANCELED ); + + $wpdb->query( + $wpdb->prepare( // wpcs: PreparedSQLPlaceholders replacement count ok. + "UPDATE {$wpdb->actionscheduler_actions} SET status = %s WHERE action_id IN {$query_in}", + $parameters + ) + ); + + do_action( 'action_scheduler_bulk_cancel_actions', $action_ids ); + } + } + + /** + * Delete an action. + * + * @param int $action_id Action ID. + */ + public function delete_action( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $deleted = $wpdb->delete( $wpdb->actionscheduler_actions, [ 'action_id' => $action_id ], [ '%d' ] ); + if ( empty( $deleted ) ) { + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + do_action( 'action_scheduler_deleted_action', $action_id ); + } + + /** + * Get the schedule date for an action. + * + * @param string $action_id Action ID. + * + * @throws \InvalidArgumentException + * @return \DateTime The local date the action is scheduled to run, or the date that it ran. + */ + public function get_date( $action_id ) { + $date = $this->get_date_gmt( $action_id ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + return $date; + } + + /** + * Get the GMT schedule date for an action. + * + * @param int $action_id Action ID. + * + * @throws \InvalidArgumentException + * @return \DateTime The GMT date the action is scheduled to run, or the date that it ran. + */ + protected function get_date_gmt( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $record = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d", $action_id ) ); + if ( empty( $record ) ) { + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + if ( $record->status == self::STATUS_PENDING ) { + return as_get_datetime_object( $record->scheduled_date_gmt ); + } else { + return as_get_datetime_object( $record->last_attempt_gmt ); + } + } + + /** + * Stake a claim on actions. + * + * @param int $max_actions Maximum number of action to include in claim. + * @param \DateTime $before_date Jobs must be schedule before this date. Defaults to now. + * + * @return ActionScheduler_ActionClaim + */ + public function stake_claim( $max_actions = 10, \DateTime $before_date = null, $hooks = array(), $group = '' ) { + $claim_id = $this->generate_claim_id(); + + $this->claim_before_date = $before_date; + $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group ); + $action_ids = $this->find_actions_by_claim_id( $claim_id ); + $this->claim_before_date = null; + + return new ActionScheduler_ActionClaim( $claim_id, $action_ids ); + } + + /** + * Generate a new action claim. + * + * @return int Claim ID. + */ + protected function generate_claim_id() { + /** @var \wpdb $wpdb */ + global $wpdb; + $now = as_get_datetime_object(); + $wpdb->insert( $wpdb->actionscheduler_claims, [ 'date_created_gmt' => $now->format( 'Y-m-d H:i:s' ) ] ); + + return $wpdb->insert_id; + } + + /** + * Mark actions claimed. + * + * @param string $claim_id Claim Id. + * @param int $limit Number of action to include in claim. + * @param \DateTime $before_date Should use UTC timezone. + * + * @return int The number of actions that were claimed. + * @throws \RuntimeException + */ + protected function claim_actions( $claim_id, $limit, \DateTime $before_date = null, $hooks = array(), $group = '' ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $now = as_get_datetime_object(); + $date = is_null( $before_date ) ? $now : clone $before_date; + + // can't use $wpdb->update() because of the <= condition + $update = "UPDATE {$wpdb->actionscheduler_actions} SET claim_id=%d, last_attempt_gmt=%s, last_attempt_local=%s"; + $params = array( + $claim_id, + $now->format( 'Y-m-d H:i:s' ), + current_time( 'mysql' ), + ); + + $where = "WHERE claim_id = 0 AND scheduled_date_gmt <= %s AND status=%s"; + $params[] = $date->format( 'Y-m-d H:i:s' ); + $params[] = self::STATUS_PENDING; + + if ( ! empty( $hooks ) ) { + $placeholders = array_fill( 0, count( $hooks ), '%s' ); + $where .= ' AND hook IN (' . join( ', ', $placeholders ) . ')'; + $params = array_merge( $params, array_values( $hooks ) ); + } + + if ( ! empty( $group ) ) { + + $group_id = $this->get_group_id( $group, false ); + + // throw exception if no matching group found, this matches ActionScheduler_wpPostStore's behaviour + if ( empty( $group_id ) ) { + /* translators: %s: group name */ + throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'woocommerce' ), $group ) ); + } + + $where .= ' AND group_id = %d'; + $params[] = $group_id; + } + + $order = "ORDER BY attempts ASC, scheduled_date_gmt ASC, action_id ASC LIMIT %d"; + $params[] = $limit; + + $sql = $wpdb->prepare( "{$update} {$where} {$order}", $params ); + + $rows_affected = $wpdb->query( $sql ); + if ( $rows_affected === false ) { + throw new \RuntimeException( __( 'Unable to claim actions. Database error.', 'woocommerce' ) ); + } + + return (int) $rows_affected; + } + + /** + * Get the number of active claims. + * + * @return int + */ + public function get_claim_count() { + global $wpdb; + + $sql = "SELECT COUNT(DISTINCT claim_id) FROM {$wpdb->actionscheduler_actions} WHERE claim_id != 0 AND status IN ( %s, %s)"; + $sql = $wpdb->prepare( $sql, [ self::STATUS_PENDING, self::STATUS_RUNNING ] ); + + return (int) $wpdb->get_var( $sql ); + } + + /** + * Return an action's claim ID, as stored in the claim_id column. + * + * @param string $action_id Action ID. + * @return mixed + */ + public function get_claim_id( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $sql = "SELECT claim_id FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; + $sql = $wpdb->prepare( $sql, $action_id ); + + return (int) $wpdb->get_var( $sql ); + } + + /** + * Retrieve the action IDs of action in a claim. + * + * @return int[] + */ + public function find_actions_by_claim_id( $claim_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $action_ids = array(); + $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object(); + $cut_off = $before_date->format( 'Y-m-d H:i:s' ); + + $sql = $wpdb->prepare( + "SELECT action_id, scheduled_date_gmt FROM {$wpdb->actionscheduler_actions} WHERE claim_id = %d", + $claim_id + ); + + // Verify that the scheduled date for each action is within the expected bounds (in some unusual + // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify). + foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { + if ( $claimed_action->scheduled_date_gmt <= $cut_off ) { + $action_ids[] = absint( $claimed_action->action_id ); + } + } + + return $action_ids; + } + + /** + * Release actions from a claim and delete the claim. + * + * @param ActionScheduler_ActionClaim $claim Claim object. + */ + public function release_claim( ActionScheduler_ActionClaim $claim ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $wpdb->update( $wpdb->actionscheduler_actions, [ 'claim_id' => 0 ], [ 'claim_id' => $claim->get_id() ], [ '%d' ], [ '%d' ] ); + $wpdb->delete( $wpdb->actionscheduler_claims, [ 'claim_id' => $claim->get_id() ], [ '%d' ] ); + } + + /** + * Remove the claim from an action. + * + * @param int $action_id Action ID. + * + * @return void + */ + public function unclaim_action( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $wpdb->update( + $wpdb->actionscheduler_actions, + [ 'claim_id' => 0 ], + [ 'action_id' => $action_id ], + [ '%s' ], + [ '%d' ] + ); + } + + /** + * Mark an action as failed. + * + * @param int $action_id Action ID. + */ + public function mark_failure( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $updated = $wpdb->update( + $wpdb->actionscheduler_actions, + [ 'status' => self::STATUS_FAILED ], + [ 'action_id' => $action_id ], + [ '%s' ], + [ '%d' ] + ); + if ( empty( $updated ) ) { + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + } + + /** + * Add execution message to action log. + * + * @param int $action_id Action ID. + * + * @return void + */ + public function log_execution( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + + $sql = "UPDATE {$wpdb->actionscheduler_actions} SET attempts = attempts+1, status=%s, last_attempt_gmt = %s, last_attempt_local = %s WHERE action_id = %d"; + $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time( 'mysql', true ), current_time( 'mysql' ), $action_id ); + $wpdb->query( $sql ); + } + + /** + * Mark an action as complete. + * + * @param int $action_id Action ID. + * + * @return void + */ + public function mark_complete( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $updated = $wpdb->update( + $wpdb->actionscheduler_actions, + [ + 'status' => self::STATUS_COMPLETE, + 'last_attempt_gmt' => current_time( 'mysql', true ), + 'last_attempt_local' => current_time( 'mysql' ), + ], + [ 'action_id' => $action_id ], + [ '%s' ], + [ '%d' ] + ); + if ( empty( $updated ) ) { + throw new \InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + } + + /** + * Get an action's status. + * + * @param int $action_id Action ID. + * + * @return string + */ + public function get_status( $action_id ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $sql = "SELECT status FROM {$wpdb->actionscheduler_actions} WHERE action_id=%d"; + $sql = $wpdb->prepare( $sql, $action_id ); + $status = $wpdb->get_var( $sql ); + + if ( $status === null ) { + throw new \InvalidArgumentException( __( 'Invalid action ID. No status found.', 'woocommerce' ) ); + } elseif ( empty( $status ) ) { + throw new \RuntimeException( __( 'Unknown status found for action.', 'woocommerce' ) ); + } else { + return $status; + } + } +} diff --git a/packages/action-scheduler/classes/data-stores/ActionScheduler_HybridStore.php b/packages/action-scheduler/classes/data-stores/ActionScheduler_HybridStore.php new file mode 100644 index 0000000..22d61a6 --- /dev/null +++ b/packages/action-scheduler/classes/data-stores/ActionScheduler_HybridStore.php @@ -0,0 +1,426 @@ +demarkation_id = (int) get_option( self::DEMARKATION_OPTION, 0 ); + if ( empty( $config ) ) { + $config = Controller::instance()->get_migration_config_object(); + } + $this->primary_store = $config->get_destination_store(); + $this->secondary_store = $config->get_source_store(); + $this->migration_runner = new Runner( $config ); + } + + /** + * Initialize the table data store tables. + * + * @codeCoverageIgnore + */ + public function init() { + add_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10, 2 ); + $this->primary_store->init(); + $this->secondary_store->init(); + remove_action( 'action_scheduler/created_table', [ $this, 'set_autoincrement' ], 10 ); + } + + /** + * When the actions table is created, set its autoincrement + * value to be one higher than the posts table to ensure that + * there are no ID collisions. + * + * @param string $table_name + * @param string $table_suffix + * + * @return void + * @codeCoverageIgnore + */ + public function set_autoincrement( $table_name, $table_suffix ) { + if ( ActionScheduler_StoreSchema::ACTIONS_TABLE === $table_suffix ) { + if ( empty( $this->demarkation_id ) ) { + $this->demarkation_id = $this->set_demarkation_id(); + } + /** @var \wpdb $wpdb */ + global $wpdb; + /** + * A default date of '0000-00-00 00:00:00' is invalid in MySQL 5.7 when configured with + * sql_mode including both STRICT_TRANS_TABLES and NO_ZERO_DATE. + */ + $default_date = new DateTime( 'tomorrow' ); + $null_action = new ActionScheduler_NullAction(); + $date_gmt = $this->get_scheduled_date_string( $null_action, $default_date ); + $date_local = $this->get_scheduled_date_string_local( $null_action, $default_date ); + + $row_count = $wpdb->insert( + $wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE}, + [ + 'action_id' => $this->demarkation_id, + 'hook' => '', + 'status' => '', + 'scheduled_date_gmt' => $date_gmt, + 'scheduled_date_local' => $date_local, + 'last_attempt_gmt' => $date_gmt, + 'last_attempt_local' => $date_local, + ] + ); + if ( $row_count > 0 ) { + $wpdb->delete( + $wpdb->{ActionScheduler_StoreSchema::ACTIONS_TABLE}, + [ 'action_id' => $this->demarkation_id ] + ); + } + } + } + + /** + * Store the demarkation id in WP options. + * + * @param int $id The ID to set as the demarkation point between the two stores + * Leave null to use the next ID from the WP posts table. + * + * @return int The new ID. + * + * @codeCoverageIgnore + */ + private function set_demarkation_id( $id = null ) { + if ( empty( $id ) ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $id = (int) $wpdb->get_var( "SELECT MAX(ID) FROM $wpdb->posts" ); + $id ++; + } + update_option( self::DEMARKATION_OPTION, $id ); + + return $id; + } + + /** + * Find the first matching action from the secondary store. + * If it exists, migrate it to the primary store immediately. + * After it migrates, the secondary store will logically contain + * the next matching action, so return the result thence. + * + * @param string $hook + * @param array $params + * + * @return string + */ + public function find_action( $hook, $params = [] ) { + $found_unmigrated_action = $this->secondary_store->find_action( $hook, $params ); + if ( ! empty( $found_unmigrated_action ) ) { + $this->migrate( [ $found_unmigrated_action ] ); + } + + return $this->primary_store->find_action( $hook, $params ); + } + + /** + * Find actions matching the query in the secondary source first. + * If any are found, migrate them immediately. Then the secondary + * store will contain the canonical results. + * + * @param array $query + * @param string $query_type Whether to select or count the results. Default, select. + * + * @return int[] + */ + public function query_actions( $query = [], $query_type = 'select' ) { + $found_unmigrated_actions = $this->secondary_store->query_actions( $query, 'select' ); + if ( ! empty( $found_unmigrated_actions ) ) { + $this->migrate( $found_unmigrated_actions ); + } + + return $this->primary_store->query_actions( $query, $query_type ); + } + + /** + * Get a count of all actions in the store, grouped by status + * + * @return array Set of 'status' => int $count pairs for statuses with 1 or more actions of that status. + */ + public function action_counts() { + $unmigrated_actions_count = $this->secondary_store->action_counts(); + $migrated_actions_count = $this->primary_store->action_counts(); + $actions_count_by_status = array(); + + foreach ( $this->get_status_labels() as $status_key => $status_label ) { + + $count = 0; + + if ( isset( $unmigrated_actions_count[ $status_key ] ) ) { + $count += $unmigrated_actions_count[ $status_key ]; + } + + if ( isset( $migrated_actions_count[ $status_key ] ) ) { + $count += $migrated_actions_count[ $status_key ]; + } + + $actions_count_by_status[ $status_key ] = $count; + } + + $actions_count_by_status = array_filter( $actions_count_by_status ); + + return $actions_count_by_status; + } + + /** + * If any actions would have been claimed by the secondary store, + * migrate them immediately, then ask the primary store for the + * canonical claim. + * + * @param int $max_actions + * @param DateTime|null $before_date + * + * @return ActionScheduler_ActionClaim + */ + public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) { + $claim = $this->secondary_store->stake_claim( $max_actions, $before_date, $hooks, $group ); + + $claimed_actions = $claim->get_actions(); + if ( ! empty( $claimed_actions ) ) { + $this->migrate( $claimed_actions ); + } + + $this->secondary_store->release_claim( $claim ); + + return $this->primary_store->stake_claim( $max_actions, $before_date, $hooks, $group ); + } + + /** + * Migrate a list of actions to the table data store. + * + * @param array $action_ids List of action IDs. + */ + private function migrate( $action_ids ) { + $this->migration_runner->migrate_actions( $action_ids ); + } + + /** + * Save an action to the primary store. + * + * @param ActionScheduler_Action $action Action object to be saved. + * @param DateTime $date Optional. Schedule date. Default null. + * + * @return int The action ID + */ + public function save_action( ActionScheduler_Action $action, DateTime $date = null ) { + return $this->primary_store->save_action( $action, $date ); + } + + /** + * Retrieve an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function fetch_action( $action_id ) { + $store = $this->get_store_from_action_id( $action_id, true ); + if ( $store ) { + return $store->fetch_action( $action_id ); + } else { + return new ActionScheduler_NullAction(); + } + } + + /** + * Cancel an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function cancel_action( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->cancel_action( $action_id ); + } + } + + /** + * Delete an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function delete_action( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->delete_action( $action_id ); + } + } + + /** + * Get the schedule date an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function get_date( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + return $store->get_date( $action_id ); + } else { + return null; + } + } + + /** + * Mark an existing action as failed whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function mark_failure( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->mark_failure( $action_id ); + } + } + + /** + * Log the execution of an existing action whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function log_execution( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->log_execution( $action_id ); + } + } + + /** + * Mark an existing action complete whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function mark_complete( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + $store->mark_complete( $action_id ); + } + } + + /** + * Get an existing action status whether migrated or not. + * + * @param int $action_id Action ID. + */ + public function get_status( $action_id ) { + $store = $this->get_store_from_action_id( $action_id ); + if ( $store ) { + return $store->get_status( $action_id ); + } + return null; + } + + /** + * Return which store an action is stored in. + * + * @param int $action_id ID of the action. + * @param bool $primary_first Optional flag indicating search the primary store first. + * @return ActionScheduler_Store + */ + protected function get_store_from_action_id( $action_id, $primary_first = false ) { + if ( $primary_first ) { + $stores = [ + $this->primary_store, + $this->secondary_store, + ]; + } elseif ( $action_id < $this->demarkation_id ) { + $stores = [ + $this->secondary_store, + $this->primary_store, + ]; + } else { + $stores = [ + $this->primary_store, + ]; + } + + foreach ( $stores as $store ) { + $action = $store->fetch_action( $action_id ); + if ( ! is_a( $action, 'ActionScheduler_NullAction' ) ) { + return $store; + } + } + return null; + } + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * + * All claim-related functions should operate solely + * on the primary store. + * * * * * * * * * * * * * * * * * * * * * * * * * * */ + + /** + * Get the claim count from the table data store. + */ + public function get_claim_count() { + return $this->primary_store->get_claim_count(); + } + + /** + * Retrieve the claim ID for an action from the table data store. + * + * @param int $action_id Action ID. + */ + public function get_claim_id( $action_id ) { + return $this->primary_store->get_claim_id( $action_id ); + } + + /** + * Release a claim in the table data store. + * + * @param ActionScheduler_ActionClaim $claim Claim object. + */ + public function release_claim( ActionScheduler_ActionClaim $claim ) { + $this->primary_store->release_claim( $claim ); + } + + /** + * Release claims on an action in the table data store. + * + * @param int $action_id Action ID. + */ + public function unclaim_action( $action_id ) { + $this->primary_store->unclaim_action( $action_id ); + } + + /** + * Retrieve a list of action IDs by claim. + * + * @param int $claim_id Claim ID. + */ + public function find_actions_by_claim_id( $claim_id ) { + return $this->primary_store->find_actions_by_claim_id( $claim_id ); + } +} diff --git a/packages/action-scheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php new file mode 100644 index 0000000..7215ddd --- /dev/null +++ b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpCommentLogger.php @@ -0,0 +1,240 @@ +create_wp_comment( $action_id, $message, $date ); + return $comment_id; + } + + protected function create_wp_comment( $action_id, $message, DateTime $date ) { + + $comment_date_gmt = $date->format('Y-m-d H:i:s'); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + $comment_data = array( + 'comment_post_ID' => $action_id, + 'comment_date' => $date->format('Y-m-d H:i:s'), + 'comment_date_gmt' => $comment_date_gmt, + 'comment_author' => self::AGENT, + 'comment_content' => $message, + 'comment_agent' => self::AGENT, + 'comment_type' => self::TYPE, + ); + return wp_insert_comment($comment_data); + } + + /** + * @param string $entry_id + * + * @return ActionScheduler_LogEntry + */ + public function get_entry( $entry_id ) { + $comment = $this->get_comment( $entry_id ); + if ( empty($comment) || $comment->comment_type != self::TYPE ) { + return new ActionScheduler_NullLogEntry(); + } + + $date = as_get_datetime_object( $comment->comment_date_gmt ); + ActionScheduler_TimezoneHelper::set_local_timezone( $date ); + return new ActionScheduler_LogEntry( $comment->comment_post_ID, $comment->comment_content, $date ); + } + + /** + * @param string $action_id + * + * @return ActionScheduler_LogEntry[] + */ + public function get_logs( $action_id ) { + $status = 'all'; + if ( get_post_status($action_id) == 'trash' ) { + $status = 'post-trashed'; + } + $comments = get_comments(array( + 'post_id' => $action_id, + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + 'type' => self::TYPE, + 'status' => $status, + )); + $logs = array(); + foreach ( $comments as $c ) { + $entry = $this->get_entry( $c ); + if ( !empty($entry) ) { + $logs[] = $entry; + } + } + return $logs; + } + + protected function get_comment( $comment_id ) { + return get_comment( $comment_id ); + } + + + + /** + * @param WP_Comment_Query $query + */ + public function filter_comment_queries( $query ) { + foreach ( array('ID', 'parent', 'post_author', 'post_name', 'post_parent', 'type', 'post_type', 'post_id', 'post_ID') as $key ) { + if ( !empty($query->query_vars[$key]) ) { + return; // don't slow down queries that wouldn't include action_log comments anyway + } + } + $query->query_vars['action_log_filter'] = TRUE; + add_filter( 'comments_clauses', array( $this, 'filter_comment_query_clauses' ), 10, 2 ); + } + + /** + * @param array $clauses + * @param WP_Comment_Query $query + * + * @return array + */ + public function filter_comment_query_clauses( $clauses, $query ) { + if ( !empty($query->query_vars['action_log_filter']) ) { + $clauses['where'] .= $this->get_where_clause(); + } + return $clauses; + } + + /** + * Make sure Action Scheduler logs are excluded from comment feeds, which use WP_Query, not + * the WP_Comment_Query class handled by @see self::filter_comment_queries(). + * + * @param string $where + * @param WP_Query $query + * + * @return string + */ + public function filter_comment_feed( $where, $query ) { + if ( is_comment_feed() ) { + $where .= $this->get_where_clause(); + } + return $where; + } + + /** + * Return a SQL clause to exclude Action Scheduler comments. + * + * @return string + */ + protected function get_where_clause() { + global $wpdb; + return sprintf( " AND {$wpdb->comments}.comment_type != '%s'", self::TYPE ); + } + + /** + * Remove action log entries from wp_count_comments() + * + * @param array $stats + * @param int $post_id + * + * @return object + */ + public function filter_comment_count( $stats, $post_id ) { + global $wpdb; + + if ( 0 === $post_id ) { + $stats = $this->get_comment_count(); + } + + return $stats; + } + + /** + * Retrieve the comment counts from our cache, or the database if the cached version isn't set. + * + * @return object + */ + protected function get_comment_count() { + global $wpdb; + + $stats = get_transient( 'as_comment_count' ); + + if ( ! $stats ) { + $stats = array(); + + $count = $wpdb->get_results( "SELECT comment_approved, COUNT( * ) AS num_comments FROM {$wpdb->comments} WHERE comment_type NOT IN('order_note','action_log') GROUP BY comment_approved", ARRAY_A ); + + $total = 0; + $stats = array(); + $approved = array( '0' => 'moderated', '1' => 'approved', 'spam' => 'spam', 'trash' => 'trash', 'post-trashed' => 'post-trashed' ); + + foreach ( (array) $count as $row ) { + // Don't count post-trashed toward totals + if ( 'post-trashed' != $row['comment_approved'] && 'trash' != $row['comment_approved'] ) { + $total += $row['num_comments']; + } + if ( isset( $approved[ $row['comment_approved'] ] ) ) { + $stats[ $approved[ $row['comment_approved'] ] ] = $row['num_comments']; + } + } + + $stats['total_comments'] = $total; + $stats['all'] = $total; + + foreach ( $approved as $key ) { + if ( empty( $stats[ $key ] ) ) { + $stats[ $key ] = 0; + } + } + + $stats = (object) $stats; + set_transient( 'as_comment_count', $stats ); + } + + return $stats; + } + + /** + * Delete comment count cache whenever there is new comment or the status of a comment changes. Cache + * will be regenerated next time ActionScheduler_wpCommentLogger::filter_comment_count() is called. + */ + public function delete_comment_count_cache() { + delete_transient( 'as_comment_count' ); + } + + /** + * @codeCoverageIgnore + */ + public function init() { + add_action( 'action_scheduler_before_process_queue', array( $this, 'disable_comment_counting' ), 10, 0 ); + add_action( 'action_scheduler_after_process_queue', array( $this, 'enable_comment_counting' ), 10, 0 ); + + parent::init(); + + add_action( 'pre_get_comments', array( $this, 'filter_comment_queries' ), 10, 1 ); + add_action( 'wp_count_comments', array( $this, 'filter_comment_count' ), 20, 2 ); // run after WC_Comments::wp_count_comments() to make sure we exclude order notes and action logs + add_action( 'comment_feed_where', array( $this, 'filter_comment_feed' ), 10, 2 ); + + // Delete comments count cache whenever there is a new comment or a comment status changes + add_action( 'wp_insert_comment', array( $this, 'delete_comment_count_cache' ) ); + add_action( 'wp_set_comment_status', array( $this, 'delete_comment_count_cache' ) ); + } + + public function disable_comment_counting() { + wp_defer_comment_counting(true); + } + public function enable_comment_counting() { + wp_defer_comment_counting(false); + } + +} diff --git a/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php new file mode 100644 index 0000000..58e8475 --- /dev/null +++ b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore.php @@ -0,0 +1,839 @@ +validate_action( $action ); + $post_array = $this->create_post_array( $action, $scheduled_date ); + $post_id = $this->save_post_array( $post_array ); + $this->save_post_schedule( $post_id, $action->get_schedule() ); + $this->save_action_group( $post_id, $action->get_group() ); + do_action( 'action_scheduler_stored_action', $post_id ); + return $post_id; + } catch ( Exception $e ) { + throw new RuntimeException( sprintf( __( 'Error saving action: %s', 'woocommerce' ), $e->getMessage() ), 0 ); + } + } + + protected function create_post_array( ActionScheduler_Action $action, DateTime $scheduled_date = NULL ) { + $post = array( + 'post_type' => self::POST_TYPE, + 'post_title' => $action->get_hook(), + 'post_content' => json_encode($action->get_args()), + 'post_status' => ( $action->is_finished() ? 'publish' : 'pending' ), + 'post_date_gmt' => $this->get_scheduled_date_string( $action, $scheduled_date ), + 'post_date' => $this->get_scheduled_date_string_local( $action, $scheduled_date ), + ); + return $post; + } + + protected function save_post_array( $post_array ) { + add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); + + $has_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + kses_remove_filters(); + } + + $post_id = wp_insert_post($post_array); + + if ( $has_kses ) { + kses_init_filters(); + } + + remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); + remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); + + if ( is_wp_error($post_id) || empty($post_id) ) { + throw new RuntimeException( __( 'Unable to save action.', 'woocommerce' ) ); + } + return $post_id; + } + + public function filter_insert_post_data( $postdata ) { + if ( $postdata['post_type'] == self::POST_TYPE ) { + $postdata['post_author'] = 0; + if ( $postdata['post_status'] == 'future' ) { + $postdata['post_status'] = 'publish'; + } + } + return $postdata; + } + + /** + * Create a (probably unique) post name for scheduled actions in a more performant manner than wp_unique_post_slug(). + * + * When an action's post status is transitioned to something other than 'draft', 'pending' or 'auto-draft, like 'publish' + * or 'failed' or 'trash', WordPress will find a unique slug (stored in post_name column) using the wp_unique_post_slug() + * function. This is done to ensure URL uniqueness. The approach taken by wp_unique_post_slug() is to iterate over existing + * post_name values that match, and append a number 1 greater than the largest. This makes sense when manually creating a + * post from the Edit Post screen. It becomes a bottleneck when automatically processing thousands of actions, with a + * database containing thousands of related post_name values. + * + * WordPress 5.1 introduces the 'pre_wp_unique_post_slug' filter for plugins to address this issue. + * + * We can short-circuit WordPress's wp_unique_post_slug() approach using the 'pre_wp_unique_post_slug' filter. This + * method is available to be used as a callback on that filter. It provides a more scalable approach to generating a + * post_name/slug that is probably unique. Because Action Scheduler never actually uses the post_name field, or an + * action's slug, being probably unique is good enough. + * + * For more backstory on this issue, see: + * - https://github.com/woocommerce/action-scheduler/issues/44 and + * - https://core.trac.wordpress.org/ticket/21112 + * + * @param string $override_slug Short-circuit return value. + * @param string $slug The desired slug (post_name). + * @param int $post_ID Post ID. + * @param string $post_status The post status. + * @param string $post_type Post type. + * @return string + */ + public function set_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) { + if ( self::POST_TYPE == $post_type ) { + $override_slug = uniqid( self::POST_TYPE . '-', true ) . '-' . wp_generate_password( 32, false ); + } + return $override_slug; + } + + protected function save_post_schedule( $post_id, $schedule ) { + update_post_meta( $post_id, self::SCHEDULE_META_KEY, $schedule ); + } + + protected function save_action_group( $post_id, $group ) { + if ( empty($group) ) { + wp_set_object_terms( $post_id, array(), self::GROUP_TAXONOMY, FALSE ); + } else { + wp_set_object_terms( $post_id, array($group), self::GROUP_TAXONOMY, FALSE ); + } + } + + public function fetch_action( $action_id ) { + $post = $this->get_post( $action_id ); + if ( empty($post) || $post->post_type != self::POST_TYPE ) { + return $this->get_null_action(); + } + + try { + $action = $this->make_action_from_post( $post ); + } catch ( ActionScheduler_InvalidActionException $exception ) { + do_action( 'action_scheduler_failed_fetch_action', $post->ID, $exception ); + return $this->get_null_action(); + } + + return $action; + } + + protected function get_post( $action_id ) { + if ( empty($action_id) ) { + return NULL; + } + return get_post($action_id); + } + + protected function get_null_action() { + return new ActionScheduler_NullAction(); + } + + protected function make_action_from_post( $post ) { + $hook = $post->post_title; + + $args = json_decode( $post->post_content, true ); + $this->validate_args( $args, $post->ID ); + + $schedule = get_post_meta( $post->ID, self::SCHEDULE_META_KEY, true ); + $this->validate_schedule( $schedule, $post->ID ); + + $group = wp_get_object_terms( $post->ID, self::GROUP_TAXONOMY, array('fields' => 'names') ); + $group = empty( $group ) ? '' : reset($group); + + return ActionScheduler::factory()->get_stored_action( $this->get_action_status_by_post_status( $post->post_status ), $hook, $args, $schedule, $group ); + } + + /** + * @param string $post_status + * + * @throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels() + * @return string + */ + protected function get_action_status_by_post_status( $post_status ) { + + switch ( $post_status ) { + case 'publish' : + $action_status = self::STATUS_COMPLETE; + break; + case 'trash' : + $action_status = self::STATUS_CANCELED; + break; + default : + if ( ! array_key_exists( $post_status, $this->get_status_labels() ) ) { + throw new InvalidArgumentException( sprintf( 'Invalid post status: "%s". No matching action status available.', $post_status ) ); + } + $action_status = $post_status; + break; + } + + return $action_status; + } + + /** + * @param string $action_status + * @throws InvalidArgumentException if $post_status not in known status fields returned by $this->get_status_labels() + * @return string + */ + protected function get_post_status_by_action_status( $action_status ) { + + switch ( $action_status ) { + case self::STATUS_COMPLETE : + $post_status = 'publish'; + break; + case self::STATUS_CANCELED : + $post_status = 'trash'; + break; + default : + if ( ! array_key_exists( $action_status, $this->get_status_labels() ) ) { + throw new InvalidArgumentException( sprintf( 'Invalid action status: "%s".', $action_status ) ); + } + $post_status = $action_status; + break; + } + + return $post_status; + } + + /** + * Returns the SQL statement to query (or count) actions. + * + * @param array $query Filtering options + * @param string $select_or_count Whether the SQL should select and return the IDs or just the row count + * @throws InvalidArgumentException if $select_or_count not count or select + * @return string SQL statement. The returned SQL is already properly escaped. + */ + protected function get_query_actions_sql( array $query, $select_or_count = 'select' ) { + + if ( ! in_array( $select_or_count, array( 'select', 'count' ) ) ) { + throw new InvalidArgumentException( __( 'Invalid schedule. Cannot save action.', 'woocommerce' ) ); + } + + $query = wp_parse_args( $query, array( + 'hook' => '', + 'args' => NULL, + 'date' => NULL, + 'date_compare' => '<=', + 'modified' => NULL, + 'modified_compare' => '<=', + 'group' => '', + 'status' => '', + 'claimed' => NULL, + 'per_page' => 5, + 'offset' => 0, + 'orderby' => 'date', + 'order' => 'ASC', + 'search' => '', + ) ); + + /** @var wpdb $wpdb */ + global $wpdb; + $sql = ( 'count' === $select_or_count ) ? 'SELECT count(p.ID)' : 'SELECT p.ID '; + $sql .= "FROM {$wpdb->posts} p"; + $sql_params = array(); + if ( empty( $query['group'] ) && 'group' === $query['orderby'] ) { + $sql .= " LEFT JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID"; + $sql .= " LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id"; + $sql .= " LEFT JOIN {$wpdb->terms} t ON tt.term_id=t.term_id"; + } elseif ( ! empty( $query['group'] ) ) { + $sql .= " INNER JOIN {$wpdb->term_relationships} tr ON tr.object_id=p.ID"; + $sql .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id=tt.term_taxonomy_id"; + $sql .= " INNER JOIN {$wpdb->terms} t ON tt.term_id=t.term_id"; + $sql .= " AND t.slug=%s"; + $sql_params[] = $query['group']; + } + $sql .= " WHERE post_type=%s"; + $sql_params[] = self::POST_TYPE; + if ( $query['hook'] ) { + $sql .= " AND p.post_title=%s"; + $sql_params[] = $query['hook']; + } + if ( !is_null($query['args']) ) { + $sql .= " AND p.post_content=%s"; + $sql_params[] = json_encode($query['args']); + } + + if ( $query['status'] ) { + $post_statuses = array_map( array( $this, 'get_post_status_by_action_status' ), (array) $query['status'] ); + $placeholders = array_fill( 0, count( $post_statuses ), '%s' ); + $sql .= ' AND p.post_status IN (' . join( ', ', $placeholders ) . ')'; + $sql_params = array_merge( $sql_params, array_values( $post_statuses ) ); + } + + if ( $query['date'] instanceof DateTime ) { + $date = clone $query['date']; + $date->setTimezone( new DateTimeZone('UTC') ); + $date_string = $date->format('Y-m-d H:i:s'); + $comparator = $this->validate_sql_comparator($query['date_compare']); + $sql .= " AND p.post_date_gmt $comparator %s"; + $sql_params[] = $date_string; + } + + if ( $query['modified'] instanceof DateTime ) { + $modified = clone $query['modified']; + $modified->setTimezone( new DateTimeZone('UTC') ); + $date_string = $modified->format('Y-m-d H:i:s'); + $comparator = $this->validate_sql_comparator($query['modified_compare']); + $sql .= " AND p.post_modified_gmt $comparator %s"; + $sql_params[] = $date_string; + } + + if ( $query['claimed'] === TRUE ) { + $sql .= " AND p.post_password != ''"; + } elseif ( $query['claimed'] === FALSE ) { + $sql .= " AND p.post_password = ''"; + } elseif ( !is_null($query['claimed']) ) { + $sql .= " AND p.post_password = %s"; + $sql_params[] = $query['claimed']; + } + + if ( ! empty( $query['search'] ) ) { + $sql .= " AND (p.post_title LIKE %s OR p.post_content LIKE %s OR p.post_password LIKE %s)"; + for( $i = 0; $i < 3; $i++ ) { + $sql_params[] = sprintf( '%%%s%%', $query['search'] ); + } + } + + if ( 'select' === $select_or_count ) { + switch ( $query['orderby'] ) { + case 'hook': + $orderby = 'p.post_title'; + break; + case 'group': + $orderby = 't.name'; + break; + case 'status': + $orderby = 'p.post_status'; + break; + case 'modified': + $orderby = 'p.post_modified'; + break; + case 'claim_id': + $orderby = 'p.post_password'; + break; + case 'schedule': + case 'date': + default: + $orderby = 'p.post_date_gmt'; + break; + } + if ( 'ASC' === strtoupper( $query['order'] ) ) { + $order = 'ASC'; + } else { + $order = 'DESC'; + } + $sql .= " ORDER BY $orderby $order"; + if ( $query['per_page'] > 0 ) { + $sql .= " LIMIT %d, %d"; + $sql_params[] = $query['offset']; + $sql_params[] = $query['per_page']; + } + } + + return $wpdb->prepare( $sql, $sql_params ); + } + + /** + * Query for action count or list of action IDs. + * + * @since x.x.x $query['status'] accepts array of statuses instead of a single status. + * + * @see ActionScheduler_Store::query_actions for $query arg usage. + * + * @param array $query Query filtering options. + * @param string $query_type Whether to select or count the results. Defaults to select. + * + * @return string|array|null The IDs of actions matching the query. Null on failure. + */ + public function query_actions( $query = array(), $query_type = 'select' ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $sql = $this->get_query_actions_sql( $query, $query_type ); + + return ( 'count' === $query_type ) ? $wpdb->get_var( $sql ) : $wpdb->get_col( $sql ); + } + + /** + * Get a count of all actions in the store, grouped by status + * + * @return array + */ + public function action_counts() { + + $action_counts_by_status = array(); + $action_stati_and_labels = $this->get_status_labels(); + $posts_count_by_status = (array) wp_count_posts( self::POST_TYPE, 'readable' ); + + foreach ( $posts_count_by_status as $post_status_name => $count ) { + + try { + $action_status_name = $this->get_action_status_by_post_status( $post_status_name ); + } catch ( Exception $e ) { + // Ignore any post statuses that aren't for actions + continue; + } + if ( array_key_exists( $action_status_name, $action_stati_and_labels ) ) { + $action_counts_by_status[ $action_status_name ] = $count; + } + } + + return $action_counts_by_status; + } + + /** + * @param string $action_id + * + * @throws InvalidArgumentException + */ + public function cancel_action( $action_id ) { + $post = get_post( $action_id ); + if ( empty( $post ) || ( $post->post_type != self::POST_TYPE ) ) { + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + do_action( 'action_scheduler_canceled_action', $action_id ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); + wp_trash_post( $action_id ); + remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); + } + + public function delete_action( $action_id ) { + $post = get_post( $action_id ); + if ( empty( $post ) || ( $post->post_type != self::POST_TYPE ) ) { + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + do_action( 'action_scheduler_deleted_action', $action_id ); + + wp_delete_post( $action_id, TRUE ); + } + + /** + * @param string $action_id + * + * @throws InvalidArgumentException + * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran. + */ + public function get_date( $action_id ) { + $next = $this->get_date_gmt( $action_id ); + return ActionScheduler_TimezoneHelper::set_local_timezone( $next ); + } + + /** + * @param string $action_id + * + * @throws InvalidArgumentException + * @return ActionScheduler_DateTime The date the action is schedule to run, or the date that it ran. + */ + public function get_date_gmt( $action_id ) { + $post = get_post( $action_id ); + if ( empty( $post ) || ( $post->post_type != self::POST_TYPE ) ) { + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + if ( $post->post_status == 'publish' ) { + return as_get_datetime_object( $post->post_modified_gmt ); + } else { + return as_get_datetime_object( $post->post_date_gmt ); + } + } + + /** + * @param int $max_actions + * @param DateTime $before_date Jobs must be schedule before this date. Defaults to now. + * @param array $hooks Claim only actions with a hook or hooks. + * @param string $group Claim only actions in the given group. + * + * @return ActionScheduler_ActionClaim + * @throws RuntimeException When there is an error staking a claim. + * @throws InvalidArgumentException When the given group is not valid. + */ + public function stake_claim( $max_actions = 10, DateTime $before_date = null, $hooks = array(), $group = '' ) { + $this->claim_before_date = $before_date; + $claim_id = $this->generate_claim_id(); + $this->claim_actions( $claim_id, $max_actions, $before_date, $hooks, $group ); + $action_ids = $this->find_actions_by_claim_id( $claim_id ); + $this->claim_before_date = null; + + return new ActionScheduler_ActionClaim( $claim_id, $action_ids ); + } + + /** + * @return int + */ + public function get_claim_count(){ + global $wpdb; + + $sql = "SELECT COUNT(DISTINCT post_password) FROM {$wpdb->posts} WHERE post_password != '' AND post_type = %s AND post_status IN ('in-progress','pending')"; + $sql = $wpdb->prepare( $sql, array( self::POST_TYPE ) ); + + return $wpdb->get_var( $sql ); + } + + protected function generate_claim_id() { + $claim_id = md5(microtime(true) . rand(0,1000)); + return substr($claim_id, 0, 20); // to fit in db field with 20 char limit + } + + /** + * @param string $claim_id + * @param int $limit + * @param DateTime $before_date Should use UTC timezone. + * @param array $hooks Claim only actions with a hook or hooks. + * @param string $group Claim only actions in the given group. + * + * @return int The number of actions that were claimed + * @throws RuntimeException When there is a database error. + * @throws InvalidArgumentException When the group is invalid. + */ + protected function claim_actions( $claim_id, $limit, DateTime $before_date = null, $hooks = array(), $group = '' ) { + // Set up initial variables. + $date = null === $before_date ? as_get_datetime_object() : clone $before_date; + $limit_ids = ! empty( $group ); + $ids = $limit_ids ? $this->get_actions_by_group( $group, $limit, $date ) : array(); + + // If limiting by IDs and no posts found, then return early since we have nothing to update. + if ( $limit_ids && 0 === count( $ids ) ) { + return 0; + } + + /** @var wpdb $wpdb */ + global $wpdb; + + /* + * Build up custom query to update the affected posts. Parameters are built as a separate array + * to make it easier to identify where they are in the query. + * + * We can't use $wpdb->update() here because of the "ID IN ..." clause. + */ + $update = "UPDATE {$wpdb->posts} SET post_password = %s, post_modified_gmt = %s, post_modified = %s"; + $params = array( + $claim_id, + current_time( 'mysql', true ), + current_time( 'mysql' ), + ); + + // Build initial WHERE clause. + $where = "WHERE post_type = %s AND post_status = %s AND post_password = ''"; + $params[] = self::POST_TYPE; + $params[] = ActionScheduler_Store::STATUS_PENDING; + + if ( ! empty( $hooks ) ) { + $placeholders = array_fill( 0, count( $hooks ), '%s' ); + $where .= ' AND post_title IN (' . join( ', ', $placeholders ) . ')'; + $params = array_merge( $params, array_values( $hooks ) ); + } + + /* + * Add the IDs to the WHERE clause. IDs not escaped because they came directly from a prior DB query. + * + * If we're not limiting by IDs, then include the post_date_gmt clause. + */ + if ( $limit_ids ) { + $where .= ' AND ID IN (' . join( ',', $ids ) . ')'; + } else { + $where .= ' AND post_date_gmt <= %s'; + $params[] = $date->format( 'Y-m-d H:i:s' ); + } + + // Add the ORDER BY clause and,ms limit. + $order = 'ORDER BY menu_order ASC, post_date_gmt ASC, ID ASC LIMIT %d'; + $params[] = $limit; + + // Run the query and gather results. + $rows_affected = $wpdb->query( $wpdb->prepare( "{$update} {$where} {$order}", $params ) ); + if ( $rows_affected === false ) { + throw new RuntimeException( __( 'Unable to claim actions. Database error.', 'woocommerce' ) ); + } + + return (int) $rows_affected; + } + + /** + * Get IDs of actions within a certain group and up to a certain date/time. + * + * @param string $group The group to use in finding actions. + * @param int $limit The number of actions to retrieve. + * @param DateTime $date DateTime object representing cutoff time for actions. Actions retrieved will be + * up to and including this DateTime. + * + * @return array IDs of actions in the appropriate group and before the appropriate time. + * @throws InvalidArgumentException When the group does not exist. + */ + protected function get_actions_by_group( $group, $limit, DateTime $date ) { + // Ensure the group exists before continuing. + if ( ! term_exists( $group, self::GROUP_TAXONOMY )) { + throw new InvalidArgumentException( sprintf( __( 'The group "%s" does not exist.', 'woocommerce' ), $group ) ); + } + + // Set up a query for post IDs to use later. + $query = new WP_Query(); + $query_args = array( + 'fields' => 'ids', + 'post_type' => self::POST_TYPE, + 'post_status' => ActionScheduler_Store::STATUS_PENDING, + 'has_password' => false, + 'posts_per_page' => $limit * 3, + 'suppress_filters' => true, + 'no_found_rows' => true, + 'orderby' => array( + 'menu_order' => 'ASC', + 'date' => 'ASC', + 'ID' => 'ASC', + ), + 'date_query' => array( + 'column' => 'post_date_gmt', + 'before' => $date->format( 'Y-m-d H:i' ), + 'inclusive' => true, + ), + 'tax_query' => array( + array( + 'taxonomy' => self::GROUP_TAXONOMY, + 'field' => 'slug', + 'terms' => $group, + 'include_children' => false, + ), + ), + ); + + return $query->query( $query_args ); + } + + /** + * @param string $claim_id + * @return array + */ + public function find_actions_by_claim_id( $claim_id ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $sql = "SELECT ID, post_date_gmt FROM {$wpdb->posts} WHERE post_type = %s AND post_password = %s"; + $sql = $wpdb->prepare( $sql, array( self::POST_TYPE, $claim_id ) ); + + $action_ids = array(); + $before_date = isset( $this->claim_before_date ) ? $this->claim_before_date : as_get_datetime_object(); + $cut_off = $before_date->format( 'Y-m-d H:i:s' ); + + // Verify that the scheduled date for each action is within the expected bounds (in some unusual + // cases, we cannot depend on MySQL to honor all of the WHERE conditions we specify). + foreach ( $wpdb->get_results( $sql ) as $claimed_action ) { + if ( $claimed_action->post_date_gmt <= $cut_off ) { + $action_ids[] = absint( $claimed_action->ID ); + } + } + + return $action_ids; + } + + public function release_claim( ActionScheduler_ActionClaim $claim ) { + $action_ids = $this->find_actions_by_claim_id( $claim->get_id() ); + if ( empty( $action_ids ) ) { + return; // nothing to do + } + $action_id_string = implode( ',', array_map( 'intval', $action_ids ) ); + /** @var wpdb $wpdb */ + global $wpdb; + $sql = "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID IN ($action_id_string) AND post_password = %s"; + $sql = $wpdb->prepare( $sql, array( $claim->get_id() ) ); + $result = $wpdb->query( $sql ); + if ( $result === false ) { + /* translators: %s: claim ID */ + throw new RuntimeException( sprintf( __( 'Unable to unlock claim %s. Database error.', 'woocommerce' ), $claim->get_id() ) ); + } + } + + /** + * @param string $action_id + */ + public function unclaim_action( $action_id ) { + /** @var wpdb $wpdb */ + global $wpdb; + $sql = "UPDATE {$wpdb->posts} SET post_password = '' WHERE ID = %d AND post_type = %s"; + $sql = $wpdb->prepare( $sql, $action_id, self::POST_TYPE ); + $result = $wpdb->query( $sql ); + if ( $result === false ) { + /* translators: %s: action ID */ + throw new RuntimeException( sprintf( __( 'Unable to unlock claim on action %s. Database error.', 'woocommerce' ), $action_id ) ); + } + } + + public function mark_failure( $action_id ) { + /** @var wpdb $wpdb */ + global $wpdb; + $sql = "UPDATE {$wpdb->posts} SET post_status = %s WHERE ID = %d AND post_type = %s"; + $sql = $wpdb->prepare( $sql, self::STATUS_FAILED, $action_id, self::POST_TYPE ); + $result = $wpdb->query( $sql ); + if ( $result === false ) { + /* translators: %s: action ID */ + throw new RuntimeException( sprintf( __( 'Unable to mark failure on action %s. Database error.', 'woocommerce' ), $action_id ) ); + } + } + + /** + * Return an action's claim ID, as stored in the post password column + * + * @param string $action_id + * @return mixed + */ + public function get_claim_id( $action_id ) { + return $this->get_post_column( $action_id, 'post_password' ); + } + + /** + * Return an action's status, as stored in the post status column + * + * @param string $action_id + * @return mixed + */ + public function get_status( $action_id ) { + $status = $this->get_post_column( $action_id, 'post_status' ); + + if ( $status === null ) { + throw new InvalidArgumentException( __( 'Invalid action ID. No status found.', 'woocommerce' ) ); + } + + return $this->get_action_status_by_post_status( $status ); + } + + private function get_post_column( $action_id, $column_name ) { + /** @var \wpdb $wpdb */ + global $wpdb; + return $wpdb->get_var( $wpdb->prepare( "SELECT {$column_name} FROM {$wpdb->posts} WHERE ID=%d AND post_type=%s", $action_id, self::POST_TYPE ) ); + } + + /** + * @param string $action_id + */ + public function log_execution( $action_id ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $sql = "UPDATE {$wpdb->posts} SET menu_order = menu_order+1, post_status=%s, post_modified_gmt = %s, post_modified = %s WHERE ID = %d AND post_type = %s"; + $sql = $wpdb->prepare( $sql, self::STATUS_RUNNING, current_time('mysql', true), current_time('mysql'), $action_id, self::POST_TYPE ); + $wpdb->query($sql); + } + + /** + * Record that an action was completed. + * + * @param int $action_id ID of the completed action. + * @throws InvalidArgumentException|RuntimeException + */ + public function mark_complete( $action_id ) { + $post = get_post( $action_id ); + if ( empty( $post ) || ( $post->post_type != self::POST_TYPE ) ) { + throw new InvalidArgumentException( sprintf( __( 'Unidentified action %s', 'woocommerce' ), $action_id ) ); + } + add_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10, 1 ); + add_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10, 5 ); + $result = wp_update_post(array( + 'ID' => $action_id, + 'post_status' => 'publish', + ), TRUE); + remove_filter( 'wp_insert_post_data', array( $this, 'filter_insert_post_data' ), 10 ); + remove_filter( 'pre_wp_unique_post_slug', array( $this, 'set_unique_post_slug' ), 10 ); + if ( is_wp_error( $result ) ) { + throw new RuntimeException( $result->get_error_message() ); + } + } + + /** + * Mark action as migrated when there is an error deleting the action. + * + * @param int $action_id Action ID. + */ + public function mark_migrated( $action_id ) { + wp_update_post( + array( + 'ID' => $action_id, + 'post_status' => 'migrated' + ) + ); + } + + /** + * Determine whether the post store can be migrated. + * + * @return bool + */ + public function migration_dependencies_met( $setting ) { + global $wpdb; + + $dependencies_met = get_transient( self::DEPENDENCIES_MET ); + if ( empty( $dependencies_met ) ) { + $maximum_args_length = apply_filters( 'action_scheduler_maximum_args_length', 191 ); + $found_action = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND CHAR_LENGTH(post_content) > %d LIMIT 1", + $maximum_args_length, + self::POST_TYPE + ) + ); + $dependencies_met = $found_action ? 'no' : 'yes'; + set_transient( self::DEPENDENCIES_MET, $dependencies_met, DAY_IN_SECONDS ); + } + + return 'yes' == $dependencies_met ? $setting : false; + } + + /** + * InnoDB indexes have a maximum size of 767 bytes by default, which is only 191 characters with utf8mb4. + * + * Previously, AS wasn't concerned about args length, as we used the (unindex) post_content column. However, + * as we prepare to move to custom tables, and can use an indexed VARCHAR column instead, we want to warn + * developers of this impending requirement. + * + * @param ActionScheduler_Action $action + */ + protected function validate_action( ActionScheduler_Action $action ) { + try { + parent::validate_action( $action ); + } catch ( Exception $e ) { + $message = sprintf( __( '%s Support for strings longer than this will be removed in a future version.', 'woocommerce' ), $e->getMessage() ); + _doing_it_wrong( 'ActionScheduler_Action::$args', $message, '2.1.0' ); + } + } + + /** + * @codeCoverageIgnore + */ + public function init() { + add_filter( 'action_scheduler_migration_dependencies_met', array( $this, 'migration_dependencies_met' ) ); + + $post_type_registrar = new ActionScheduler_wpPostStore_PostTypeRegistrar(); + $post_type_registrar->register(); + + $post_status_registrar = new ActionScheduler_wpPostStore_PostStatusRegistrar(); + $post_status_registrar->register(); + + $taxonomy_registrar = new ActionScheduler_wpPostStore_TaxonomyRegistrar(); + $taxonomy_registrar->register(); + } +} diff --git a/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php new file mode 100644 index 0000000..4aaf896 --- /dev/null +++ b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostStatusRegistrar.php @@ -0,0 +1,58 @@ +post_status_args(), $this->post_status_running_labels() ) ); + register_post_status( ActionScheduler_Store::STATUS_FAILED, array_merge( $this->post_status_args(), $this->post_status_failed_labels() ) ); + } + + /** + * Build the args array for the post type definition + * + * @return array + */ + protected function post_status_args() { + $args = array( + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => true, + 'show_in_admin_status_list' => true, + ); + + return apply_filters( 'action_scheduler_post_status_args', $args ); + } + + /** + * Build the args array for the post type definition + * + * @return array + */ + protected function post_status_failed_labels() { + $labels = array( + 'label' => _x( 'Failed', 'post', 'woocommerce' ), + /* translators: %s: count */ + 'label_count' => _n_noop( 'Failed (%s)', 'Failed (%s)', 'woocommerce' ), + ); + + return apply_filters( 'action_scheduler_post_status_failed_labels', $labels ); + } + + /** + * Build the args array for the post type definition + * + * @return array + */ + protected function post_status_running_labels() { + $labels = array( + 'label' => _x( 'In-Progress', 'post', 'woocommerce' ), + /* translators: %s: count */ + 'label_count' => _n_noop( 'In-Progress (%s)', 'In-Progress (%s)', 'woocommerce' ), + ); + + return apply_filters( 'action_scheduler_post_status_running_labels', $labels ); + } +} diff --git a/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php new file mode 100644 index 0000000..7aa29b2 --- /dev/null +++ b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_PostTypeRegistrar.php @@ -0,0 +1,50 @@ +post_type_args() ); + } + + /** + * Build the args array for the post type definition + * + * @return array + */ + protected function post_type_args() { + $args = array( + 'label' => __( 'Scheduled Actions', 'woocommerce' ), + 'description' => __( 'Scheduled actions are hooks triggered on a cetain date and time.', 'woocommerce' ), + 'public' => false, + 'map_meta_cap' => true, + 'hierarchical' => false, + 'supports' => array('title', 'editor','comments'), + 'rewrite' => false, + 'query_var' => false, + 'can_export' => true, + 'ep_mask' => EP_NONE, + 'labels' => array( + 'name' => __( 'Scheduled Actions', 'woocommerce' ), + 'singular_name' => __( 'Scheduled Action', 'woocommerce' ), + 'menu_name' => _x( 'Scheduled Actions', 'Admin menu name', 'woocommerce' ), + 'add_new' => __( 'Add', 'woocommerce' ), + 'add_new_item' => __( 'Add New Scheduled Action', 'woocommerce' ), + 'edit' => __( 'Edit', 'woocommerce' ), + 'edit_item' => __( 'Edit Scheduled Action', 'woocommerce' ), + 'new_item' => __( 'New Scheduled Action', 'woocommerce' ), + 'view' => __( 'View Action', 'woocommerce' ), + 'view_item' => __( 'View Action', 'woocommerce' ), + 'search_items' => __( 'Search Scheduled Actions', 'woocommerce' ), + 'not_found' => __( 'No actions found', 'woocommerce' ), + 'not_found_in_trash' => __( 'No actions found in trash', 'woocommerce' ), + ), + ); + + $args = apply_filters('action_scheduler_post_type_args', $args); + return $args; + } +} + \ No newline at end of file diff --git a/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php new file mode 100644 index 0000000..844e596 --- /dev/null +++ b/packages/action-scheduler/classes/data-stores/ActionScheduler_wpPostStore_TaxonomyRegistrar.php @@ -0,0 +1,26 @@ +taxonomy_args() ); + } + + protected function taxonomy_args() { + $args = array( + 'label' => __( 'Action Group', 'woocommerce' ), + 'public' => false, + 'hierarchical' => false, + 'show_admin_column' => true, + 'query_var' => false, + 'rewrite' => false, + ); + + $args = apply_filters( 'action_scheduler_taxonomy_args', $args ); + return $args; + } +} + \ No newline at end of file diff --git a/packages/action-scheduler/classes/migration/ActionMigrator.php b/packages/action-scheduler/classes/migration/ActionMigrator.php new file mode 100644 index 0000000..4e93e7a --- /dev/null +++ b/packages/action-scheduler/classes/migration/ActionMigrator.php @@ -0,0 +1,109 @@ +source = $source_store; + $this->destination = $destination_store; + $this->log_migrator = $log_migrator; + } + + /** + * Migrate an action. + * + * @param int $source_action_id Action ID. + * + * @return int 0|new action ID + */ + public function migrate( $source_action_id ) { + try { + $action = $this->source->fetch_action( $source_action_id ); + $status = $this->source->get_status( $source_action_id ); + } catch ( \Exception $e ) { + $action = null; + $status = ''; + } + + if ( is_null( $action ) || empty( $status ) || ! $action->get_schedule()->get_date() ) { + // null action or empty status means the fetch operation failed or the action didn't exist + // null schedule means it's missing vital data + // delete it and move on + try { + $this->source->delete_action( $source_action_id ); + } catch ( \Exception $e ) { + // nothing to do, it didn't exist in the first place + } + do_action( 'action_scheduler/no_action_to_migrate', $source_action_id, $this->source, $this->destination ); + + return 0; + } + + try { + + // Make sure the last attempt date is set correctly for completed and failed actions + $last_attempt_date = ( $status !== \ActionScheduler_Store::STATUS_PENDING ) ? $this->source->get_date( $source_action_id ) : null; + + $destination_action_id = $this->destination->save_action( $action, null, $last_attempt_date ); + } catch ( \Exception $e ) { + do_action( 'action_scheduler/migrate_action_failed', $source_action_id, $this->source, $this->destination ); + + return 0; // could not save the action in the new store + } + + try { + switch ( $status ) { + case \ActionScheduler_Store::STATUS_FAILED : + $this->destination->mark_failure( $destination_action_id ); + break; + case \ActionScheduler_Store::STATUS_CANCELED : + $this->destination->cancel_action( $destination_action_id ); + break; + } + + $this->log_migrator->migrate( $source_action_id, $destination_action_id ); + $this->source->delete_action( $source_action_id ); + + $test_action = $this->source->fetch_action( $source_action_id ); + if ( ! is_a( $test_action, 'ActionScheduler_NullAction' ) ) { + throw new \RuntimeException( sprintf( __( 'Unable to remove source migrated action %s', 'woocommerce' ), $source_action_id ) ); + } + do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); + + return $destination_action_id; + } catch ( \Exception $e ) { + // could not delete from the old store + $this->source->mark_migrated( $source_action_id ); + do_action( 'action_scheduler/migrate_action_incomplete', $source_action_id, $destination_action_id, $this->source, $this->destination ); + do_action( 'action_scheduler/migrated_action', $source_action_id, $destination_action_id, $this->source, $this->destination ); + + return $destination_action_id; + } + } +} diff --git a/packages/action-scheduler/classes/migration/ActionScheduler_DBStoreMigrator.php b/packages/action-scheduler/classes/migration/ActionScheduler_DBStoreMigrator.php new file mode 100644 index 0000000..8a9ce4b --- /dev/null +++ b/packages/action-scheduler/classes/migration/ActionScheduler_DBStoreMigrator.php @@ -0,0 +1,47 @@ + $this->get_scheduled_date_string( $action, $last_attempt_date ), + 'last_attempt_local' => $this->get_scheduled_date_string_local( $action, $last_attempt_date ), + ]; + + $wpdb->update( $wpdb->actionscheduler_actions, $data, array( 'action_id' => $action_id ), array( '%s', '%s' ), array( '%d' ) ); + } + + return $action_id; + } catch ( \Exception $e ) { + throw new \RuntimeException( sprintf( __( 'Error saving action: %s', 'woocommerce' ), $e->getMessage() ), 0 ); + } + } +} diff --git a/packages/action-scheduler/classes/migration/BatchFetcher.php b/packages/action-scheduler/classes/migration/BatchFetcher.php new file mode 100644 index 0000000..4872801 --- /dev/null +++ b/packages/action-scheduler/classes/migration/BatchFetcher.php @@ -0,0 +1,86 @@ +store = $source_store; + } + + /** + * Retrieve a list of actions. + * + * @param int $count The number of actions to retrieve + * + * @return int[] A list of action IDs + */ + public function fetch( $count = 10 ) { + foreach ( $this->get_query_strategies( $count ) as $query ) { + $action_ids = $this->store->query_actions( $query ); + if ( ! empty( $action_ids ) ) { + return $action_ids; + } + } + + return []; + } + + /** + * Generate a list of prioritized of action search parameters. + * + * @param int $count Number of actions to find. + * + * @return array + */ + private function get_query_strategies( $count ) { + $now = as_get_datetime_object(); + $args = [ + 'date' => $now, + 'per_page' => $count, + 'offset' => 0, + 'orderby' => 'date', + 'order' => 'ASC', + ]; + + $priorities = [ + Store::STATUS_PENDING, + Store::STATUS_FAILED, + Store::STATUS_CANCELED, + Store::STATUS_COMPLETE, + Store::STATUS_RUNNING, + '', // any other unanticipated status + ]; + + foreach ( $priorities as $status ) { + yield wp_parse_args( [ + 'status' => $status, + 'date_compare' => '<=', + ], $args ); + yield wp_parse_args( [ + 'status' => $status, + 'date_compare' => '>=', + ], $args ); + } + } +} \ No newline at end of file diff --git a/packages/action-scheduler/classes/migration/Config.php b/packages/action-scheduler/classes/migration/Config.php new file mode 100644 index 0000000..c52443e --- /dev/null +++ b/packages/action-scheduler/classes/migration/Config.php @@ -0,0 +1,168 @@ +source_store ) ) { + throw new \RuntimeException( __( 'Source store must be configured before running a migration', 'woocommerce' ) ); + } + + return $this->source_store; + } + + /** + * Set the configured source store. + * + * @param ActionScheduler_Store $store Source store object. + */ + public function set_source_store( Store $store ) { + $this->source_store = $store; + } + + /** + * Get the configured source loger. + * + * @return ActionScheduler_Logger + */ + public function get_source_logger() { + if ( empty( $this->source_logger ) ) { + throw new \RuntimeException( __( 'Source logger must be configured before running a migration', 'woocommerce' ) ); + } + + return $this->source_logger; + } + + /** + * Set the configured source logger. + * + * @param ActionScheduler_Logger $logger + */ + public function set_source_logger( Logger $logger ) { + $this->source_logger = $logger; + } + + /** + * Get the configured destination store. + * + * @return ActionScheduler_Store + */ + public function get_destination_store() { + if ( empty( $this->destination_store ) ) { + throw new \RuntimeException( __( 'Destination store must be configured before running a migration', 'woocommerce' ) ); + } + + return $this->destination_store; + } + + /** + * Set the configured destination store. + * + * @param ActionScheduler_Store $store + */ + public function set_destination_store( Store $store ) { + $this->destination_store = $store; + } + + /** + * Get the configured destination logger. + * + * @return ActionScheduler_Logger + */ + public function get_destination_logger() { + if ( empty( $this->destination_logger ) ) { + throw new \RuntimeException( __( 'Destination logger must be configured before running a migration', 'woocommerce' ) ); + } + + return $this->destination_logger; + } + + /** + * Set the configured destination logger. + * + * @param ActionScheduler_Logger $logger + */ + public function set_destination_logger( Logger $logger ) { + $this->destination_logger = $logger; + } + + /** + * Get flag indicating whether it's a dry run. + * + * @return bool + */ + public function get_dry_run() { + return $this->dry_run; + } + + /** + * Set flag indicating whether it's a dry run. + * + * @param bool $dry_run + */ + public function set_dry_run( $dry_run ) { + $this->dry_run = (bool) $dry_run; + } + + /** + * Get progress bar object. + * + * @return ActionScheduler\WPCLI\ProgressBar + */ + public function get_progress_bar() { + return $this->progress_bar; + } + + /** + * Set progress bar object. + * + * @param ActionScheduler\WPCLI\ProgressBar $progress_bar + */ + public function set_progress_bar( ProgressBar $progress_bar ) { + $this->progress_bar = $progress_bar; + } +} diff --git a/packages/action-scheduler/classes/migration/Controller.php b/packages/action-scheduler/classes/migration/Controller.php new file mode 100644 index 0000000..30c14fc --- /dev/null +++ b/packages/action-scheduler/classes/migration/Controller.php @@ -0,0 +1,226 @@ +migration_scheduler = $migration_scheduler; + $this->store_classname = ''; + } + + /** + * Set the action store class name. + * + * @param string $class Classname of the store class. + * + * @return string + */ + public function get_store_class( $class ) { + if ( \ActionScheduler_DataController::is_migration_complete() ) { + return \ActionScheduler_DataController::DATASTORE_CLASS; + } elseif ( \ActionScheduler_Store::DEFAULT_CLASS !== $class ) { + $this->store_classname = $class; + return $class; + } else { + return 'ActionScheduler_HybridStore'; + } + } + + /** + * Set the action logger class name. + * + * @param string $class Classname of the logger class. + * + * @return string + */ + public function get_logger_class( $class ) { + \ActionScheduler_Store::instance(); + + if ( $this->has_custom_datastore() ) { + $this->logger_classname = $class; + return $class; + } else { + return \ActionScheduler_DataController::LOGGER_CLASS; + } + } + + /** + * Get flag indicating whether a custom datastore is in use. + * + * @return bool + */ + public function has_custom_datastore() { + return (bool) $this->store_classname; + } + + /** + * Set up the background migration process. + * + * @return void + */ + public function schedule_migration() { + $logging_tables = new ActionScheduler_LoggerSchema(); + $store_tables = new ActionScheduler_StoreSchema(); + + /* + * In some unusual cases, the expected tables may not have been created. In such cases + * we do not schedule a migration as doing so will lead to fatal error conditions. + * + * In such cases the user will likely visit the Tools > Scheduled Actions screen to + * investigate, and will see appropriate messaging (this step also triggers an attempt + * to rebuild any missing tables). + * + * @see https://github.com/woocommerce/action-scheduler/issues/653 + */ + if ( + ActionScheduler_DataController::is_migration_complete() + || $this->migration_scheduler->is_migration_scheduled() + || ! $store_tables->tables_exist() + || ! $logging_tables->tables_exist() + ) { + return; + } + + $this->migration_scheduler->schedule_migration(); + } + + /** + * Get the default migration config object + * + * @return ActionScheduler\Migration\Config + */ + public function get_migration_config_object() { + static $config = null; + + if ( ! $config ) { + $source_store = $this->store_classname ? new $this->store_classname() : new \ActionScheduler_wpPostStore(); + $source_logger = $this->logger_classname ? new $this->logger_classname() : new \ActionScheduler_wpCommentLogger(); + + $config = new Config(); + $config->set_source_store( $source_store ); + $config->set_source_logger( $source_logger ); + $config->set_destination_store( new \ActionScheduler_DBStoreMigrator() ); + $config->set_destination_logger( new \ActionScheduler_DBLogger() ); + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + $config->set_progress_bar( new ProgressBar( '', 0 ) ); + } + } + + return apply_filters( 'action_scheduler/migration_config', $config ); + } + + /** + * Hook dashboard migration notice. + */ + public function hook_admin_notices() { + if ( ! $this->allow_migration() || \ActionScheduler_DataController::is_migration_complete() ) { + return; + } + add_action( 'admin_notices', array( $this, 'display_migration_notice' ), 10, 0 ); + } + + /** + * Show a dashboard notice that migration is in progress. + */ + public function display_migration_notice() { + printf( '

    %s

    ', esc_html__( 'Action Scheduler migration in progress. The list of scheduled actions may be incomplete.', 'woocommerce' ) ); + } + + /** + * Add store classes. Hook migration. + */ + private function hook() { + add_filter( 'action_scheduler_store_class', array( $this, 'get_store_class' ), 100, 1 ); + add_filter( 'action_scheduler_logger_class', array( $this, 'get_logger_class' ), 100, 1 ); + add_action( 'init', array( $this, 'maybe_hook_migration' ) ); + add_action( 'wp_loaded', array( $this, 'schedule_migration' ) ); + + // Action Scheduler may be displayed as a Tools screen or WooCommerce > Status administration screen + add_action( 'load-tools_page_action-scheduler', array( $this, 'hook_admin_notices' ), 10, 0 ); + add_action( 'load-woocommerce_page_wc-status', array( $this, 'hook_admin_notices' ), 10, 0 ); + } + + /** + * Possibly hook the migration scheduler action. + * + * @author Jeremy Pry + */ + public function maybe_hook_migration() { + if ( ! $this->allow_migration() || \ActionScheduler_DataController::is_migration_complete() ) { + return; + } + + $this->migration_scheduler->hook(); + } + + /** + * Allow datastores to enable migration to AS tables. + */ + public function allow_migration() { + if ( ! \ActionScheduler_DataController::dependencies_met() ) { + return false; + } + + if ( null === $this->migrate_custom_store ) { + $this->migrate_custom_store = apply_filters( 'action_scheduler_migrate_data_store', false ); + } + + return ( ! $this->has_custom_datastore() ) || $this->migrate_custom_store; + } + + /** + * Proceed with the migration if the dependencies have been met. + */ + public static function init() { + if ( \ActionScheduler_DataController::dependencies_met() ) { + self::instance()->hook(); + } + } + + /** + * Singleton factory. + */ + public static function instance() { + if ( ! isset( self::$instance ) ) { + self::$instance = new static( new Scheduler() ); + } + + return self::$instance; + } +} diff --git a/packages/action-scheduler/classes/migration/DryRun_ActionMigrator.php b/packages/action-scheduler/classes/migration/DryRun_ActionMigrator.php new file mode 100644 index 0000000..ffc21c2 --- /dev/null +++ b/packages/action-scheduler/classes/migration/DryRun_ActionMigrator.php @@ -0,0 +1,28 @@ +source = $source_logger; + $this->destination = $destination_Logger; + } + + /** + * Migrate an action log. + * + * @param int $source_action_id Source logger object. + * @param int $destination_action_id Destination logger object. + */ + public function migrate( $source_action_id, $destination_action_id ) { + $logs = $this->source->get_logs( $source_action_id ); + foreach ( $logs as $log ) { + if ( $log->get_action_id() == $source_action_id ) { + $this->destination->log( $destination_action_id, $log->get_message(), $log->get_date() ); + } + } + } +} diff --git a/packages/action-scheduler/classes/migration/Runner.php b/packages/action-scheduler/classes/migration/Runner.php new file mode 100644 index 0000000..d1c41dc --- /dev/null +++ b/packages/action-scheduler/classes/migration/Runner.php @@ -0,0 +1,136 @@ +source_store = $config->get_source_store(); + $this->destination_store = $config->get_destination_store(); + $this->source_logger = $config->get_source_logger(); + $this->destination_logger = $config->get_destination_logger(); + + $this->batch_fetcher = new BatchFetcher( $this->source_store ); + if ( $config->get_dry_run() ) { + $this->log_migrator = new DryRun_LogMigrator( $this->source_logger, $this->destination_logger ); + $this->action_migrator = new DryRun_ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator ); + } else { + $this->log_migrator = new LogMigrator( $this->source_logger, $this->destination_logger ); + $this->action_migrator = new ActionMigrator( $this->source_store, $this->destination_store, $this->log_migrator ); + } + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + $this->progress_bar = $config->get_progress_bar(); + } + } + + /** + * Run migration batch. + * + * @param int $batch_size Optional batch size. Default 10. + * + * @return int Size of batch processed. + */ + public function run( $batch_size = 10 ) { + $batch = $this->batch_fetcher->fetch( $batch_size ); + $batch_size = count( $batch ); + + if ( ! $batch_size ) { + return 0; + } + + if ( $this->progress_bar ) { + /* translators: %d: amount of actions */ + $this->progress_bar->set_message( sprintf( _n( 'Migrating %d action', 'Migrating %d actions', $batch_size, 'woocommerce' ), number_format_i18n( $batch_size ) ) ); + $this->progress_bar->set_count( $batch_size ); + } + + $this->migrate_actions( $batch ); + + return $batch_size; + } + + /** + * Migration a batch of actions. + * + * @param array $action_ids List of action IDs to migrate. + */ + public function migrate_actions( array $action_ids ) { + do_action( 'action_scheduler/migration_batch_starting', $action_ids ); + + \ActionScheduler::logger()->unhook_stored_action(); + $this->destination_logger->unhook_stored_action(); + + foreach ( $action_ids as $source_action_id ) { + $destination_action_id = $this->action_migrator->migrate( $source_action_id ); + if ( $destination_action_id ) { + $this->destination_logger->log( $destination_action_id, sprintf( + /* translators: 1: source action ID 2: source store class 3: destination action ID 4: destination store class */ + __( 'Migrated action with ID %1$d in %2$s to ID %3$d in %4$s', 'woocommerce' ), + $source_action_id, + get_class( $this->source_store ), + $destination_action_id, + get_class( $this->destination_store ) + ) ); + } + + if ( $this->progress_bar ) { + $this->progress_bar->tick(); + } + } + + if ( $this->progress_bar ) { + $this->progress_bar->finish(); + } + + \ActionScheduler::logger()->hook_stored_action(); + + do_action( 'action_scheduler/migration_batch_complete', $action_ids ); + } + + /** + * Initialize destination store and logger. + */ + public function init_destination() { + $this->destination_store->init(); + $this->destination_logger->init(); + } +} diff --git a/packages/action-scheduler/classes/migration/Scheduler.php b/packages/action-scheduler/classes/migration/Scheduler.php new file mode 100644 index 0000000..dcbe2db --- /dev/null +++ b/packages/action-scheduler/classes/migration/Scheduler.php @@ -0,0 +1,128 @@ +get_migration_runner(); + $count = $migration_runner->run( $this->get_batch_size() ); + + if ( $count === 0 ) { + $this->mark_complete(); + } else { + $this->schedule_migration( time() + $this->get_schedule_interval() ); + } + } + + /** + * Mark the migration complete. + */ + public function mark_complete() { + $this->unschedule_migration(); + + \ActionScheduler_DataController::mark_migration_complete(); + do_action( 'action_scheduler/migration_complete' ); + } + + /** + * Get a flag indicating whether the migration is scheduled. + * + * @return bool Whether there is a pending action in the store to handle the migration + */ + public function is_migration_scheduled() { + $next = as_next_scheduled_action( self::HOOK ); + + return ! empty( $next ); + } + + /** + * Schedule the migration. + * + * @param int $when Optional timestamp to run the next migration batch. Defaults to now. + * + * @return string The action ID + */ + public function schedule_migration( $when = 0 ) { + $next = as_next_scheduled_action( self::HOOK ); + + if ( ! empty( $next ) ) { + return $next; + } + + if ( empty( $when ) ) { + $when = time() + MINUTE_IN_SECONDS; + } + + return as_schedule_single_action( $when, self::HOOK, array(), self::GROUP ); + } + + /** + * Remove the scheduled migration action. + */ + public function unschedule_migration() { + as_unschedule_action( self::HOOK, null, self::GROUP ); + } + + /** + * Get migration batch schedule interval. + * + * @return int Seconds between migration runs. Defaults to 0 seconds to allow chaining migration via Async Runners. + */ + private function get_schedule_interval() { + return (int) apply_filters( 'action_scheduler/migration_interval', 0 ); + } + + /** + * Get migration batch size. + * + * @return int Number of actions to migrate in each batch. Defaults to 250. + */ + private function get_batch_size() { + return (int) apply_filters( 'action_scheduler/migration_batch_size', 250 ); + } + + /** + * Get migration runner object. + * + * @return Runner + */ + private function get_migration_runner() { + $config = Controller::instance()->get_migration_config_object(); + + return new Runner( $config ); + } + +} diff --git a/packages/action-scheduler/classes/schedules/ActionScheduler_CanceledSchedule.php b/packages/action-scheduler/classes/schedules/ActionScheduler_CanceledSchedule.php new file mode 100644 index 0000000..840e482 --- /dev/null +++ b/packages/action-scheduler/classes/schedules/ActionScheduler_CanceledSchedule.php @@ -0,0 +1,57 @@ +__wakeup() for details. + **/ + private $timestamp = NULL; + + /** + * @param DateTime $after + * + * @return DateTime|null + */ + public function calculate_next( DateTime $after ) { + return null; + } + + /** + * Cancelled actions should never have a next schedule, even if get_next() + * is called with $after < $this->scheduled_date. + * + * @param DateTime $after + * @return DateTime|null + */ + public function get_next( DateTime $after ) { + return null; + } + + /** + * @return bool + */ + public function is_recurring() { + return false; + } + + /** + * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, schedules used different property names to refer + * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To maintain backward + * compatibility with schedules serialized and stored prior to 3.0, we need to correctly + * map the old property names with matching visibility. + */ + public function __wakeup() { + if ( ! is_null( $this->timestamp ) ) { + $this->scheduled_timestamp = $this->timestamp; + unset( $this->timestamp ); + } + parent::__wakeup(); + } +} diff --git a/packages/action-scheduler/classes/schedules/ActionScheduler_CronSchedule.php b/packages/action-scheduler/classes/schedules/ActionScheduler_CronSchedule.php new file mode 100644 index 0000000..7859307 --- /dev/null +++ b/packages/action-scheduler/classes/schedules/ActionScheduler_CronSchedule.php @@ -0,0 +1,102 @@ +__wakeup() for details. + **/ + private $start_timestamp = NULL; + + /** + * Deprecated property @see $this->__wakeup() for details. + **/ + private $cron = NULL; + + /** + * Wrapper for parent constructor to accept a cron expression string and map it to a CronExpression for this + * objects $recurrence property. + * + * @param DateTime $start The date & time to run the action at or after. If $start aligns with the CronSchedule passed via $recurrence, it will be used. If it does not align, the first matching date after it will be used. + * @param CronExpression|string $recurrence The CronExpression used to calculate the schedule's next instance. + * @param DateTime|null $first (Optional) The date & time the first instance of this interval schedule ran. Default null, meaning this is the first instance. + */ + public function __construct( DateTime $start, $recurrence, DateTime $first = null ) { + if ( ! is_a( $recurrence, 'CronExpression' ) ) { + $recurrence = CronExpression::factory( $recurrence ); + } + + // For backward compatibility, we need to make sure the date is set to the first matching cron date, not whatever date is passed in. Importantly, by passing true as the 3rd param, if $start matches the cron expression, then it will be used. This was previously handled in the now deprecated next() method. + $date = $recurrence->getNextRunDate( $start, 0, true ); + + // parent::__construct() will set this to $date by default, but that may be different to $start now. + $first = empty( $first ) ? $start : $first; + + parent::__construct( $date, $recurrence, $first ); + } + + /** + * Calculate when an instance of this schedule would start based on a given + * date & time using its the CronExpression. + * + * @param DateTime $after + * @return DateTime + */ + protected function calculate_next( DateTime $after ) { + return $this->recurrence->getNextRunDate( $after, 0, false ); + } + + /** + * @return string + */ + public function get_recurrence() { + return strval( $this->recurrence ); + } + + /** + * Serialize cron schedules with data required prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, reccuring schedules used different property names to + * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To guard against the + * possibility of infinite loops if downgrading to Action Scheduler < 3.0.0, we need to + * also store the data with the old property names so if it's unserialized in AS < 3.0, + * the schedule doesn't end up with a null recurrence. + * + * @return array + */ + public function __sleep() { + + $sleep_params = parent::__sleep(); + + $this->start_timestamp = $this->scheduled_timestamp; + $this->cron = $this->recurrence; + + return array_merge( $sleep_params, array( + 'start_timestamp', + 'cron' + ) ); + } + + /** + * Unserialize cron schedules serialized/stored prior to AS 3.0.0 + * + * For more background, @see ActionScheduler_Abstract_RecurringSchedule::__wakeup(). + */ + public function __wakeup() { + if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->start_timestamp ) ) { + $this->scheduled_timestamp = $this->start_timestamp; + unset( $this->start_timestamp ); + } + + if ( is_null( $this->recurrence ) && ! is_null( $this->cron ) ) { + $this->recurrence = $this->cron; + unset( $this->cron ); + } + parent::__wakeup(); + } +} + diff --git a/packages/action-scheduler/classes/schedules/ActionScheduler_IntervalSchedule.php b/packages/action-scheduler/classes/schedules/ActionScheduler_IntervalSchedule.php new file mode 100644 index 0000000..11a591e --- /dev/null +++ b/packages/action-scheduler/classes/schedules/ActionScheduler_IntervalSchedule.php @@ -0,0 +1,81 @@ +__wakeup() for details. + **/ + private $start_timestamp = NULL; + + /** + * Deprecated property @see $this->__wakeup() for details. + **/ + private $interval_in_seconds = NULL; + + /** + * Calculate when this schedule should start after a given date & time using + * the number of seconds between recurrences. + * + * @param DateTime $after + * @return DateTime + */ + protected function calculate_next( DateTime $after ) { + $after->modify( '+' . (int) $this->get_recurrence() . ' seconds' ); + return $after; + } + + /** + * @return int + */ + public function interval_in_seconds() { + _deprecated_function( __METHOD__, '3.0.0', '(int)ActionScheduler_Abstract_RecurringSchedule::get_recurrence()' ); + return (int) $this->get_recurrence(); + } + + /** + * Serialize interval schedules with data required prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, reccuring schedules used different property names to + * refer to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To guard against the + * possibility of infinite loops if downgrading to Action Scheduler < 3.0.0, we need to + * also store the data with the old property names so if it's unserialized in AS < 3.0, + * the schedule doesn't end up with a null/false/0 recurrence. + * + * @return array + */ + public function __sleep() { + + $sleep_params = parent::__sleep(); + + $this->start_timestamp = $this->scheduled_timestamp; + $this->interval_in_seconds = $this->recurrence; + + return array_merge( $sleep_params, array( + 'start_timestamp', + 'interval_in_seconds' + ) ); + } + + /** + * Unserialize interval schedules serialized/stored prior to AS 3.0.0 + * + * For more background, @see ActionScheduler_Abstract_RecurringSchedule::__wakeup(). + */ + public function __wakeup() { + if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->start_timestamp ) ) { + $this->scheduled_timestamp = $this->start_timestamp; + unset( $this->start_timestamp ); + } + + if ( is_null( $this->recurrence ) && ! is_null( $this->interval_in_seconds ) ) { + $this->recurrence = $this->interval_in_seconds; + unset( $this->interval_in_seconds ); + } + parent::__wakeup(); + } +} diff --git a/packages/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php b/packages/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php new file mode 100644 index 0000000..0ca9f7c --- /dev/null +++ b/packages/action-scheduler/classes/schedules/ActionScheduler_NullSchedule.php @@ -0,0 +1,28 @@ +scheduled_date = null; + } + + /** + * This schedule has no scheduled DateTime, so we need to override the parent __sleep() + * @return array + */ + public function __sleep() { + return array(); + } + + public function __wakeup() { + $this->scheduled_date = null; + } +} diff --git a/packages/action-scheduler/classes/schedules/ActionScheduler_Schedule.php b/packages/action-scheduler/classes/schedules/ActionScheduler_Schedule.php new file mode 100644 index 0000000..d61a9f7 --- /dev/null +++ b/packages/action-scheduler/classes/schedules/ActionScheduler_Schedule.php @@ -0,0 +1,18 @@ +__wakeup() for details. + **/ + private $timestamp = NULL; + + /** + * @param DateTime $after + * + * @return DateTime|null + */ + public function calculate_next( DateTime $after ) { + return null; + } + + /** + * @return bool + */ + public function is_recurring() { + return false; + } + + /** + * Serialize schedule with data required prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, schedules used different property names to refer + * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To guard against the + * scheduled date for single actions always being seen as "now" if downgrading to + * Action Scheduler < 3.0.0, we need to also store the data with the old property names + * so if it's unserialized in AS < 3.0, the schedule doesn't end up with a null recurrence. + * + * @return array + */ + public function __sleep() { + + $sleep_params = parent::__sleep(); + + $this->timestamp = $this->scheduled_timestamp; + + return array_merge( $sleep_params, array( + 'timestamp', + ) ); + } + + /** + * Unserialize recurring schedules serialized/stored prior to AS 3.0.0 + * + * Prior to Action Scheduler 3.0.0, schedules used different property names to refer + * to equivalent data. For example, ActionScheduler_IntervalSchedule::start_timestamp + * was the same as ActionScheduler_SimpleSchedule::timestamp. Action Scheduler 3.0.0 + * aligned properties and property names for better inheritance. To maintain backward + * compatibility with schedules serialized and stored prior to 3.0, we need to correctly + * map the old property names with matching visibility. + */ + public function __wakeup() { + + if ( is_null( $this->scheduled_timestamp ) && ! is_null( $this->timestamp ) ) { + $this->scheduled_timestamp = $this->timestamp; + unset( $this->timestamp ); + } + parent::__wakeup(); + } +} diff --git a/packages/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php b/packages/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php new file mode 100644 index 0000000..af4aa5c --- /dev/null +++ b/packages/action-scheduler/classes/schema/ActionScheduler_LoggerSchema.php @@ -0,0 +1,90 @@ +tables = [ + self::LOG_TABLE, + ]; + } + + /** + * Performs additional setup work required to support this schema. + */ + public function init() { + add_action( 'action_scheduler_before_schema_update', array( $this, 'update_schema_3_0' ), 10, 2 ); + } + + protected function get_table_definition( $table ) { + global $wpdb; + $table_name = $wpdb->$table; + $charset_collate = $wpdb->get_charset_collate(); + switch ( $table ) { + + case self::LOG_TABLE: + + $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; + return "CREATE TABLE {$table_name} ( + log_id bigint(20) unsigned NOT NULL auto_increment, + action_id bigint(20) unsigned NOT NULL, + message text NOT NULL, + log_date_gmt datetime NULL default '${default_date}', + log_date_local datetime NULL default '${default_date}', + PRIMARY KEY (log_id), + KEY action_id (action_id), + KEY log_date_gmt (log_date_gmt) + ) $charset_collate"; + + default: + return ''; + } + } + + /** + * Update the logs table schema, allowing datetime fields to be NULL. + * + * This is needed because the NOT NULL constraint causes a conflict with some versions of MySQL + * configured with sql_mode=NO_ZERO_DATE, which can for instance lead to tables not being created. + * + * Most other schema updates happen via ActionScheduler_Abstract_Schema::update_table(), however + * that method relies on dbDelta() and this change is not possible when using that function. + * + * @param string $table Name of table being updated. + * @param string $db_version The existing schema version of the table. + */ + public function update_schema_3_0( $table, $db_version ) { + global $wpdb; + + if ( 'actionscheduler_logs' !== $table || version_compare( $db_version, '3', '>=' ) ) { + return; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $table_name = $wpdb->prefix . 'actionscheduler_logs'; + $table_list = $wpdb->get_col( "SHOW TABLES LIKE '${table_name}'" ); + $default_date = ActionScheduler_StoreSchema::DEFAULT_DATE; + + if ( ! empty( $table_list ) ) { + $query = " + ALTER TABLE ${table_name} + MODIFY COLUMN log_date_gmt datetime NULL default '${default_date}', + MODIFY COLUMN log_date_local datetime NULL default '${default_date}' + "; + $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } +} diff --git a/packages/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php b/packages/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php new file mode 100644 index 0000000..b1c64cd --- /dev/null +++ b/packages/action-scheduler/classes/schema/ActionScheduler_StoreSchema.php @@ -0,0 +1,130 @@ +tables = [ + self::ACTIONS_TABLE, + self::CLAIMS_TABLE, + self::GROUPS_TABLE, + ]; + } + + /** + * Performs additional setup work required to support this schema. + */ + public function init() { + add_action( 'action_scheduler_before_schema_update', array( $this, 'update_schema_5_0' ), 10, 2 ); + } + + protected function get_table_definition( $table ) { + global $wpdb; + $table_name = $wpdb->$table; + $charset_collate = $wpdb->get_charset_collate(); + $max_index_length = 191; // @see wp_get_db_schema() + $default_date = self::DEFAULT_DATE; + switch ( $table ) { + + case self::ACTIONS_TABLE: + + return "CREATE TABLE {$table_name} ( + action_id bigint(20) unsigned NOT NULL auto_increment, + hook varchar(191) NOT NULL, + status varchar(20) NOT NULL, + scheduled_date_gmt datetime NULL default '${default_date}', + scheduled_date_local datetime NULL default '${default_date}', + args varchar($max_index_length), + schedule longtext, + group_id bigint(20) unsigned NOT NULL default '0', + attempts int(11) NOT NULL default '0', + last_attempt_gmt datetime NULL default '${default_date}', + last_attempt_local datetime NULL default '${default_date}', + claim_id bigint(20) unsigned NOT NULL default '0', + extended_args varchar(8000) DEFAULT NULL, + PRIMARY KEY (action_id), + KEY hook (hook($max_index_length)), + KEY status (status), + KEY scheduled_date_gmt (scheduled_date_gmt), + KEY args (args($max_index_length)), + KEY group_id (group_id), + KEY last_attempt_gmt (last_attempt_gmt), + KEY claim_id (claim_id), + KEY `claim_id_status_scheduled_date_gmt` (`claim_id`, `status`, `scheduled_date_gmt`) + ) $charset_collate"; + + case self::CLAIMS_TABLE: + + return "CREATE TABLE {$table_name} ( + claim_id bigint(20) unsigned NOT NULL auto_increment, + date_created_gmt datetime NULL default '${default_date}', + PRIMARY KEY (claim_id), + KEY date_created_gmt (date_created_gmt) + ) $charset_collate"; + + case self::GROUPS_TABLE: + + return "CREATE TABLE {$table_name} ( + group_id bigint(20) unsigned NOT NULL auto_increment, + slug varchar(255) NOT NULL, + PRIMARY KEY (group_id), + KEY slug (slug($max_index_length)) + ) $charset_collate"; + + default: + return ''; + } + } + + /** + * Update the actions table schema, allowing datetime fields to be NULL. + * + * This is needed because the NOT NULL constraint causes a conflict with some versions of MySQL + * configured with sql_mode=NO_ZERO_DATE, which can for instance lead to tables not being created. + * + * Most other schema updates happen via ActionScheduler_Abstract_Schema::update_table(), however + * that method relies on dbDelta() and this change is not possible when using that function. + * + * @param string $table Name of table being updated. + * @param string $db_version The existing schema version of the table. + */ + public function update_schema_5_0( $table, $db_version ) { + global $wpdb; + + if ( 'actionscheduler_actions' !== $table || version_compare( $db_version, '5', '>=' ) ) { + return; + } + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $table_name = $wpdb->prefix . 'actionscheduler_actions'; + $table_list = $wpdb->get_col( "SHOW TABLES LIKE '${table_name}'" ); + $default_date = self::DEFAULT_DATE; + + if ( ! empty( $table_list ) ) { + $query = " + ALTER TABLE ${table_name} + MODIFY COLUMN scheduled_date_gmt datetime NULL default '${default_date}', + MODIFY COLUMN scheduled_date_local datetime NULL default '${default_date}', + MODIFY COLUMN last_attempt_gmt datetime NULL default '${default_date}', + MODIFY COLUMN last_attempt_local datetime NULL default '${default_date}' + "; + $wpdb->query( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + } + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } +} diff --git a/packages/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php b/packages/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php new file mode 100644 index 0000000..dac17aa --- /dev/null +++ b/packages/action-scheduler/deprecated/ActionScheduler_Abstract_QueueRunner_Deprecated.php @@ -0,0 +1,27 @@ +get_date(); + $replacement_method = 'get_date()'; + } else { + $return_value = $this->get_next( $after ); + $replacement_method = 'get_next( $after )'; + } + + _deprecated_function( __METHOD__, '3.0.0', __CLASS__ . '::' . $replacement_method ); + + return $return_value; + } +} diff --git a/packages/action-scheduler/deprecated/ActionScheduler_Store_Deprecated.php b/packages/action-scheduler/deprecated/ActionScheduler_Store_Deprecated.php new file mode 100644 index 0000000..002dc75 --- /dev/null +++ b/packages/action-scheduler/deprecated/ActionScheduler_Store_Deprecated.php @@ -0,0 +1,49 @@ +mark_failure( $action_id ); + } + + /** + * Add base hooks + * + * @since 2.2.6 + */ + protected static function hook() { + _deprecated_function( __METHOD__, '3.0.0' ); + } + + /** + * Remove base hooks + * + * @since 2.2.6 + */ + protected static function unhook() { + _deprecated_function( __METHOD__, '3.0.0' ); + } + + /** + * Get the site's local time. + * + * @deprecated 2.1.0 + * @return DateTimeZone + */ + protected function get_local_timezone() { + _deprecated_function( __FUNCTION__, '2.1.0', 'ActionScheduler_TimezoneHelper::set_local_timezone()' ); + return ActionScheduler_TimezoneHelper::get_local_timezone(); + } +} diff --git a/packages/action-scheduler/deprecated/functions.php b/packages/action-scheduler/deprecated/functions.php new file mode 100644 index 0000000..f782c4b --- /dev/null +++ b/packages/action-scheduler/deprecated/functions.php @@ -0,0 +1,126 @@ + '' - the name of the action that will be triggered + * 'args' => NULL - the args array that will be passed with the action + * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'group' => '' - the group the action belongs to + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING + * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID + * 'per_page' => 5 - Number of results to return + * 'offset' => 0 + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', or 'date' + * 'order' => 'ASC' + * @param string $return_format OBJECT, ARRAY_A, or ids + * + * @deprecated 2.1.0 + * + * @return array + */ +function wc_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { + _deprecated_function( __FUNCTION__, '2.1.0', 'as_get_scheduled_actions()' ); + return as_get_scheduled_actions( $args, $return_format ); +} diff --git a/packages/action-scheduler/functions.php b/packages/action-scheduler/functions.php new file mode 100644 index 0000000..5f05546 --- /dev/null +++ b/packages/action-scheduler/functions.php @@ -0,0 +1,319 @@ +async( $hook, $args, $group ); +} + +/** + * Schedule an action to run one time + * + * @param int $timestamp When the job will run. + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * + * @return int The action ID. + */ +function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + return ActionScheduler::factory()->single( $hook, $args, $timestamp, $group ); +} + +/** + * Schedule a recurring action + * + * @param int $timestamp When the first instance of the job will run. + * @param int $interval_in_seconds How long to wait between runs. + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * + * @return int The action ID. + */ +function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + return ActionScheduler::factory()->recurring( $hook, $args, $timestamp, $interval_in_seconds, $group ); +} + +/** + * Schedule an action that recurs on a cron-like schedule. + * + * @param int $base_timestamp The first instance of the action will be scheduled + * to run at a time calculated after this timestamp matching the cron + * expression. This can be used to delay the first instance of the action. + * @param string $schedule A cron-link schedule string + * @see http://en.wikipedia.org/wiki/Cron + * * * * * * * + * ┬ ┬ ┬ ┬ ┬ ┬ + * | | | | | | + * | | | | | + year [optional] + * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) + * | | | +---------- month (1 - 12) + * | | +--------------- day of month (1 - 31) + * | +-------------------- hour (0 - 23) + * +------------------------- min (0 - 59) + * @param string $hook The hook to trigger. + * @param array $args Arguments to pass when the hook triggers. + * @param string $group The group to assign this job to. + * + * @return int The action ID. + */ +function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + return ActionScheduler::factory()->cron( $hook, $args, $timestamp, $schedule, $group ); +} + +/** + * Cancel the next occurrence of a scheduled action. + * + * While only the next instance of a recurring or cron action is unscheduled by this method, that will also prevent + * all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled in + * a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled + * only after the former action is run. If the next instance is never run, because it's unscheduled by this function, + * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled + * by this method also. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Args that would have been passed to the job. + * @param string $group The group the job is assigned to. + * + * @return string|null The scheduled action ID if a scheduled action was found, or null if no matching action found. + */ +function as_unschedule_action( $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return 0; + } + $params = array( + 'hook' => $hook, + 'status' => ActionScheduler_Store::STATUS_PENDING, + 'orderby' => 'date', + 'order' => 'ASC', + 'group' => $group, + ); + if ( is_array( $args ) ) { + $params['args'] = $args; + } + + $action_id = ActionScheduler::store()->query_action( $params ); + if ( $action_id ) { + ActionScheduler::store()->cancel_action( $action_id ); + } + + return $action_id; +} + +/** + * Cancel all occurrences of a scheduled action. + * + * @param string $hook The hook that the job will trigger. + * @param array $args Args that would have been passed to the job. + * @param string $group The group the job is assigned to. + */ +function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return; + } + if ( empty( $args ) ) { + if ( ! empty( $hook ) && empty( $group ) ) { + ActionScheduler_Store::instance()->cancel_actions_by_hook( $hook ); + return; + } + if ( ! empty( $group ) && empty( $hook ) ) { + ActionScheduler_Store::instance()->cancel_actions_by_group( $group ); + return; + } + } + do { + $unscheduled_action = as_unschedule_action( $hook, $args, $group ); + } while ( ! empty( $unscheduled_action ) ); +} + +/** + * Check if there is an existing action in the queue with a given hook, args and group combination. + * + * An action in the queue could be pending, in-progress or async. If the is pending for a time in + * future, its scheduled date will be returned as a timestamp. If it is currently being run, or an + * async action sitting in the queue waiting to be processed, in which case boolean true will be + * returned. Or there may be no async, in-progress or pending action for this hook, in which case, + * boolean false will be the return value. + * + * @param string $hook + * @param array $args + * @param string $group + * + * @return int|bool The timestamp for the next occurrence of a pending scheduled action, true for an async or in-progress action or false if there is no matching action. + */ +function as_next_scheduled_action( $hook, $args = null, $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return false; + } + + $params = array( + 'hook' => $hook, + 'orderby' => 'date', + 'order' => 'ASC', + 'group' => $group, + ); + + if ( is_array( $args ) ) { + $params['args'] = $args; + } + + $params['status'] = ActionScheduler_Store::STATUS_RUNNING; + $action_id = ActionScheduler::store()->query_action( $params ); + if ( $action_id ) { + return true; + } + + $params['status'] = ActionScheduler_Store::STATUS_PENDING; + $action_id = ActionScheduler::store()->query_action( $params ); + if ( null === $action_id ) { + return false; + } + + $action = ActionScheduler::store()->fetch_action( $action_id ); + $scheduled_date = $action->get_schedule()->get_date(); + if ( $scheduled_date ) { + return (int) $scheduled_date->format( 'U' ); + } elseif ( null === $scheduled_date ) { // pending async action with NullSchedule + return true; + } + + return false; +} + +/** + * Check if there is a scheduled action in the queue but more efficiently than as_next_scheduled_action(). + * + * It's recommended to use this function when you need to know whether a specific action is currently scheduled + * (pending or in-progress). + * + * @since x.x.x + * + * @param string $hook The hook of the action. + * @param array $args Args that have been passed to the action. Null will matches any args. + * @param string $group The group the job is assigned to. + * + * @return bool True if a matching action is pending or in-progress, false otherwise. + */ +function as_has_scheduled_action( $hook, $args = null, $group = '' ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return false; + } + + $query_args = array( + 'hook' => $hook, + 'status' => array( ActionScheduler_Store::STATUS_RUNNING, ActionScheduler_Store::STATUS_PENDING ), + 'group' => $group, + 'orderby' => 'none', + ); + + if ( null !== $args ) { + $query_args['args'] = $args; + } + + $action_id = ActionScheduler::store()->query_action( $query_args ); + + return $action_id !== null; +} + +/** + * Find scheduled actions + * + * @param array $args Possible arguments, with their default values: + * 'hook' => '' - the name of the action that will be triggered + * 'args' => NULL - the args array that will be passed with the action + * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. + * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '=' + * 'group' => '' - the group the action belongs to + * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING + * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID + * 'per_page' => 5 - Number of results to return + * 'offset' => 0 + * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', 'date' or 'none' + * 'order' => 'ASC' + * + * @param string $return_format OBJECT, ARRAY_A, or ids. + * + * @return array + */ +function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { + if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { + return array(); + } + $store = ActionScheduler::store(); + foreach ( array('date', 'modified') as $key ) { + if ( isset($args[$key]) ) { + $args[$key] = as_get_datetime_object($args[$key]); + } + } + $ids = $store->query_actions( $args ); + + if ( $return_format == 'ids' || $return_format == 'int' ) { + return $ids; + } + + $actions = array(); + foreach ( $ids as $action_id ) { + $actions[$action_id] = $store->fetch_action( $action_id ); + } + + if ( $return_format == ARRAY_A ) { + foreach ( $actions as $action_id => $action_object ) { + $actions[$action_id] = get_object_vars($action_object); + } + } + + return $actions; +} + +/** + * Helper function to create an instance of DateTime based on a given + * string and timezone. By default, will return the current date/time + * in the UTC timezone. + * + * Needed because new DateTime() called without an explicit timezone + * will create a date/time in PHP's timezone, but we need to have + * assurance that a date/time uses the right timezone (which we almost + * always want to be UTC), which means we need to always include the + * timezone when instantiating datetimes rather than leaving it up to + * the PHP default. + * + * @param mixed $date_string A date/time string. Valid formats are explained in http://php.net/manual/en/datetime.formats.php. + * @param string $timezone A timezone identifier, like UTC or Europe/Lisbon. The list of valid identifiers is available http://php.net/manual/en/timezones.php. + * + * @return ActionScheduler_DateTime + */ +function as_get_datetime_object( $date_string = null, $timezone = 'UTC' ) { + if ( is_object( $date_string ) && $date_string instanceof DateTime ) { + $date = new ActionScheduler_DateTime( $date_string->format( 'Y-m-d H:i:s' ), new DateTimeZone( $timezone ) ); + } elseif ( is_numeric( $date_string ) ) { + $date = new ActionScheduler_DateTime( '@' . $date_string, new DateTimeZone( $timezone ) ); + } else { + $date = new ActionScheduler_DateTime( $date_string, new DateTimeZone( $timezone ) ); + } + return $date; +} diff --git a/packages/action-scheduler/lib/WP_Async_Request.php b/packages/action-scheduler/lib/WP_Async_Request.php new file mode 100644 index 0000000..d7dea1c --- /dev/null +++ b/packages/action-scheduler/lib/WP_Async_Request.php @@ -0,0 +1,170 @@ +identifier = $this->prefix . '_' . $this->action; + + add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); + add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); + } + + /** + * Set data used during the request + * + * @param array $data Data. + * + * @return $this + */ + public function data( $data ) { + $this->data = $data; + + return $this; + } + + /** + * Dispatch the async request + * + * @return array|WP_Error + */ + public function dispatch() { + $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); + $args = $this->get_post_args(); + + return wp_remote_post( esc_url_raw( $url ), $args ); + } + + /** + * Get query args + * + * @return array + */ + protected function get_query_args() { + if ( property_exists( $this, 'query_args' ) ) { + return $this->query_args; + } + + return array( + 'action' => $this->identifier, + 'nonce' => wp_create_nonce( $this->identifier ), + ); + } + + /** + * Get query URL + * + * @return string + */ + protected function get_query_url() { + if ( property_exists( $this, 'query_url' ) ) { + return $this->query_url; + } + + return admin_url( 'admin-ajax.php' ); + } + + /** + * Get post args + * + * @return array + */ + protected function get_post_args() { + if ( property_exists( $this, 'post_args' ) ) { + return $this->post_args; + } + + return array( + 'timeout' => 0.01, + 'blocking' => false, + 'body' => $this->data, + 'cookies' => $_COOKIE, + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ); + } + + /** + * Maybe handle + * + * Check for correct nonce and pass to handler. + */ + public function maybe_handle() { + // Don't lock up other requests while processing + session_write_close(); + + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); + } + + /** + * Handle + * + * Override this method to perform any actions required + * during the async request. + */ + abstract protected function handle(); + + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression.php b/packages/action-scheduler/lib/cron-expression/CronExpression.php new file mode 100755 index 0000000..7f33c37 --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression.php @@ -0,0 +1,318 @@ + + * @link http://en.wikipedia.org/wiki/Cron + */ +class CronExpression +{ + const MINUTE = 0; + const HOUR = 1; + const DAY = 2; + const MONTH = 3; + const WEEKDAY = 4; + const YEAR = 5; + + /** + * @var array CRON expression parts + */ + private $cronParts; + + /** + * @var CronExpression_FieldFactory CRON field factory + */ + private $fieldFactory; + + /** + * @var array Order in which to test of cron parts + */ + private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE); + + /** + * Factory method to create a new CronExpression. + * + * @param string $expression The CRON expression to create. There are + * several special predefined values which can be used to substitute the + * CRON expression: + * + * @yearly, @annually) - Run once a year, midnight, Jan. 1 - 0 0 1 1 * + * @monthly - Run once a month, midnight, first of month - 0 0 1 * * + * @weekly - Run once a week, midnight on Sun - 0 0 * * 0 + * @daily - Run once a day, midnight - 0 0 * * * + * @hourly - Run once an hour, first minute - 0 * * * * + * +*@param CronExpression_FieldFactory $fieldFactory (optional) Field factory to use + * + * @return CronExpression + */ + public static function factory($expression, CronExpression_FieldFactory $fieldFactory = null) + { + $mappings = array( + '@yearly' => '0 0 1 1 *', + '@annually' => '0 0 1 1 *', + '@monthly' => '0 0 1 * *', + '@weekly' => '0 0 * * 0', + '@daily' => '0 0 * * *', + '@hourly' => '0 * * * *' + ); + + if (isset($mappings[$expression])) { + $expression = $mappings[$expression]; + } + + return new self($expression, $fieldFactory ? $fieldFactory : new CronExpression_FieldFactory()); + } + + /** + * Parse a CRON expression + * + * @param string $expression CRON expression (e.g. '8 * * * *') + * @param CronExpression_FieldFactory $fieldFactory Factory to create cron fields + */ + public function __construct($expression, CronExpression_FieldFactory $fieldFactory) + { + $this->fieldFactory = $fieldFactory; + $this->setExpression($expression); + } + + /** + * Set or change the CRON expression + * + * @param string $value CRON expression (e.g. 8 * * * *) + * + * @return CronExpression + * @throws InvalidArgumentException if not a valid CRON expression + */ + public function setExpression($value) + { + $this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); + if (count($this->cronParts) < 5) { + throw new InvalidArgumentException( + $value . ' is not a valid CRON expression' + ); + } + + foreach ($this->cronParts as $position => $part) { + $this->setPart($position, $part); + } + + return $this; + } + + /** + * Set part of the CRON expression + * + * @param int $position The position of the CRON expression to set + * @param string $value The value to set + * + * @return CronExpression + * @throws InvalidArgumentException if the value is not valid for the part + */ + public function setPart($position, $value) + { + if (!$this->fieldFactory->getField($position)->validate($value)) { + throw new InvalidArgumentException( + 'Invalid CRON field value ' . $value . ' as position ' . $position + ); + } + + $this->cronParts[$position] = $value; + + return $this; + } + + /** + * Get a next run date relative to the current date or a specific date + * + * @param string|DateTime $currentTime (optional) Relative calculation date + * @param int $nth (optional) Number of matches to skip before returning a + * matching next run date. 0, the default, will return the current + * date and time if the next run date falls on the current date and + * time. Setting this value to 1 will skip the first match and go to + * the second match. Setting this value to 2 will skip the first 2 + * matches and so on. + * @param bool $allowCurrentDate (optional) Set to TRUE to return the + * current date if it matches the cron expression + * + * @return DateTime + * @throws RuntimeException on too many iterations + */ + public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) + { + return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate); + } + + /** + * Get a previous run date relative to the current date or a specific date + * + * @param string|DateTime $currentTime (optional) Relative calculation date + * @param int $nth (optional) Number of matches to skip before returning + * @param bool $allowCurrentDate (optional) Set to TRUE to return the + * current date if it matches the cron expression + * + * @return DateTime + * @throws RuntimeException on too many iterations + * @see CronExpression::getNextRunDate + */ + public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) + { + return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate); + } + + /** + * Get multiple run dates starting at the current date or a specific date + * + * @param int $total Set the total number of dates to calculate + * @param string|DateTime $currentTime (optional) Relative calculation date + * @param bool $invert (optional) Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate (optional) Set to TRUE to return the + * current date if it matches the cron expression + * + * @return array Returns an array of run dates + */ + public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false) + { + $matches = array(); + for ($i = 0; $i < max(0, $total); $i++) { + $matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate); + } + + return $matches; + } + + /** + * Get all or part of the CRON expression + * + * @param string $part (optional) Specify the part to retrieve or NULL to + * get the full cron schedule string. + * + * @return string|null Returns the CRON expression, a part of the + * CRON expression, or NULL if the part was specified but not found + */ + public function getExpression($part = null) + { + if (null === $part) { + return implode(' ', $this->cronParts); + } elseif (array_key_exists($part, $this->cronParts)) { + return $this->cronParts[$part]; + } + + return null; + } + + /** + * Helper method to output the full expression. + * + * @return string Full CRON expression + */ + public function __toString() + { + return $this->getExpression(); + } + + /** + * Determine if the cron is due to run based on the current date or a + * specific date. This method assumes that the current number of + * seconds are irrelevant, and should be called once per minute. + * + * @param string|DateTime $currentTime (optional) Relative calculation date + * + * @return bool Returns TRUE if the cron is due to run or FALSE if not + */ + public function isDue($currentTime = 'now') + { + if ('now' === $currentTime) { + $currentDate = date('Y-m-d H:i'); + $currentTime = strtotime($currentDate); + } elseif ($currentTime instanceof DateTime) { + $currentDate = $currentTime->format('Y-m-d H:i'); + $currentTime = strtotime($currentDate); + } else { + $currentTime = new DateTime($currentTime); + $currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0); + $currentDate = $currentTime->format('Y-m-d H:i'); + $currentTime = (int)($currentTime->getTimestamp()); + } + + return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime; + } + + /** + * Get the next or previous run date of the expression relative to a date + * + * @param string|DateTime $currentTime (optional) Relative calculation date + * @param int $nth (optional) Number of matches to skip before returning + * @param bool $invert (optional) Set to TRUE to go backwards in time + * @param bool $allowCurrentDate (optional) Set to TRUE to return the + * current date if it matches the cron expression + * + * @return DateTime + * @throws RuntimeException on too many iterations + */ + protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false) + { + if ($currentTime instanceof DateTime) { + $currentDate = $currentTime; + } else { + $currentDate = new DateTime($currentTime ? $currentTime : 'now'); + $currentDate->setTimezone(new DateTimeZone(date_default_timezone_get())); + } + + $currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0); + $nextRun = clone $currentDate; + $nth = (int) $nth; + + // Set a hard limit to bail on an impossible date + for ($i = 0; $i < 1000; $i++) { + + foreach (self::$order as $position) { + $part = $this->getExpression($position); + if (null === $part) { + continue; + } + + $satisfied = false; + // Get the field object used to validate this part + $field = $this->fieldFactory->getField($position); + // Check if this is singular or a list + if (strpos($part, ',') === false) { + $satisfied = $field->isSatisfiedBy($nextRun, $part); + } else { + foreach (array_map('trim', explode(',', $part)) as $listPart) { + if ($field->isSatisfiedBy($nextRun, $listPart)) { + $satisfied = true; + break; + } + } + } + + // If the field is not satisfied, then start over + if (!$satisfied) { + $field->increment($nextRun, $invert); + continue 2; + } + } + + // Skip this match if needed + if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { + $this->fieldFactory->getField(0)->increment($nextRun, $invert); + continue; + } + + return $nextRun; + } + + // @codeCoverageIgnoreStart + throw new RuntimeException('Impossible CRON expression'); + // @codeCoverageIgnoreEnd + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php b/packages/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php new file mode 100755 index 0000000..f8d5c00 --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_AbstractField.php @@ -0,0 +1,100 @@ + + */ +abstract class CronExpression_AbstractField implements CronExpression_FieldInterface +{ + /** + * Check to see if a field is satisfied by a value + * + * @param string $dateValue Date value to check + * @param string $value Value to test + * + * @return bool + */ + public function isSatisfied($dateValue, $value) + { + if ($this->isIncrementsOfRanges($value)) { + return $this->isInIncrementsOfRanges($dateValue, $value); + } elseif ($this->isRange($value)) { + return $this->isInRange($dateValue, $value); + } + + return $value == '*' || $dateValue == $value; + } + + /** + * Check if a value is a range + * + * @param string $value Value to test + * + * @return bool + */ + public function isRange($value) + { + return strpos($value, '-') !== false; + } + + /** + * Check if a value is an increments of ranges + * + * @param string $value Value to test + * + * @return bool + */ + public function isIncrementsOfRanges($value) + { + return strpos($value, '/') !== false; + } + + /** + * Test if a value is within a range + * + * @param string $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInRange($dateValue, $value) + { + $parts = array_map('trim', explode('-', $value, 2)); + + return $dateValue >= $parts[0] && $dateValue <= $parts[1]; + } + + /** + * Test if a value is within an increments of ranges (offset[-to]/step size) + * + * @param string $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInIncrementsOfRanges($dateValue, $value) + { + $parts = array_map('trim', explode('/', $value, 2)); + $stepSize = isset($parts[1]) ? $parts[1] : 0; + if ($parts[0] == '*' || $parts[0] === '0') { + return (int) $dateValue % $stepSize == 0; + } + + $range = explode('-', $parts[0], 2); + $offset = $range[0]; + $to = isset($range[1]) ? $range[1] : $dateValue; + // Ensure that the date value is within the range + if ($dateValue < $offset || $dateValue > $to) { + return false; + } + + for ($i = $offset; $i <= $to; $i+= $stepSize) { + if ($i == $dateValue) { + return true; + } + } + + return false; + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php b/packages/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php new file mode 100755 index 0000000..40c1d6c --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_DayOfMonthField.php @@ -0,0 +1,110 @@ + + */ +class CronExpression_DayOfMonthField extends CronExpression_AbstractField +{ + /** + * Get the nearest day of the week for a given day in a month + * + * @param int $currentYear Current year + * @param int $currentMonth Current month + * @param int $targetDay Target day of the month + * + * @return DateTime Returns the nearest date + */ + private static function getNearestWeekday($currentYear, $currentMonth, $targetDay) + { + $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT); + $target = new DateTime("$currentYear-$currentMonth-$tday"); + $currentWeekday = (int) $target->format('N'); + + if ($currentWeekday < 6) { + return $target; + } + + $lastDayOfMonth = $target->format('t'); + + foreach (array(-1, 1, -2, 2) as $i) { + $adjusted = $targetDay + $i; + if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { + $target->setDate($currentYear, $currentMonth, $adjusted); + if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { + return $target; + } + } + } + } + + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + // ? states that the field value is to be skipped + if ($value == '?') { + return true; + } + + $fieldValue = $date->format('d'); + + // Check to see if this is the last day of the month + if ($value == 'L') { + return $fieldValue == $date->format('t'); + } + + // Check to see if this is the nearest weekday to a particular value + if (strpos($value, 'W')) { + // Parse the target day + $targetDay = substr($value, 0, strpos($value, 'W')); + // Find out if the current day is the nearest day of the week + return $date->format('j') == self::getNearestWeekday( + $date->format('Y'), + $date->format('m'), + $targetDay + )->format('j'); + } + + return $this->isSatisfied($date->format('d'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('previous day'); + $date->setTime(23, 59); + } else { + $date->modify('next day'); + $date->setTime(0, 0); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-\?LW0-9A-Za-z]+/', $value); + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php b/packages/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php new file mode 100755 index 0000000..e9f68a7 --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_DayOfWeekField.php @@ -0,0 +1,124 @@ + + */ +class CronExpression_DayOfWeekField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + if ($value == '?') { + return true; + } + + // Convert text day of the week values to integers + $value = str_ireplace( + array('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'), + range(0, 6), + $value + ); + + $currentYear = $date->format('Y'); + $currentMonth = $date->format('m'); + $lastDayOfMonth = $date->format('t'); + + // Find out if this is the last specific weekday of the month + if (strpos($value, 'L')) { + $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L'))); + $tdate = clone $date; + $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth); + while ($tdate->format('w') != $weekday) { + $tdate->setDate($currentYear, $currentMonth, --$lastDayOfMonth); + } + + return $date->format('j') == $lastDayOfMonth; + } + + // Handle # hash tokens + if (strpos($value, '#')) { + list($weekday, $nth) = explode('#', $value); + // Validate the hash fields + if ($weekday < 1 || $weekday > 5) { + throw new InvalidArgumentException("Weekday must be a value between 1 and 5. {$weekday} given"); + } + if ($nth > 5) { + throw new InvalidArgumentException('There are never more than 5 of a given weekday in a month'); + } + // The current weekday must match the targeted weekday to proceed + if ($date->format('N') != $weekday) { + return false; + } + + $tdate = clone $date; + $tdate->setDate($currentYear, $currentMonth, 1); + $dayCount = 0; + $currentDay = 1; + while ($currentDay < $lastDayOfMonth + 1) { + if ($tdate->format('N') == $weekday) { + if (++$dayCount >= $nth) { + break; + } + } + $tdate->setDate($currentYear, $currentMonth, ++$currentDay); + } + + return $date->format('j') == $currentDay; + } + + // Handle day of the week values + if (strpos($value, '-')) { + $parts = explode('-', $value); + if ($parts[0] == '7') { + $parts[0] = '0'; + } elseif ($parts[1] == '0') { + $parts[1] = '7'; + } + $value = implode('-', $parts); + } + + // Test to see which Sunday to use -- 0 == 7 == Sunday + $format = in_array(7, str_split($value)) ? 'N' : 'w'; + $fieldValue = $date->format($format); + + return $this->isSatisfied($fieldValue, $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('-1 day'); + $date->setTime(23, 59, 0); + } else { + $date->modify('+1 day'); + $date->setTime(0, 0, 0); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value); + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php b/packages/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php new file mode 100755 index 0000000..556ba1a --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_FieldFactory.php @@ -0,0 +1,55 @@ + + * @link http://en.wikipedia.org/wiki/Cron + */ +class CronExpression_FieldFactory +{ + /** + * @var array Cache of instantiated fields + */ + private $fields = array(); + + /** + * Get an instance of a field object for a cron expression position + * + * @param int $position CRON expression position value to retrieve + * + * @return CronExpression_FieldInterface + * @throws InvalidArgumentException if a position is not valid + */ + public function getField($position) + { + if (!isset($this->fields[$position])) { + switch ($position) { + case 0: + $this->fields[$position] = new CronExpression_MinutesField(); + break; + case 1: + $this->fields[$position] = new CronExpression_HoursField(); + break; + case 2: + $this->fields[$position] = new CronExpression_DayOfMonthField(); + break; + case 3: + $this->fields[$position] = new CronExpression_MonthField(); + break; + case 4: + $this->fields[$position] = new CronExpression_DayOfWeekField(); + break; + case 5: + $this->fields[$position] = new CronExpression_YearField(); + break; + default: + throw new InvalidArgumentException( + $position . ' is not a valid position' + ); + } + } + + return $this->fields[$position]; + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php b/packages/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php new file mode 100755 index 0000000..5d5109b --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_FieldInterface.php @@ -0,0 +1,39 @@ + + */ +interface CronExpression_FieldInterface +{ + /** + * Check if the respective value of a DateTime field satisfies a CRON exp + * + * @param DateTime $date DateTime object to check + * @param string $value CRON expression to test against + * + * @return bool Returns TRUE if satisfied, FALSE otherwise + */ + public function isSatisfiedBy(DateTime $date, $value); + + /** + * When a CRON expression is not satisfied, this method is used to increment + * or decrement a DateTime object by the unit of the cron field + * + * @param DateTime $date DateTime object to change + * @param bool $invert (optional) Set to TRUE to decrement + * + * @return CronExpression_FieldInterface + */ + public function increment(DateTime $date, $invert = false); + + /** + * Validates a CRON expression for a given field + * + * @param string $value CRON expression value to validate + * + * @return bool Returns TRUE if valid, FALSE otherwise + */ + public function validate($value); +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_HoursField.php b/packages/action-scheduler/lib/cron-expression/CronExpression_HoursField.php new file mode 100755 index 0000000..088ca73 --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_HoursField.php @@ -0,0 +1,47 @@ + + */ +class CronExpression_HoursField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + return $this->isSatisfied($date->format('H'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + // Change timezone to UTC temporarily. This will + // allow us to go back or forwards and hour even + // if DST will be changed between the hours. + $timezone = $date->getTimezone(); + $date->setTimezone(new DateTimeZone('UTC')); + if ($invert) { + $date->modify('-1 hour'); + $date->setTime($date->format('H'), 59); + } else { + $date->modify('+1 hour'); + $date->setTime($date->format('H'), 0); + } + $date->setTimezone($timezone); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9]+/', $value); + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php b/packages/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php new file mode 100755 index 0000000..436acf2 --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_MinutesField.php @@ -0,0 +1,39 @@ + + */ +class CronExpression_MinutesField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + return $this->isSatisfied($date->format('i'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('-1 minute'); + } else { + $date->modify('+1 minute'); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9]+/', $value); + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_MonthField.php b/packages/action-scheduler/lib/cron-expression/CronExpression_MonthField.php new file mode 100755 index 0000000..d3deb12 --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_MonthField.php @@ -0,0 +1,55 @@ + + */ +class CronExpression_MonthField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + // Convert text month values to integers + $value = str_ireplace( + array( + 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', + 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC' + ), + range(1, 12), + $value + ); + + return $this->isSatisfied($date->format('m'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + // $date->modify('last day of previous month'); // remove for php 5.2 compat + $date->modify('previous month'); + $date->modify($date->format('Y-m-t')); + $date->setTime(23, 59); + } else { + //$date->modify('first day of next month'); // remove for php 5.2 compat + $date->modify('next month'); + $date->modify($date->format('Y-m-01')); + $date->setTime(0, 0); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9A-Z]+/', $value); + } +} diff --git a/packages/action-scheduler/lib/cron-expression/CronExpression_YearField.php b/packages/action-scheduler/lib/cron-expression/CronExpression_YearField.php new file mode 100755 index 0000000..f11562e --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/CronExpression_YearField.php @@ -0,0 +1,43 @@ + + */ +class CronExpression_YearField extends CronExpression_AbstractField +{ + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTime $date, $value) + { + return $this->isSatisfied($date->format('Y'), $value); + } + + /** + * {@inheritdoc} + */ + public function increment(DateTime $date, $invert = false) + { + if ($invert) { + $date->modify('-1 year'); + $date->setDate($date->format('Y'), 12, 31); + $date->setTime(23, 59, 0); + } else { + $date->modify('+1 year'); + $date->setDate($date->format('Y'), 1, 1); + $date->setTime(0, 0, 0); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + return (bool) preg_match('/[\*,\/\-0-9]+/', $value); + } +} diff --git a/packages/action-scheduler/lib/cron-expression/LICENSE b/packages/action-scheduler/lib/cron-expression/LICENSE new file mode 100755 index 0000000..c6d88ac --- /dev/null +++ b/packages/action-scheduler/lib/cron-expression/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Michael Dowling and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/action-scheduler/license.txt b/packages/action-scheduler/license.txt new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/packages/action-scheduler/license.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/packages/action-scheduler/readme.txt b/packages/action-scheduler/readme.txt new file mode 100644 index 0000000..9f6e238 --- /dev/null +++ b/packages/action-scheduler/readme.txt @@ -0,0 +1,76 @@ +=== Action Scheduler === +Contributors: Automattic, wpmuguru, claudiosanches, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, royho, barryhughes-1 +Tags: scheduler, cron +Requires at least: 5.2 +Tested up to: 5.7 +Stable tag: 3.3.0 +License: GPLv3 +Requires PHP: 5.6 + +Action Scheduler - Job Queue for WordPress + +== Description == + +Action Scheduler is a scalable, traceable job queue for background processing large sets of actions in WordPress. It's specially designed to be distributed in WordPress plugins. + +Action Scheduler works by triggering an action hook to run at some time in the future. Each hook can be scheduled with unique data, to allow callbacks to perform operations on that data. The hook can also be scheduled to run on one or more occassions. + +Think of it like an extension to `do_action()` which adds the ability to delay and repeat a hook. + +## Battle-Tested Background Processing + +Every month, Action Scheduler processes millions of payments for [Subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/), webhooks for [WooCommerce](https://wordpress.org/plugins/woocommerce/), as well as emails and other events for a range of other plugins. + +It's been seen on live sites processing queues in excess of 50,000 jobs and doing resource intensive operations, like processing payments and creating orders, at a sustained rate of over 10,000 / hour without negatively impacting normal site operations. + +This is all on infrastructure and WordPress sites outside the control of the plugin author. + +If your plugin needs background processing, especially of large sets of tasks, Action Scheduler can help. + +## Learn More + +To learn more about how to Action Scheduler works, and how to use it in your plugin, check out the docs on [ActionScheduler.org](https://actionscheduler.org). + +There you will find: + +* [Usage guide](https://actionscheduler.org/usage/): instructions on installing and using Action Scheduler +* [WP CLI guide](https://actionscheduler.org/wp-cli/): instructions on running Action Scheduler at scale via WP CLI +* [API Reference](https://actionscheduler.org/api/): complete reference guide for all API functions +* [Administration Guide](https://actionscheduler.org/admin/): guide to managing scheduled actions via the administration screen +* [Guide to Background Processing at Scale](https://actionscheduler.org/perf/): instructions for running Action Scheduler at scale via the default WP Cron queue runner + +## Credits + +Action Scheduler is developed and maintained by [Automattic](http://automattic.com/) with significant early development completed by [Flightless](https://flightless.us/). + +Collaboration is cool. We'd love to work with you to improve Action Scheduler. [Pull Requests](https://github.com/woocommerce/action-scheduler/pulls) welcome. + +== Changelog == + += 3.3.0 - 2021-09-15 = +* Enhancement - Adds as_has_scheduled_action() to provide a performant way to test for existing actions. #645 +* Fix - Improves compatibility with environments where NO_ZERO_DATE is enabled. #519 +* Fix - Adds safety checks to guard against errors when our database tables cannot be created. #645 +* Dev - Now supports queries that use multiple statuses. #649 +* Dev - Minimum requirements for WordPress and PHP bumped (to 5.2 and 5.6 respectively). #723 + += 3.2.1 - 2021-06-21 = +* Fix - Add extra safety/account for different versions of AS and different loading patterns. #714 +* Fix - Handle hidden columns (Tools → Scheduled Actions) | #600. + += 3.2.0 - 2021-06-03 = +* Fix - Add "no ordering" option to as_next_scheduled_action(). +* Fix - Add secondary scheduled date checks when claiming actions (DBStore) | #634. +* Fix - Add secondary scheduled date checks when claiming actions (wpPostStore) | #634. +* Fix - Adds a new index to the action table, reducing the potential for deadlocks (props: @glagonikas). +* Fix - Fix unit tests infrastructure and adapt tests to PHP 8. +* Fix - Identify in-use data store. +* Fix - Improve test_migration_is_scheduled. +* Fix - PHP notice on list table. +* Fix - Speed up clean up and batch selects. +* Fix - Update pending dependencies. +* Fix - [PHP 8.0] Only pass action arg values through to do_action_ref_array(). +* Fix - [PHP 8] Set the PHP version to 7.1 in composer.json for PHP 8 compatibility. +* Fix - add is_initialized() to docs. +* Fix - fix file permissions. +* Fix - fixes #664 by replacing __ with esc_html__. diff --git a/packages/woocommerce-admin/chunk-src-version-param.js b/packages/woocommerce-admin/chunk-src-version-param.js new file mode 100644 index 0000000..4eaef44 --- /dev/null +++ b/packages/woocommerce-admin/chunk-src-version-param.js @@ -0,0 +1,77 @@ +const pluginName = 'AsyncChunkSrcVersionParameterPlugin'; +/** + * Inspired by: https://github.com/webpack/webpack/issues/8115#issuecomment-663902035. + * + * This plugin modifies the webpack bootstrap code generated by the plugin at + * webpack/lib/web/JsonpMainTemplatePlugin.js and the CSS chunk loading code generated + * by @automattic/mini-css-extract-plugin-with-rtl. + * + * It will rename the function jsonpScriptSrc generated by that to webpackJsonpScriptSrc + * and install a new version that checks a user provided variable containing a script + * version parameter to specify in async chunk URLs. + * + * The jsonpScriptSrc override is only for webpack 4 (tested with 4.43 and 4.44). + * + * Webpack 5 has official support for this https://github.com/webpack/webpack/pull/8462 + * so it won't be necessary. + * + * It will also append the ?ver parameter to CSS chunk hrefs loaded by @automattic/mini-css-extract-plugin-with-rtl. + */ +class AsyncChunkSrcVersionParameterPlugin { + _applyMainTemplate( mainTemplate ) { + // Append script version to all async JS chunks loaded with jsonpScriptSrc(). + mainTemplate.hooks.localVars.tap( + // Use stage 1 to ensure this executes after webpack/lib/web/JsonpMainTemplatePlugin.js. + { name: pluginName, stage: 1 }, + ( source ) => { + if ( source.includes( 'function jsonpScriptSrc' ) ) { + const modSource = source.replace( + 'function jsonpScriptSrc', + 'function webpackJsonpScriptSrc' + ); + return `${ modSource } + +function jsonpScriptSrc(chunkId) { + var src = webpackJsonpScriptSrc(chunkId); + if ( window.wcAdminAssets && window.wcAdminAssets.version ) { + src += '?ver=' + window.wcAdminAssets.version; + } + return src; +} +`; + } + + return source; + } + ); + + // Append script version to all async CSS chunks loaded by @automattic/mini-css-extract-plugin-with-rtl. + mainTemplate.hooks.requireEnsure.tap( + // Use stage 1 to ensure this executes after @automattic/mini-css-extract-plugin-with-rtl. + { name: pluginName, stage: 1 }, + ( source ) => { + if ( + source.includes( '// mini-css-extract-plugin CSS loading' ) + ) { + return source.replace( + 'linkTag.href = fullhref;', + `linkTag.href = fullhref; +if ( window.wcAdminAssets && window.wcAdminAssets.version ) { + linkTag.href += '?ver=' + window.wcAdminAssets.version; +}` + ); + } + + return source; + } + ); + } + + apply( compiler ) { + compiler.hooks.thisCompilation.tap( pluginName, ( compilation ) => { + this._applyMainTemplate( compilation.mainTemplate ); + } ); + } +} + +module.exports = AsyncChunkSrcVersionParameterPlugin; diff --git a/packages/woocommerce-admin/dist/activity-panels-help/style-rtl.css b/packages/woocommerce-admin/dist/activity-panels-help/style-rtl.css new file mode 100644 index 0000000..062e581 --- /dev/null +++ b/packages/woocommerce-admin/dist/activity-panels-help/style-rtl.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-layout__activity-panel-header{height:50px;background:#e0e0e0;padding:16px;display:flex;justify-content:space-between;align-items:center}@media(min-width:783px){.woocommerce-layout__activity-panel-header{padding:16px 24px}}.woocommerce-layout__activity-panel-header h3{font-size:13px;font-weight:600;line-height:16px;margin:0;padding:0}.woocommerce-layout__activity-panel-header .woocommerce-ellipsis-menu__toggle.components-button:not(:disabled):not([aria-disabled=true]):focus,.woocommerce-layout__activity-panel-header .woocommerce-ellipsis-menu__toggle.components-button:not(:disabled):not([aria-disabled=true]):hover{box-shadow:none;border-radius:10px;background:#ccc}.woocommerce-layout__inbox-title{color:#1e1e1e;display:flex;align-items:center}.woocommerce-layout__inbox-subtitle{color:#757575}.woocommerce-layout__inbox-badge{margin-right:6px;background-color:#757575;border-radius:13px;padding:0 6px;color:#fff;display:inline-block;text-align:center;vertical-align:top} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/activity-panels-inbox/style-rtl.css b/packages/woocommerce-admin/dist/activity-panels-inbox/style-rtl.css new file mode 100644 index 0000000..1e59638 --- /dev/null +++ b/packages/woocommerce-admin/dist/activity-panels-inbox/style-rtl.css @@ -0,0 +1 @@ +.woocommerce-layout__activity-panel-content .woocommerce-inbox-message,.woocommerce-layout__activity-panel-content .woocommerce-notification-panels>div{margin-top:24px}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications{border-top:1px solid #e0e0e0}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications>div{border:none;margin-bottom:0}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification h3{color:#007cba;color:var(--wp-admin-theme-color);font-weight:700;font-size:14px}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification div,.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification p{color:#757575}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification .woocommerce-abbreviated-card__content{padding:12px 0 12px 24px}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification .woocommerce-abbreviated-card__content>:not(:first-child){margin-top:4px}.woocommerce-activity-panel .woocommerce-activity-card{position:relative;padding:24px;padding:var(--main-gap);background:#fff;border-bottom:1px solid #e0e0e0;color:#757575;font-size:13px;font-size:.8125rem}.woocommerce-activity-panel .woocommerce-activity-card:not(.woocommerce-empty-activity-card){display:grid;grid-template-columns:50px 1fr;grid-template-areas:"icon header" "icon body" "icon actions"}.woocommerce-activity-panel .woocommerce-activity-card__button{display:block;height:unset;background:none;align-items:unset;transition:unset;text-align:right;width:100%;padding:0}.woocommerce-activity-card__unread{position:absolute;top:18px;top:calc(var(--main-gap) - 6px);left:18px;left:calc(var(--main-gap) - 6px);width:6px;height:6px;border-radius:50%;background:#ca4a1f}.woocommerce-activity-card__icon{-ms-grid-row:1;-ms-grid-row-span:3;-ms-grid-column:1;grid-area:icon;fill:#e0e0e0}.woocommerce-activity-card__header{margin-bottom:16px;display:flex;flex-direction:column}.woocommerce-activity-card__header .woocommerce-activity-card__title{margin:0;font-size:14px;font-size:.875rem;order:2}.woocommerce-empty-activity-card .woocommerce-activity-card__header .woocommerce-activity-card__title{color:#1e1e1e;font-style:normal;line-height:24px;font-weight:400}.woocommerce-activity-card__button .woocommerce-activity-card__header .woocommerce-activity-card__title{margin-bottom:8px}.woocommerce-activity-card__header .woocommerce-activity-card__title a{text-decoration:none}.woocommerce-activity-card__header .woocommerce-activity-card__date{color:#757575;font-size:12px;font-size:.75rem;margin-bottom:12px;order:1}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{order:3}.woocommerce-activity-card__button .woocommerce-activity-card__header .woocommerce-activity-card__subtitle{margin-bottom:4px}@media(min-width:783px){.woocommerce-activity-card__header{-ms-grid-row:1;-ms-grid-column:2;grid-area:header;display:grid;grid-template:"title date" "subtitle date"/1fr auto}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__header{-ms-grid-row:1;-ms-grid-column:1}.woocommerce-activity-card__header .woocommerce-activity-card__title{grid-area:title}.woocommerce-activity-card__header .woocommerce-activity-card__date{display:block;grid-area:date;justify-self:end;margin-bottom:0}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{grid-area:subtitle}}@media (min-width:783px){.woocommerce-activity-card__header .woocommerce-activity-card__title{-ms-grid-row:1;-ms-grid-column:1}.woocommerce-activity-card__header .woocommerce-activity-card__date{-ms-grid-row:1;-ms-grid-row-span:2;-ms-grid-column:2}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{-ms-grid-row:2;-ms-grid-column:1}}.woocommerce-activity-card__body{-ms-grid-row:2;-ms-grid-column:2;grid-area:body}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__body{-ms-grid-row:2;-ms-grid-column:1}.woocommerce-activity-card__body>p:first-child{margin-top:0}.woocommerce-activity-card__body>p:last-child{margin-bottom:0}.woocommerce-empty-activity-card .woocommerce-activity-card__body{color:#757575;font-style:normal;font-weight:400;font-size:13px;font-size:.8125rem;line-height:20px}.woocommerce-activity-card__actions{-ms-grid-row:3;-ms-grid-column:2;grid-area:actions;margin-top:16px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__actions{-ms-grid-row:3;-ms-grid-column:1}.woocommerce-activity-card__actions>*+*{margin-right:.5em}.woocommerce-activity-card__actions .components-button{height:24px;padding:4px 10px;font-size:11px;font-size:.6875rem}.woocommerce-activity-card__actions .components-button.is-destructive:not(:hover){box-shadow:none}.woocommerce-activity-card.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;height:16px}.woocommerce-activity-card.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-activity-card.is-loading .is-placeholder{animation:none}}.woocommerce-activity-card.is-loading .woocommerce-activity-card__title{width:80%}.woocommerce-activity-card.is-loading .woocommerce-activity-card__subtitle{margin-top:4px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__date{width:100%;margin-bottom:16px}@media(min-width:783px){.woocommerce-activity-card.is-loading .woocommerce-activity-card__date{text-align:left;margin-bottom:0}}.woocommerce-activity-card.is-loading .woocommerce-activity-card__date .is-placeholder{width:68px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__icon{margin-left:24px;margin-left:var(--main-gap)}.woocommerce-activity-card.is-loading .woocommerce-activity-card__icon .is-placeholder{height:33px;width:33px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__body .is-placeholder{width:100%;margin-bottom:4px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__body .is-placeholder:last-of-type{width:65%;margin-bottom:0}.woocommerce-activity-card.is-loading .woocommerce-activity-card__actions .is-placeholder{width:91px;height:24px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card{grid-template-columns:1fr;grid-template-areas:"header" "body" "actions"}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__icon{display:none}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-flag{display:inline-block}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__subtitle span+span:before{content:" • "}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{grid-template-columns:72px 1fr;height:100%;opacity:1;padding:24px;padding:var(--main-gap)}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{transition:opacity .3s,height 0s,padding 0s}}@media(max-width:782px){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{grid-template-columns:64px 1fr}}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card .woocommerce-activity-card__header{margin-bottom:12px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card.actioned{height:0;opacity:0;padding:0}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card.actioned{transition:opacity .3s,height 0s .3s,padding 0s .3s}}.woocommerce-stock-activity-card__image-overlay__product{height:33px;position:relative;width:33px}.woocommerce-stock-activity-card__image-overlay__product.is-placeholder:before{background-color:#757575;border-radius:2px;content:"";position:absolute;right:0;left:0;bottom:0;top:0;opacity:.1}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-stock-activity-card{transition:opacity .3s}}.woocommerce-stock-activity-card.is-dimmed{opacity:.7}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__stock-quantity{background:#f0f0f0;color:#757575;padding:3px 8px;border-radius:3px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__stock-quantity.out-of-stock{color:#d94f4f}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity{display:inline-flex;width:50px;margin-left:10px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input{border-radius:2px;height:30px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]{-moz-appearance:textfield}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]::-webkit-inner-spin-button,.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.woocommerce-stock-activity-card .woocommerce-activity-card__subtitle{color:#757575;font-size:12px;font-size:.75rem}.woocommerce-empty-activity-card{background:#f0f0f0;margin:20px;border-bottom:unset}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}#activity-panel-inbox{margin:0 24px}.woocommerce-layout__inbox-panel-header{padding:24px}.woocommerce-homepage-column .woocommerce-layout__inbox-panel-header{padding:0 24px}.woocommerce-inbox-message-enter{opacity:0;max-height:0;transform:translateX(-50%)}.woocommerce-inbox-message-enter-active{transition:opacity .5s,transform .5s,max-height .5s}.woocommerce-inbox-message-enter-active,.woocommerce-inbox-message-exit{opacity:1;max-height:100vh;transform:translateX(0)}.woocommerce-inbox-message-exit-active{opacity:0;max-height:0;transform:translateX(-50%);transition:opacity .5s,transform .5s,max-height .5s} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/analytics-report-categories/style-rtl.css b/packages/woocommerce-admin/dist/analytics-report-categories/style-rtl.css new file mode 100644 index 0000000..51a21c1 --- /dev/null +++ b/packages/woocommerce-admin/dist/analytics-report-categories/style-rtl.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-table__product-categories>.woocommerce-table__breadcrumbs{display:inline-block;margin-left:12px}.woocommerce-table__product-categories .components-popover__content{padding:0 16px;text-align:right}.woocommerce-table__product-categories .components-popover__content .woocommerce-table__breadcrumbs{margin-top:12px;margin-bottom:12px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/analytics-report-customers/style-rtl.css b/packages/woocommerce-admin/dist/analytics-report-customers/style-rtl.css new file mode 100644 index 0000000..0042f7f --- /dev/null +++ b/packages/woocommerce-admin/dist/analytics-report-customers/style-rtl.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-report-table__scroll-point{position:relative;top:-48px}@media(max-width:782px){.woocommerce-report-table__scroll-point{top:-62px}}.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-108px}@media(max-width:782px){.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-122px}}.woocommerce-report-table .woocommerce-search{flex-grow:1}.woocommerce-report-table .components-card__header{display:grid;grid-gap:12px;grid-template-columns:min-content 1fr min-content}.woocommerce-report-table .woocommerce-table__compare.components-button{padding:8px}.woocommerce-report-table .woocommerce-ellipsis-menu{justify-self:flex-end}button.woocommerce-table__download-button{padding:6px 12px;color:#000;text-decoration:none;align-items:center}button.woocommerce-table__download-button svg{margin-left:8px;height:24px;width:24px}@media(max-width:782px){button.woocommerce-table__download-button svg{margin-left:0}button.woocommerce-table__download-button .woocommerce-table__download-button__label{display:none}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/analytics-report-orders/style-rtl.css b/packages/woocommerce-admin/dist/analytics-report-orders/style-rtl.css new file mode 100644 index 0000000..cf53e37 --- /dev/null +++ b/packages/woocommerce-admin/dist/analytics-report-orders/style-rtl.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-orders-table__status{flex-direction:row-reverse}.woocommerce-orders-table__status .woocommerce-order-status__indicator{margin-left:0;margin-right:8px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/analytics-report-products/style-rtl.css b/packages/woocommerce-admin/dist/analytics-report-products/style-rtl.css new file mode 100644 index 0000000..51a21c1 --- /dev/null +++ b/packages/woocommerce-admin/dist/analytics-report-products/style-rtl.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-table__product-categories>.woocommerce-table__breadcrumbs{display:inline-block;margin-left:12px}.woocommerce-table__product-categories .components-popover__content{padding:0 16px;text-align:right}.woocommerce-table__product-categories .components-popover__content .woocommerce-table__breadcrumbs{margin-top:12px;margin-bottom:12px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/analytics-report-stock/style-rtl.css b/packages/woocommerce-admin/dist/analytics-report-stock/style-rtl.css new file mode 100644 index 0000000..0042f7f --- /dev/null +++ b/packages/woocommerce-admin/dist/analytics-report-stock/style-rtl.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-report-table__scroll-point{position:relative;top:-48px}@media(max-width:782px){.woocommerce-report-table__scroll-point{top:-62px}}.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-108px}@media(max-width:782px){.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-122px}}.woocommerce-report-table .woocommerce-search{flex-grow:1}.woocommerce-report-table .components-card__header{display:grid;grid-gap:12px;grid-template-columns:min-content 1fr min-content}.woocommerce-report-table .woocommerce-table__compare.components-button{padding:8px}.woocommerce-report-table .woocommerce-ellipsis-menu{justify-self:flex-end}button.woocommerce-table__download-button{padding:6px 12px;color:#000;text-decoration:none;align-items:center}button.woocommerce-table__download-button svg{margin-left:8px;height:24px;width:24px}@media(max-width:782px){button.woocommerce-table__download-button svg{margin-left:0}button.woocommerce-table__download-button .woocommerce-table__download-button__label{display:none}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/analytics-report/style-rtl.css b/packages/woocommerce-admin/dist/analytics-report/style-rtl.css new file mode 100644 index 0000000..68a621e --- /dev/null +++ b/packages/woocommerce-admin/dist/analytics-report/style-rtl.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-analytics__table-placeholder .woocommerce-card__body{padding:0}.woocommerce-analytics__table-placeholder .woocommerce-table__table{margin-bottom:0}.woocommerce-analytics__table-placeholder .woocommerce-table__table tr:last-child{border-bottom-style:none} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/analytics-settings/style-rtl.css b/packages/woocommerce-admin/dist/analytics-settings/style-rtl.css new file mode 100644 index 0000000..fb60c8e --- /dev/null +++ b/packages/woocommerce-admin/dist/analytics-settings/style-rtl.css @@ -0,0 +1 @@ +@media(min-width:783px){.woocommerce-settings__wrapper{padding:0 13px}}.woocommerce-settings__actions{margin-bottom:40px}@media(min-width:1281px){.woocommerce-settings__actions{margin-right:15%}}.woocommerce-settings__actions button{margin-left:16px}.woocommerce-setting{display:flex;margin-bottom:24px}@media(max-width:1280px){.woocommerce-setting{flex-direction:column}}.woocommerce-setting__label{font-size:16px;font-size:1rem;margin-bottom:16px;padding-left:16px;font-weight:700}@media(min-width:1281px){.woocommerce-setting__label{width:15%}}.woocommerce-setting__input{display:flex;flex-direction:column}@media(min-width:1281px){.woocommerce-setting__input{width:35%}.woocommerce-setting__input .woocommerce-filters-filter{width:100%}}.woocommerce-setting__input label{width:100%;display:block;margin-bottom:12px;color:#757575}.woocommerce-setting__input .woocommerce-filters-filter label{margin-bottom:0}.woocommerce-setting__input button:not(.components-tab-panel__tabs-item){margin-bottom:12px;align-self:flex-start}.woocommerce-setting__input .components-base-control__field{display:flex}.woocommerce-setting__input .woocommerce-filters-date__content-controls{padding-bottom:0}.woocommerce-setting__options-group-label{display:block;font-weight:700;margin-bottom:12px}.woocommerce-setting__help{font-style:italic;color:#757575}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-settings-historical-data__columns{display:grid;grid-column-gap:24px;grid-template-columns:calc(50% - 12px) calc(50% - 12px)}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column{align-self:end;margin-top:12px}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column:first-child{grid-column-start:1;grid-column-end:2;grid-row-start:1;grid-row-end:2}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column:nth-child(2){grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:2}@media(max-width:960px){.woocommerce-settings-historical-data__columns{grid-template-columns:100%}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column:first-child{grid-column-start:1;grid-column-end:2;grid-row-start:1;grid-row-end:2}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column:nth-child(2){grid-column-start:1;grid-column-end:2;grid-row-start:2;grid-row-end:3}}.woocommerce-settings-historical-data__columns .components-base-control__label,.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column-label{margin-bottom:12px}.woocommerce-settings-historical-data__columns .components-select-control__input{height:38px;padding:8px 2px}.woocommerce-settings-historical-data__columns .components-base-control__field{margin-bottom:0}.woocommerce-settings-historical-data__skip-checkbox{margin-top:24px}.woocommerce-settings-historical-data__skip-checkbox>.components-base-control__field{margin-bottom:0}.woocommerce-settings-historical-data__skip-checkbox>.components-base-control__field>.components-checkbox-control__label{display:inline-block;margin-bottom:0;width:auto}.woocommerce-settings-historical-data__progress-label{display:inline-block;font-weight:700;margin-bottom:12px;margin-top:24px}.woocommerce-settings-historical-data__progress-label+.woocommerce-settings-historical-data__progress-label{margin-right:.25em}.woocommerce-settings-historical-data__progress-bar{-webkit-appearance:none;appearance:none;border:0;height:8px;width:100%;background-color:#c4c4c4}.woocommerce-settings-historical-data__progress-bar::-moz-progress-bar{background-color:#0085ba}.woocommerce-settings-historical-data__progress-bar::-webkit-progress-bar{background-color:#c4c4c4}.woocommerce-settings-historical-data__progress-bar::-webkit-progress-value{background-color:#0085ba}.woocommerce-settings-historical-data__status{display:block;font-weight:700;margin-top:24px}.woocommerce-settings-historical-data__status>.components-spinner{float:none;height:12px;margin-right:6px;margin-left:6px;width:12px}.woocommerce-settings-historical-data__status>.components-spinner:before{right:2px;height:3px;top:2px;transform-origin:4px 4px;width:3px}.woocommerce-settings-historical-data__actions{align-items:center;display:flex} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/app/index.asset.php b/packages/woocommerce-admin/dist/app/index.asset.php new file mode 100644 index 0000000..a45b5eb --- /dev/null +++ b/packages/woocommerce-admin/dist/app/index.asset.php @@ -0,0 +1 @@ + array('lodash', 'moment', 'react', 'react-dom', 'wc-components', 'wc-csv', 'wc-currency', 'wc-customer-effort-score', 'wc-date', 'wc-experimental', 'wc-explat', 'wc-navigation', 'wc-notices', 'wc-number', 'wc-settings', 'wc-store-data', 'wc-tracks', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-data-controls', 'wp-date', 'wp-dom', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-keycodes', 'wp-notices', 'wp-plugins', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => 'bcf6645772787a314f98c4ccf548c7d6'); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/app/index.js b/packages/woocommerce-admin/dist/app/index.js new file mode 100644 index 0000000..417770f --- /dev/null +++ b/packages/woocommerce-admin/dist/app/index.js @@ -0,0 +1,2 @@ +/*! For license information please see index.js.LICENSE.txt */ +this.wc=this.wc||{},this.wc.app=function(e){function t(t){for(var n,r,i=t[0],a=t[1],c=0,s=[];c=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}(e,["icon","size"]);return Object(i.cloneElement)(t,function(e){for(var t=1;tPromise.all([n.e(0),n.e(13)]).then(n.bind(null,481))),l=Object(a.lazy)(()=>Promise.all([n.e(0),n.e(12)]).then(n.bind(null,477))),u=Object(a.lazy)(()=>Promise.all([n.e(0),n.e(16)]).then(n.bind(null,482))),d=Object(a.lazy)(()=>Promise.all([n.e(0),n.e(11)]).then(n.bind(null,483))),m=Object(a.lazy)(()=>Promise.all([n.e(0),n.e(7)]).then(n.bind(null,479))),p=Object(a.lazy)(()=>Promise.all([n.e(0),n.e(8)]).then(n.bind(null,484))),f=Object(a.lazy)(()=>Promise.all([n.e(0),n.e(15)]).then(n.bind(null,485))),h=Object(a.lazy)(()=>Promise.all([n.e(0),n.e(10)]).then(n.bind(null,486))),b=Object(a.lazy)(()=>n.e(14).then(n.bind(null,478))),v=Object(a.lazy)(()=>n.e(9).then(n.bind(null,480)));t.a=()=>{const e=[{report:"revenue",title:Object(r.__)("Revenue",'woocommerce'),component:s,navArgs:{id:"woocommerce-analytics-revenue"}},{report:"products",title:Object(r.__)("Products",'woocommerce'),component:l,navArgs:{id:"woocommerce-analytics-products"}},{report:"variations",title:Object(r.__)("Variations",'woocommerce'),component:u,navArgs:{id:"woocommerce-analytics-variations"}},{report:"orders",title:Object(r.__)("Orders",'woocommerce'),component:d,navArgs:{id:"woocommerce-analytics-orders"}},{report:"categories",title:Object(r.__)("Categories",'woocommerce'),component:m,navArgs:{id:"woocommerce-analytics-categories"}},{report:"coupons",title:Object(r.__)("Coupons",'woocommerce'),component:p,navArgs:{id:"woocommerce-analytics-coupons"}},{report:"taxes",title:Object(r.__)("Taxes",'woocommerce'),component:f,navArgs:{id:"woocommerce-analytics-taxes"}},"yes"===c?{report:"stock",title:Object(r.__)("Stock",'woocommerce'),component:b,navArgs:{id:"woocommerce-analytics-stock"}}:null,{report:"customers",title:Object(r.__)("Customers",'woocommerce'),component:v},{report:"downloads",title:Object(r.__)("Downloads",'woocommerce'),component:h,navArgs:{id:"woocommerce-analytics-downloads"}}].filter(Boolean);return Object(o.applyFilters)("woocommerce_admin_reports_list",e)}},118:function(e,t,n){"use strict";var r=n(104),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},i={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},a={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},c={};function s(e){return r.isMemo(e)?a:c[e.$$typeof]||o}c[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},c[r.Memo]=a;var l=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,m=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,f=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(f){var o=p(n);o&&o!==f&&e(t,o,r)}var a=u(n);d&&(a=a.concat(d(n)));for(var c=s(t),h=s(n),b=0;bn.e(6).then(n.bind(null,605))),Q=Object(i.lazy)(()=>n.e(17).then(n.bind(null,614))),G=Object(i.lazy)(()=>n.e(25).then(n.bind(null,606))),Y=Object(i.lazy)(()=>Promise.all([n.e(1),n.e(2),n.e(31)]).then(n.bind(null,612))),Z=Object(i.lazy)(()=>Promise.all([n.e(2),n.e(34)]).then(n.bind(null,615))),K=Object(i.lazy)(()=>Promise.all([n.e(1),n.e(47)]).then(n.bind(null,613))),J=Object(i.lazy)(()=>Promise.all([n.e(1),n.e(47)]).then(n.bind(null,607))),X=()=>{const e=[],t=[["",Object(F.f)("woocommerceTranslation")]];return e.push({container:Y,path:"/",breadcrumbs:[...t,Object(H.__)("Home",'woocommerce')],wpOpenMenu:"toplevel_page_woocommerce",navArgs:{id:"woocommerce-home"},capability:"manage_woocommerce"}),window.wcAdminFeatures.analytics&&(e.push({container:G,path:"/analytics/overview",breadcrumbs:[...t,["/analytics/overview",Object(H.__)("Analytics",'woocommerce')],Object(H.__)("Overview",'woocommerce')],wpOpenMenu:"toplevel_page_wc-admin-path--analytics-overview",navArgs:{id:"woocommerce-analytics-overview"},capability:"view_woocommerce_reports"}),e.push({container:Q,path:"/analytics/settings",breadcrumbs:[...t,["/analytics/revenue",Object(H.__)("Analytics",'woocommerce')],Object(H.__)("Settings",'woocommerce')],wpOpenMenu:"toplevel_page_wc-admin-path--analytics-overview",navArgs:{id:"woocommerce-analytics-settings"},capability:"view_woocommerce_reports"}),e.push({container:W,path:"/customers",breadcrumbs:[...t,Object(H.__)("Customers",'woocommerce')],wpOpenMenu:"toplevel_page_woocommerce",navArgs:{id:"woocommerce-analytics-customers"},capability:"view_woocommerce_reports"}),e.push({container:W,path:"/analytics/:report",breadcrumbs:({match:e})=>{const n=Object(M.find)(Object(q.a)(),{report:e.params.report});return n?[...t,["/analytics/revenue",Object(H.__)("Analytics",'woocommerce')],n.title]:[]},wpOpenMenu:"toplevel_page_wc-admin-path--analytics-overview",capability:"view_woocommerce_reports"})),window.wcAdminFeatures.marketing&&e.push({container:Z,path:"/marketing",breadcrumbs:[...t,["/marketing",Object(H.__)("Marketing",'woocommerce')],Object(H.__)("Overview",'woocommerce')],wpOpenMenu:"toplevel_page_woocommerce-marketing",navArgs:{id:"woocommerce-marketing-overview"},capability:"view_woocommerce_reports"}),window.wcAdminFeatures.onboarding&&e.push({container:K,path:"/setup-wizard",breadcrumbs:[...t,["/setup-wizard",Object(H.__)("Setup Wizard",'woocommerce')]],capability:"manage_woocommerce"}),window.wcAdminFeatures.settings&&e.push({container:J,path:"/settings/:page",breadcrumbs:({match:e})=>{const n=Object(F.f)("settingsPages"),r=n[e.params.page];return r?[...t,[n.general?"/settings/general":"/settings/"+Object.keys(n)[0],Object(H.__)("Settings",'woocommerce')],r]:[]},wpOpenMenu:"toplevel_page_woocommerce",capability:"manage_woocommerce"}),Object(U.applyFilters)("woocommerce_admin_pages_list",e)};class ee extends i.Component{componentDidMount(){window.document.documentElement.scrollTop=0,window.document.body.classList.remove("woocommerce-admin-is-loading")}componentDidUpdate(e){const t=Object(M.omit)(e.query,"chartType","filter","paged"),n=Object(M.omit)(this.props.query,"chartType","filter","paged");e.query.paged>1&&!Object(M.isEqual)(t,n)&&Object(I.getHistory)().replace(Object(I.getNewPath)({paged:1})),e.match.url!==this.props.match.url&&(window.document.documentElement.scrollTop=0)}render(){const{page:e,match:t,query:n}=this.props,{url:r,params:o}=t;return window.wpNavMenuUrlUpdate(n),window.wpNavMenuClassChange(e,r),Object(i.createElement)(i.Suspense,{fallback:Object(i.createElement)(V.Spinner,null)},Object(i.createElement)(e.container,{params:o,path:r,pathMatch:e.path,query:n}))}}window.wpNavMenuUrlUpdate=function(e){const t=Object(I.getPersistedQuery)(e),n=Object(I.getQueryExcludedScreens)();Array.from(document.querySelectorAll("#adminmenu a")).forEach(e=>function(e,t,n){if(Object($.f)(e.href)){const r=Object(M.last)(e.href.split("?")),o=Object(L.parse)(r),i=o.path||"homescreen",a=Object(I.getScreenFromPath)(i),c=n.includes(a),s="admin.php?"+Object(L.stringify)(Object.assign(o,c?{}:t));e.href=s,e.onclick=e=>{e.preventDefault(),Object(I.getHistory)().push(s)}}}(e,t,n))},window.wpNavMenuClassChange=function(e,t){Array.from(document.getElementsByClassName("current")).forEach((function(e){e.classList.remove("current")}));Array.from(document.querySelectorAll(".wp-has-current-submenu")).forEach((function(e){e.classList.remove("wp-has-current-submenu"),e.classList.remove("wp-menu-open"),e.classList.remove("selected"),e.classList.add("wp-not-current-submenu"),e.classList.add("menu-top")}));const n="/"===t?"admin.php?page=wc-admin":"admin.php?page=wc-admin&path="+encodeURIComponent(t),r="/"===t?`li > a[href$="${n}"], li > a[href*="${n}?"]`:`li > a[href*="${n}"]`,o=document.querySelectorAll(r);if(Array.from(o).forEach((function(e){e.parentElement.classList.add("current")})),e.wpOpenMenu){const t=document.querySelector("#"+e.wpOpenMenu);t&&(t.classList.remove("wp-not-current-submenu"),t.classList.add("wp-has-current-submenu"),t.classList.add("wp-menu-open"),t.classList.add("current"))}document.querySelector("#wpwrap").classList.remove("wp-responsive-open")};var te=n(6),ne=n.n(te),re=n(28),oe=n(20),ie=n(116),ae=n(291),ce=n(27),se=(n(284),n(8)),le=Object(i.createElement)(se.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(i.createElement)(se.Path,{fillRule:"evenodd",d:"M6 5.5h12a.5.5 0 01.5.5v7H14a2 2 0 11-4 0H5.5V6a.5.5 0 01.5-.5zm-.5 9V18a.5.5 0 00.5.5h12a.5.5 0 00.5-.5v-3.5h-3.337a3.5 3.5 0 01-6.326 0H5.5zM4 13V6a2 2 0 012-2h12a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2v-5z",clipRule:"evenodd"})),ue=Object(i.createElement)(se.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(i.createElement)(se.Path,{d:"M12 4.75a7.25 7.25 0 100 14.5 7.25 7.25 0 000-14.5zM3.25 12a8.75 8.75 0 1117.5 0 8.75 8.75 0 01-17.5 0zM12 8.75a1.5 1.5 0 01.167 2.99c-.465.052-.917.44-.917 1.01V14h1.5v-.845A3 3 0 109 10.25h1.5a1.5 1.5 0 011.5-1.5zM11.25 15v1.5h1.5V15h-1.5z"})),de=n(500),me=(n(285),n(164));const pe={page:1,per_page:D.QUERY_DEFAULTS.pageSize,status:"unactioned",type:D.QUERY_DEFAULTS.noteTypes,orderby:"date",order:"desc"};function fe(e){const{getNotes:t,getNotesError:n,isResolving:r}=e(D.NOTES_STORE_NAME),{getCurrentUser:o}=e(D.USER_STORE_NAME),i=o(),a=parseInt(i&&i.woocommerce_meta&&i.woocommerce_meta.activity_panel_inbox_last_read,10);if(!a)return null;t(pe);const c=Boolean(n("getNotes",[pe])),s=r("getNotes",[pe]);if(c||s)return null;const l=t(pe);return Object(me.a)(l,a)>0}const he=({icon:e,title:t,name:n,unread:r,selected:o,isPanelOpen:c,onTabClick:s})=>{const l=ne()("woocommerce-layout__activity-panel-tab",{"is-active":c&&o,"has-unread":r}),u="activity-panel-tab-"+n;return Object(i.createElement)(a.Button,{role:"tab",className:l,"aria-selected":o,"aria-controls":"activity-panel-"+n,key:u,id:u,onClick:()=>{s(n)}},e,t," ",r&&Object(i.createElement)("span",{className:"screen-reader-text"},Object(H.__)("unread activity",'woocommerce')))},be=({tabs:e,onTabClick:t,selectedTab:n,tabOpen:r=!1})=>{const[{tabOpen:c,currentTab:s},l]=Object(i.useState)({tabOpen:r,currentTab:n});return Object(i.useEffect)(()=>{l({tabOpen:r,currentTab:n})},[r,n]),Object(i.createElement)(a.NavigableMenu,{role:"tablist",orientation:"horizontal",className:"woocommerce-layout__activity-panel-tabs"},e&&e.map((e,n)=>{if(e.component){const{component:t,options:r}=e;return Object(i.createElement)(t,o()({key:n},r))}return Object(i.createElement)(he,o()({key:n,index:n,isPanelOpen:c,selected:s===e.name},e,{onTabClick:()=>{const n=s!==e.name&&""!==s||!c;n&&s===e.name||Object(z.recordEvent)("activity_panel_open",{tab:e.name}),l({tabOpen:n,currentTab:e.name}),t(e,n)}}))}))},ve=()=>Object(i.createElement)("svg",{className:"woocommerce-layout__activity-panel-tab-icon setup-progress",width:"18",height:"18",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(i.createElement)("path",{d:"M12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20Z",stroke:"#DCDCDE",strokeWidth:"2"}),Object(i.createElement)("path",{d:"M4 12V12C4 16.4183 7.58172 20 12 20V20C16.4183 20 20 16.4183 20 12V12C20 7.58172 16.4183 4 12 4V4",strokeWidth:"2",strokeLinecap:"round"}));var ge=n(253),ye=n(497);n(286);const we="highlight-tooltip__show";function Oe({title:e,closeButtonText:t,content:n,show:r=!0,id:o,onClose:c,delay:s,onShow:l=M.noop,useAnchor:u=!1}){const[d,m]=Object(i.useState)(s>0?null:r),[p,f]=Object(i.useState)(null),[h,b]=Object(i.useState)(null);function v(){if(u){const e=document.getElementById(o);b(e.getBoundingClientRect())}}Object(i.useEffect)(()=>{const e=document.getElementById(o);let t,n;e&&!p&&(u?(n=document.createElement("div"),document.body.appendChild(n)):n=e.parentElement,t=document.createElement("div"),t.classList.add("highlight-tooltip__container"),n.appendChild(t),f(t));const r=g(t);return()=>{if(t){const e=t.parentElement;e.removeChild(t),u&&e.remove()}r&&clearTimeout(r)}},[]),Object(i.useEffect)(()=>{!d&&p&&p.classList.remove(we)},[d]),Object(i.useEffect)(()=>{r!==d&&null!==d&&p&&(m(r),r?p&&g(p):p.classList.remove(we))},[r]),Object(i.useLayoutEffect)(()=>(window.addEventListener("resize",v),()=>window.removeEventListener("resize",v)),[]);const g=e=>{let t=null;return s>0?t=setTimeout(()=>{t=null,y(e)},s):d||y(e),t},y=e=>{const t=document.getElementById(o);t&&u&&b(t.getBoundingClientRect()),e&&e.classList.add(we),m(!0),l()},w=()=>{m(!1),c&&c()};return p?Object(i.createPortal)(Object(i.createElement)("div",{className:"highlight-tooltip__portal"},d?Object(i.createElement)(i.Fragment,null,Object(i.createElement)(a.IsolatedEventContainer,{className:"highlight-tooltip__overlay"}),Object(i.createElement)(a.Popover,{className:"highlight-tooltip__popover",noArrow:!1,anchorRect:h,focusOnMount:"container"},Object(i.createElement)(a.Card,{size:"medium"},Object(i.createElement)(a.CardHeader,null,e,Object(i.createElement)(a.Button,{isSmall:!0,onClick:w,icon:ye.a})),Object(i.createElement)(a.CardBody,null,n||null),Object(i.createElement)(a.CardFooter,{isBorderless:!0},Object(i.createElement)(a.Button,{size:"small",isPrimary:!0,onClick:w},t||Object(H.__)("Close",'woocommerce')))))):null),p):null}Oe.propTypes={id:p.a.string.isRequired,title:p.a.string.isRequired,closeButtonText:p.a.string.isRequired,content:p.a.oneOfType([p.a.string,p.a.node]),show:p.a.bool,onClose:p.a.func,delay:p.a.number,onShow:p.a.func,useAnchor:p.a.bool};var je=n(91);const _e=["button","submit"];function Ee(e){const t=Object(i.useRef)(e);Object(i.useEffect)(()=>{t.current=e},[e]);const n=Object(i.useRef)(!1),r=Object(i.useRef)(),o=Object(i.useCallback)(()=>{clearTimeout(r.current)},[]);Object(i.useEffect)(()=>()=>o(),[]),Object(i.useEffect)(()=>{e||o()},[e,o]);const a=Object(i.useCallback)(e=>{const{type:t,target:r}=e;Object(M.includes)(["mouseup","touchend"],t)?n.current=!1:function(e){if(!(e instanceof window.HTMLElement))return!1;switch(e.nodeName){case"A":case"BUTTON":return!0;case"INPUT":return Object(M.includes)(_e,e.type)}return!1}(r)&&(n.current=!0)},[]),c=Object(i.useCallback)(e=>{e.persist(),n.current||(r.current=setTimeout(()=>{document.hasFocus()?"function"==typeof t.current&&t.current(e):e.preventDefault()},0))},[]);return{onFocus:o,onMouseDown:a,onMouseUp:a,onTouchStart:a,onTouchEnd:a,onBlur:c}}const ke=({content:e,isPanelOpen:t,isPanelSwitching:n,currentTab:r,tab:a,closePanel:c,clearPanel:s})=>{const l="woocommerce-layout__activity-panel-wrapper",u=function(e="firstElement"){const t=Object(i.useRef)(e);return Object(i.useEffect)(()=>{t.current=e},[e]),Object(i.useCallback)(e=>{if(!e||!1===t.current)return;if(e.contains(e.ownerDocument.activeElement))return;let n=e;if("firstElement"===t.current){const t=je.focus.tabbable.find(e)[0];t&&(n=t)}n.focus()},[])}(),d=Ee(e=>{const n=e.relatedTarget&&(e.relatedTarget.closest(".woocommerce-inbox-dismiss-confirmation_modal")||e.relatedTarget.closest(".components-snackbar__action"));t&&!n&&c()}),m=Object(i.useRef)(null),p=Object(i.useCallback)(e=>{m.current=e,u(e)},[]);if(!a)return Object(i.createElement)("div",{className:l});if(!e)return null;const f=ne()(l,{"is-open":t,"is-switching":n});return Object(i.createElement)("div",o()({className:f,tabIndex:0,role:"tabpanel","aria-label":a.title,onTransitionEnd:e=>{e&&"transform"===e.propertyName&&(s(),m.current&&t&&a&&u(m.current))}},d,{ref:p}),Object(i.createElement)("div",{className:"woocommerce-layout__activity-panel-content",key:"activity-panel-"+r,id:"activity-panel-"+r},Object(i.createElement)(i.Suspense,{fallback:Object(i.createElement)(V.Spinner,null)},e)))};var xe=n(66),Se=n(103),Ce=n(254);const Te=Object(i.lazy)(()=>n.e(4).then(n.bind(null,619))),Ae=Object(i.lazy)(()=>Promise.all([n.e(2),n.e(5)]).then(n.bind(null,608))),Pe=({isEmbedded:e,query:t,userPreferencesData:n})=>{const[r,o]=Object(i.useState)(""),[a,c]=Object(i.useState)(!1),[l,u]=Object(i.useState)(!1),[d,m]=Object(i.useState)(!1),{fills:p}=Object(oe.useSlot)(Ce.a),f=Boolean(null==p?void 0:p.length),h=(e,n)=>{let r={};if("wc-admin"===t.page&&"appearance"===t.task){var o;const{getTasksStatus:t}=e(D.ONBOARDING_STORE_NAME),i=t();r={set_notice:n("woocommerce_demo_store_notice")?"Y":"N",create_homepage:!0===i.hasHomepage?"Y":"N",upload_logo:null!==(o=i.themeMods)&&void 0!==o&&o.custom_logo?"Y":"N"}}return r};function b(e,t,n){const r=Object(xe.c)(e),o=!(!t||!l)&&Object(xe.d)(e,r)>0,i=!(!t||!l)&&Object(Se.b)(e),a=!(!t||!l)&&Object(xe.a)(e);return n>0||o||i||a||f}const{hasUnreadNotes:v,hasAbbreviatedNotifications:g,thingsToDoNextCount:y,requestingTaskListOptions:w,setupTaskListComplete:O,setupTaskListHidden:j,trackedCompletedTasks:_,previewSiteBtnTrackData:E}=Object(s.useSelect)(e=>{const{getOption:n,isResolving:r}=e(D.OPTIONS_STORE_NAME),o="yes"===n("woocommerce_task_list_hidden"),i="yes"===n("woocommerce_extended_task_list_hidden"),a=function(e,t,n){return!e||n?0:e.filter(e=>e.visible&&!e.completed&&!t.includes(e.key)).length}(Object(U.applyFilters)("woocommerce_admin_onboarding_task_list",[],t),n("woocommerce_task_list_dismissed_tasks")||[],i);return{hasUnreadNotes:fe(e),hasAbbreviatedNotifications:b(e,o,a),thingsToDoNextCount:a,requestingTaskListOptions:r("getOption",["woocommerce_task_list_complete"])||r("getOption",["woocommerce_task_list_hidden"]),setupTaskListComplete:"yes"===n("woocommerce_task_list_complete"),setupTaskListHidden:o,trackedCompletedTasks:n("woocommerce_task_list_tracked_completed_tasks")||[],previewSiteBtnTrackData:h(e,n)}}),{updateOptions:k}=Object(s.useDispatch)(D.OPTIONS_STORE_NAME),{currentUserCan:x}=Object(D.useUser)(),S=()=>"wc-admin"===t.page&&!t.path,C=()=>t.task&&!t.path&&(!0===w||!1===j&&!1===O),T=()=>{Object($.f)(window.location.href)?Object(I.getHistory)().push(Object(I.getNewPath)({},"/",{})):window.location.href=Object(F.e)("admin.php?page=wc-admin")},A=()=>{const n={name:"inbox",title:Object(H.__)("Inbox",'woocommerce'),icon:Object(i.createElement)(ie.a,{icon:le}),unread:v||g,visible:(e||!S())&&!C()},r={name:"setup",title:Object(H.__)("Finish setup",'woocommerce'),icon:Object(i.createElement)(ve,null),onClick:()=>(window.location.href!==Object(F.e)("admin.php?page=wc-admin")&&("no"===j?T():k({woocommerce_task_list_hidden:"no"}).then(T)),null),visible:x("manage_woocommerce")&&!O&&!j&&!C()&&(!S()||e)},o={name:"help",title:Object(H.__)("Help",'woocommerce'),icon:Object(i.createElement)(ie.a,{icon:ue}),visible:S()&&!e||C()},a={component:ge.b,visible:!e&&S()&&!C()};return[n,r,{name:"previewSite",title:Object(H.__)("Preview site",'woocommerce'),icon:Object(i.createElement)(ie.a,{icon:de.a}),visible:"wc-admin"===t.page&&"appearance"===t.task,onClick:()=>(window.open(Object(F.f)("siteUrl")),Object(z.recordEvent)("wcadmin_tasklist_previewsite",E),null)},a,o].filter(e=>e.visible)},P=A(),N=Object(M.uniqueId)("activity-panel-header_"),R=(()=>{const{task:e}=t,r=n&&n.task_list_tracked_started_tasks,o=n&&n.help_panel_highlight_shown;return!(!(e&&"yes"!==o&&(r||{})[e]>1)||_.includes(e))})();return Object(i.createElement)("div",null,Object(i.createElement)(V.H,{id:N,className:"screen-reader-text"},Object(H.__)("Store Activity",'woocommerce')),Object(i.createElement)(V.Section,{component:"aside",id:"woocommerce-activity-panel",className:"woocommerce-layout__activity-panel","aria-labelledby":N},Object(i.createElement)(be,{tabs:P,tabOpen:l,selectedTab:r,onTabClick:(e,t)=>{e.onClick?e.onClick():(({name:e},t)=>{const n=e!==r&&""!==r&&t&&l;a||(o(e),u(t),m(n))})(e,t)}}),Object(i.createElement)(ke,{currentTab:!0,isPanelOpen:l,isPanelSwitching:d,tab:Object(M.find)(A(),{name:r}),content:(e=>{const{task:n}=t;switch(e){case"inbox":return Object(i.createElement)(Ae,{hasAbbreviatedNotifications:g,thingsToDoNextCount:y});case"help":return Object(i.createElement)(Te,{taskName:n});default:return null}})(r),closePanel:()=>(c(!0),void u(!1)),clearPanel:()=>{l||(c(!1),m(!1),o(""))}})),R?Object(i.createElement)(Oe,{delay:1e3,useAnchor:!0,title:Object(H.__)("We're here for help",'woocommerce'),content:Object(H.__)("If you have any questions, feel free to explore the WooCommerce docs listed here.",'woocommerce'),closeButtonText:Object(H.__)("Got it",'woocommerce'),id:"activity-panel-tab-help",onClose:()=>(Object(z.recordEvent)("help_tooltip_click"),void(n&&n.updateUserPreferences&&n.updateUserPreferences({help_panel_highlight_shown:"yes"}))),onShow:()=>Object(z.recordEvent)("help_tooltip_view")}):null)};Pe.defaultProps={getHistory:I.getHistory};var Ne=Pe,Re=n(101),Me=n.n(Re);const Le=()=>/iPhone|iPad|iPod/i.test(window.navigator.userAgent)?"ios":/Android/i.test(window.navigator.userAgent)?"android":"unknown",Ve=()=>Object(i.createElement)("svg",{width:"37",height:"37",viewBox:"0 0 92 92",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(i.createElement)("rect",{width:"92",height:"92",rx:"21.3953",fill:"#7F54B3"}),Object(i.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M72.5937 28.043H19.8094C16.4781 28.0459 13.7783 30.7705 13.7754 34.1324V54.4501C13.7783 57.812 16.4781 60.5366 19.8094 60.5395H44.8229L56.2573 66.9607L53.6672 60.5395H72.599C74.2009 60.5402 75.7374 59.8983 76.8702 58.7552C78.0029 57.612 78.639 56.0614 78.6383 54.4447V34.1324C78.6376 32.5157 78.0002 30.9657 76.8664 29.8235C75.7327 28.6814 74.1956 28.0408 72.5937 28.043ZM19.1057 32.4208C18.4658 32.4324 17.8646 32.7359 17.467 33.2482C17.0888 33.7635 16.9404 34.4175 17.058 35.0502C18.5962 45.0986 20.0338 51.8757 21.371 55.3816C21.8779 56.658 22.4896 57.2703 23.2063 57.2185C24.3075 57.1489 25.6263 55.5968 27.1627 52.5621C27.9964 50.8412 29.2602 48.2662 30.9539 44.837C32.3785 49.88 34.309 53.6787 36.7456 56.2331C37.4291 56.9436 38.1204 57.2748 38.8195 57.2266C39.4185 57.1931 39.953 56.8315 40.217 56.2813C40.4753 55.7358 40.5806 55.1278 40.5211 54.5248C40.3516 52.0703 40.5919 48.667 41.2421 44.3149C41.9081 39.8057 42.7523 36.5818 43.7749 34.6432C43.9822 34.2526 44.0733 33.8087 44.037 33.366C44.0039 32.7587 43.7116 32.1969 43.2374 31.829C42.7745 31.4367 42.1799 31.2446 41.5803 31.2935C40.8334 31.3325 40.1682 31.7885 39.8499 32.4797C38.2331 35.5019 37.0812 40.4109 36.3943 47.2068C35.2823 44.2394 34.4509 41.1703 33.9114 38.0412C33.623 36.4613 32.9037 35.7125 31.7536 35.7946C30.9592 35.8589 30.3063 36.3944 29.7819 37.4012L24.0348 48.5643C23.0997 44.6692 22.2205 39.9289 21.3972 34.3433C21.1997 32.9652 20.4358 32.3244 19.1057 32.4208ZM69.9089 34.6877C71.6969 35.0381 73.2407 36.2 74.1186 37.8559C74.9693 39.3247 75.3946 41.1161 75.3946 43.23C75.4148 45.9567 74.7062 48.6357 73.3477 50.9687C71.7778 53.7023 69.7195 55.0691 67.1727 55.0691C66.6933 55.0668 66.2153 55.0128 65.7467 54.9078C63.9584 54.5581 62.4143 53.396 61.5371 51.7396C60.6864 50.2452 60.261 48.4411 60.261 46.3272C60.2357 43.6127 60.945 40.9454 62.3079 38.6295C63.9023 35.8959 65.9607 34.5291 68.4829 34.5291C68.9623 34.5304 69.4402 34.5836 69.9089 34.6877ZM68.7937 49.4848C69.7707 48.5773 70.4399 47.2269 70.8012 45.4337V45.4419C70.9315 44.7826 70.9959 44.1112 70.9933 43.4382C70.986 42.5849 70.8291 41.74 70.5302 40.9452C70.1443 39.901 69.6304 39.3124 68.9884 39.1793C68.0378 38.9643 67.1239 39.5256 66.2469 40.8632C65.5812 41.8393 65.109 42.9432 64.8577 44.1106C64.7276 44.7708 64.6632 45.4432 64.6657 46.1171C64.6739 46.9677 64.8308 47.8096 65.1287 48.6019C65.5146 49.6388 66.0294 50.2274 66.6731 50.3678C67.3169 50.5081 68.0237 50.2138 68.7937 49.4848ZM57.9079 37.8559C57.0291 36.2008 55.4854 35.0392 53.6976 34.6877C53.2279 34.5837 52.749 34.5306 52.2687 34.5291C49.7443 34.5291 47.6856 35.8959 46.0927 38.6295C44.7295 40.9454 44.0201 43.6127 44.0454 46.3272C44.0454 48.4411 44.4699 50.2452 45.319 51.7396C46.1976 53.3949 47.7414 54.5566 49.5294 54.9078C49.999 55.0126 50.4779 55.0667 50.9582 55.0691C53.5055 55.0691 55.5642 53.7023 57.1343 50.9687C58.4922 48.6355 59.2001 45.9565 59.1789 43.23C59.1789 41.1161 58.7544 39.3247 57.9053 37.8559H57.9079ZM54.5903 45.4337C54.2307 47.2269 53.5614 48.5773 52.5825 49.4848C51.8115 50.2065 51.101 50.5017 50.4589 50.3678C49.8169 50.2338 49.3011 49.6461 48.9169 48.6019C48.6181 47.8097 48.4603 46.9678 48.4511 46.1171C48.4495 45.4431 48.5148 44.7707 48.6459 44.1106C48.8971 42.9432 49.3694 41.8393 50.0353 40.8632C50.9124 39.5256 51.8264 38.9643 52.7773 39.1793C53.4193 39.3124 53.9333 39.901 54.3193 40.9452C54.617 41.7404 54.7739 42.585 54.7824 43.4382C54.785 44.1112 54.7207 44.7826 54.5903 45.4419V45.4337Z",fill:"white"}));n(287);const Ie=({onInstall:e,onDismiss:t})=>{Object(i.useEffect)(()=>{const e=document.getElementsByClassName("woocommerce-layout")[0];return"android"===Le()&&e&&e.classList.add("woocommerce-layout__show-app-banner"),()=>{e&&e.classList.remove("woocommerce-layout__show-app-banner")}},[]);const[n,r]=Object(i.useState)(!1);return"android"!==Le()||n?null:Object(i.createElement)("div",{className:"woocommerce-mobile-app-banner"},Object(i.createElement)(ie.a,{icon:Object(i.createElement)(Me.a,{"data-testid":"dismiss-btn"}),onClick:()=>{t(),r(!0),Object(z.recordEvent)("wcadmin_mobile_android_banner_click",{action:"dismiss"})}}),Object(i.createElement)(Ve,null),Object(i.createElement)("div",{className:"woocommerce-mobile-app-banner__description"},Object(i.createElement)("p",{className:"woocommerce-mobile-app-banner__description__text"},Object(H.__)("Run your store from anywhere",'woocommerce')),Object(i.createElement)("p",{className:"woocommerce-mobile-app-banner__description__text"},Object(H.__)("Download the WooCommerce app",'woocommerce'))),Object(i.createElement)(a.Button,{href:"https://play.google.com/store/apps/details?id=com.woocommerce.android",isSecondary:!0,onClick:()=>{e(),r(!0),Object(z.recordEvent)("wcadmin_mobile_android_banner_click",{action:"install"})}},Object(H.__)("Install",'woocommerce')))};function Fe(){const[e,t]=Object(i.useState)(!1),n=Object(i.useRef)(null);return Object(i.useEffect)(()=>{const e=()=>{t(window.pageYOffset>20)},r=()=>{n.current=window.requestAnimationFrame(e)};return window.addEventListener("scroll",r),()=>{window.removeEventListener("scroll",r),window.cancelAnimationFrame(n.current)}},[]),e}n(288);const De=(e,t,n=null)=>{if(!t)return 0;const r=0===(o=t).indexOf("http")?o:Object(F.e)(o);var o;const{href:i}=e;if(r===i)return Number.MAX_SAFE_INTEGER;const a=(e=>{const t=e.replace(/[-\/\\^$*+?.()|[\]{}]/gi,"\\$&"),[n,r,o]=t.split(/\\\?|#/),i=o?`(.*#${o}$)`:"";return"^"+n+(r?r.split("&").reduce((e,t)=>`${e}(?=.*[?|&]${t}(&|$|#))`,""):"")+i})(r),c=new RegExp(n||a,"i");return(decodeURIComponent(i).match(c)||[]).length},ze=e=>{let t=null,n=0;return e.forEach(e=>{const r=De(window.location,e.url,e.matchExpression);r>0&&r>=n&&(n=r,t=e)}),t||null},Be=["primary","favorites","plugins","secondary"],Ue={woocommerce:{id:"woocommerce",isCategory:!0,menuId:"primary",migrate:!0,order:10,parent:"",title:"WooCommerce"}};var He=Object(i.createElement)(se.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 24 24"},Object(i.createElement)(se.Path,{d:"M20 10c0-5.51-4.49-10-10-10C4.48 0 0 4.49 0 10c0 5.52 4.48 10 10 10 5.51 0 10-4.48 10-10zM7.78 15.37L4.37 6.22c.55-.02 1.17-.08 1.17-.08.5-.06.44-1.13-.06-1.11 0 0-1.45.11-2.37.11-.18 0-.37 0-.58-.01C4.12 2.69 6.87 1.11 10 1.11c2.33 0 4.45.87 6.05 2.34-.68-.11-1.65.39-1.65 1.58 0 .74.45 1.36.9 2.1.35.61.55 1.36.55 2.46 0 1.49-1.4 5-1.4 5l-3.03-8.37c.54-.02.82-.17.82-.17.5-.05.44-1.25-.06-1.22 0 0-1.44.12-2.38.12-.87 0-2.33-.12-2.33-.12-.5-.03-.56 1.2-.06 1.22l.92.08 1.26 3.41zM17.41 10c.24-.64.74-1.87.43-4.25.7 1.29 1.05 2.71 1.05 4.25 0 3.29-1.73 6.24-4.4 7.78.97-2.59 1.94-5.2 2.92-7.78zM6.1 18.09C3.12 16.65 1.11 13.53 1.11 10c0-1.3.23-2.48.72-3.59C3.25 10.3 4.67 14.2 6.1 18.09zm4.03-6.63l2.58 6.98c-.86.29-1.76.45-2.71.45-.79 0-1.57-.11-2.29-.33.81-2.38 1.62-4.74 2.42-7.1z"}));var qe=()=>{const e=Object(F.f)("siteTitle",""),t=Object(F.f)("homeUrl",""),n=Fe(),[r,o]=Object(i.useState)(document.body.classList.contains(!1)),c="is-wc-nav-folded",l="is-wc-nav-expanded",u=()=>{document.body.classList.add(c),document.body.classList.remove(l),o(!0)},d=()=>{document.body.classList.remove(c),document.body.classList.add(l),o(!1)},m=(e=document.body.clientWidth)=>{e<=960?u():d()};Object(i.useEffect)(()=>{m();const e=[{eventName:"orientationchange",handler:e=>m(e.target.screen.availWidth)},{eventName:"resize",handler:Object(M.debounce)(()=>m(),200)}];for(const{eventName:t,handler:n}of e)window.addEventListener(t,n,!1);Object(I.addHistoryListener)(()=>m())},[]);let p=Object(i.createElement)(ie.a,{size:"36px",icon:He});const{isRequestingSiteIcon:f,siteIconUrl:h}=Object(s.useSelect)(e=>{const{isResolving:t}=e("core/data"),{getEntityRecord:n}=e("core"),r=n("root","__unstableBase",void 0)||{};return{isRequestingSiteIcon:t("core","getEntityRecord",["root","__unstableBase",void 0]),siteIconUrl:r.siteIconUrl}});h?p=Object(i.createElement)("img",{alt:Object(H.__)("Site Icon"),src:h}):f&&(p=null);const b=ne()("woocommerce-navigation-header",{"is-scrolled":n});return Object(i.createElement)("div",{className:b},Object(i.createElement)(a.Button,{onClick:()=>{document.body.classList.contains(c)?d():u()},className:"woocommerce-navigation-header__site-icon","aria-label":"Fold navigation",role:"switch","aria-checked":r?"true":"false"},p),Object(i.createElement)(a.Button,{href:t,className:"woocommerce-navigation-header__site-title",as:"span"},Object(re.decodeEntities)(e)))},$e=(n(289),Object(i.createElement)(se.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(i.createElement)(se.Path,{d:"M11.776 4.454a.25.25 0 01.448 0l2.069 4.192a.25.25 0 00.188.137l4.626.672a.25.25 0 01.139.426l-3.348 3.263a.25.25 0 00-.072.222l.79 4.607a.25.25 0 01-.362.263l-4.138-2.175a.25.25 0 00-.232 0l-4.138 2.175a.25.25 0 01-.363-.263l.79-4.607a.25.25 0 00-.071-.222L4.754 9.881a.25.25 0 01.139-.426l4.626-.672a.25.25 0 00.188-.137l2.069-4.192z"}))),We=Object(i.createElement)(se.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(i.createElement)(se.Path,{fillRule:"evenodd",d:"M9.706 8.646a.25.25 0 01-.188.137l-4.626.672a.25.25 0 00-.139.427l3.348 3.262a.25.25 0 01.072.222l-.79 4.607a.25.25 0 00.362.264l4.138-2.176a.25.25 0 01.233 0l4.137 2.175a.25.25 0 00.363-.263l-.79-4.607a.25.25 0 01.072-.222l3.347-3.262a.25.25 0 00-.139-.427l-4.626-.672a.25.25 0 01-.188-.137l-2.069-4.192a.25.25 0 00-.448 0L9.706 8.646zM12 7.39l-.948 1.921a1.75 1.75 0 01-1.317.957l-2.12.308 1.534 1.495c.412.402.6.982.503 1.55l-.362 2.11 1.896-.997a1.75 1.75 0 011.629 0l1.895.997-.362-2.11a1.75 1.75 0 01.504-1.55l1.533-1.495-2.12-.308a1.75 1.75 0 01-1.317-.957L12 7.39z",clipRule:"evenodd"}));n(290);const Qe=({id:e})=>{const{favorites:t,isResolving:n}=Object(s.useSelect)(e=>({favorites:e(D.NAVIGATION_STORE_NAME).getFavorites(),isResolving:e(D.NAVIGATION_STORE_NAME).isResolving("getFavorites")})),{addFavorite:r,removeFavorite:o}=Object(s.useDispatch)(D.NAVIGATION_STORE_NAME),c=t.includes(e);return n?null:Object(i.createElement)(a.Button,{id:"woocommerce-navigation-favorite-button",className:"woocommerce-navigation-favorite-button",isTertiary:!0,onClick:()=>{(c?o:r)(e),Object(z.recordEvent)("navigation_favorite",{id:e,action:c?"unfavorite":"favorite"})},"aria-label":c?Object(H.__)("Add this item to your favorites.",'woocommerce'):Object(H.__)("Remove this item from your favorites.",'woocommerce')},Object(i.createElement)(ie.a,{icon:c?$e:We,className:`star-${c?"filled":"empty"}-icon`}))};const Ge="woocommerce_navigation_favorites_tooltip_hidden",Ye=()=>{const{isFavoritesResolving:e,isOptionResolving:t,isTooltipHidden:n}=Object(s.useSelect)(e=>{const{getOption:t,isResolving:n}=e(D.OPTIONS_STORE_NAME);return{isFavoritesResolving:e(D.NAVIGATION_STORE_NAME).isResolving("getFavorites"),isOptionResolving:n("getOption",[Ge]),isTooltipHidden:"yes"===t(Ge)}}),{updateOptions:r}=Object(s.useDispatch)(D.OPTIONS_STORE_NAME);return e||n||t||document.body.classList.contains("is-wc-nav-folded")?null:Object(i.createElement)(Oe,{delay:1e3,title:Object(H.__)("Introducing favorites",'woocommerce'),content:Object(H.__)("You can now favorite your extensions to pin them in the top level of the navigation.",'woocommerce'),closeButtonText:Object(H.__)("Got it",'woocommerce'),id:"woocommerce-navigation-favorite-button",onClose:()=>r({[Ge]:"yes"}),useAnchor:!0})};var Ze=({category:e})=>{const{id:t,menuId:n,title:r}=e,o="woocommerce-navigation-category-title";return["plugins","favorites"].includes(n)?Object(i.createElement)("span",{className:o},Object(i.createElement)("span",{className:o+"__text"},r),Object(i.createElement)(Qe,{id:t}),Object(i.createElement)(Ye,null)):Object(i.createElement)("span",{className:o},r)};var Ke=({item:e})=>{var t;const n=Object(oe.useSlot)("woocommerce_navigation_"+e.id),r=Boolean(null==n||null===(t=n.fills)||void 0===t?void 0:t.length),o=e=>{Object(z.recordEvent)("navigation_click",{menu_item:e})};return r&&!e.isCategory?Object(i.createElement)(oe.NavigationItem,{key:e.id,item:e.id},Object(i.createElement)("div",{onClick:()=>o(e.id)},Object(i.createElement)(I.WooNavigationItem.Slot,{name:e.id}))):Object(i.createElement)(oe.NavigationItem,{key:e.id,item:e.id,title:e.title,href:e.url,navigateToMenu:!e.url&&e.id,onClick:()=>o(e.id),hideIfTargetMenuEmpty:!0})};const Je=({category:e,onBackClick:t,pluginItems:n,primaryItems:r})=>{if(!r.length&&!n.length)return null;const o=Object(U.applyFilters)("woocommerce_navigation_root_back_label",Object(H.__)("WordPress Dashboard",'woocommerce')),a=Object(U.applyFilters)("woocommerce_navigation_root_back_url",window.wcNavigation.rootBackUrl),c="woocommerce"===e.id&&a;return Object(i.createElement)(oe.NavigationMenu,{title:Object(i.createElement)(Ze,{category:e}),menu:e.id,parentMenu:e.parent,backButtonLabel:c?o:e.backButtonLabel||null,onBackButtonClick:c?()=>{t("woocommerce"),window.location=a}:()=>t(e.id)},!!r.length&&Object(i.createElement)(oe.NavigationGroup,null,r.map(e=>Object(i.createElement)(Ke,{key:e.id,item:e}))),!!n.length&&Object(i.createElement)(oe.NavigationGroup,{title:"woocommerce"===e.id?Object(H.__)("Extensions",'woocommerce'):null},n.map(e=>Object(i.createElement)(Ke,{key:e.id,item:e}))))},Xe=({category:e,items:t,onBackClick:n})=>{if(!t.length)return null;const r="woocommerce"===e.id;return Object(i.createElement)(oe.NavigationMenu,{className:"components-navigation__menu-secondary",title:!r&&Object(i.createElement)(Ze,{category:e}),menu:e.id,parentMenu:e.parent,backButtonLabel:e.backButtonLabel||null,onBackButtonClick:r?null:()=>n(e.id)},Object(i.createElement)(oe.NavigationGroup,{onBackButtonClick:()=>n(e.id)},t.map(e=>Object(i.createElement)(Ke,{key:e.id,item:e}))))};var et=()=>{const{menuItems:e}=Object(s.useSelect)(e=>({menuItems:e(D.NAVIGATION_STORE_NAME).getMenuItems()}));Object(i.useEffect)(()=>{document.documentElement.classList.remove("wp-toolbar"),document.body.classList.add("has-woocommerce-navigation");const e=document.getElementById("adminmenumain");e&&e.classList.add("folded")},[]);const[t,n]=Object(i.useState)("woocommerce-home"),[r,o]=Object(i.useState)("woocommerce");Object(i.useEffect)(()=>{const r=ze(e);r&&t!==r&&(n(r),o(r.parent));return Object(I.addHistoryListener)(()=>{setTimeout(()=>{const t=ze(e);t&&(n(t),o(t.parent))},0)})},[e]);const{currentUserCan:a}=Object(D.useUser)(),{categories:c,items:l}=Object(i.useMemo)(()=>((e,t)=>{const n={...Ue};return{items:(e=>e.sort((e,t)=>e.order===t.order?e.title.localeCompare(t.title):e.order-t.order))(e).reduce((e,r)=>{if(e[r.parent]||(e[r.parent]={},Be.forEach(t=>{e[r.parent][t]=[]})),!e[r.parent][r.menuId])return e;if(t&&r.capability&&!t(r.capability))return e;r.isCategory&&(n[r.id]=r);const o=e[r.parent][r.menuId];return o&&o.push(r),e},{}),categories:n}})(e,a),[e,a]),u=Object(i.useRef)(null),d=e=>{Object(z.recordEvent)("navigation_back_click",{category:e})},m="woocommerce"===r,p=ne()("woocommerce-navigation",{"is-root":m});return Object(i.createElement)("div",{className:p},Object(i.createElement)(qe,null),Object(i.createElement)("div",{className:"woocommerce-navigation__wrapper",ref:u},Object(i.createElement)(oe.Navigation,{activeItem:t?t.id:null,activeMenu:r,onActivateMenu:(...e)=>{u&&u.current&&(u.current.scrollTop=0),o(...e)}},Object.values(c).map(e=>{const t=l[e.id];return!!t&&[Object(i.createElement)(Je,{key:e.id,category:e,onBackClick:d,primaryItems:[...t.primary,...t.favorites],pluginItems:t.plugins}),Object(i.createElement)(Xe,{key:"secondary/"+e.id,category:e,onBackClick:d,items:t.secondary})]}))))};var tt=Object(D.withNavigationHydration)(window.wcNavigation)(et);const nt=()=>{if(new URL(window.location.href).searchParams.get("task")){const e=Object(H.__)("WooCommerce Home",'woocommerce'),t=()=>{Object(z.recordEvent)("topbar_back_button",{page_name:rt(window.title)}),Object(I.updateQueryString)({},Object(I.getHistory)().location.pathname,{})};return Object(i.createElement)(a.Tooltip,{text:e},Object(i.createElement)("div",{tabIndex:"0",role:"button","data-testid":"header-back-button",className:"woocommerce-layout__header-back-button",onKeyDown:({keyCode:e})=>{e!==ce.ENTER&&e!==ce.SPACE||t()}},Object(i.createElement)(ie.a,{icon:ae.a,onClick:t})))}return null},rt=e=>{const t=new URL(window.location.href).searchParams.get("task");return{payments:Object(H.__)("Set up payments",'woocommerce'),tax:Object(H.__)("Add tax rates",'woocommerce'),appearance:Object(H.__)("Personalize your store",'woocommerce'),marketing:Object(H.__)("Set up marketing tools",'woocommerce'),products:Object(H.__)("Add products",'woocommerce'),shipping:Object(H.__)("Set up shipping costs",'woocommerce')}[t]||e},ot=({sections:e,isEmbedded:t=!1,query:n})=>{const r=Object(i.useRef)(null),o=Object(F.f)("siteTitle",""),a=e.slice(-1)[0],c=Fe(),{updateUserPreferences:s,...l}=Object(D.useUserPreferences)(),u="yes"===l.android_app_banner_dismissed;let d=null;const m=ne()("woocommerce-layout__header",{"is-scrolled":c});Object(i.useLayoutEffect)(()=>(p(),window.addEventListener("resize",p),()=>{window.removeEventListener("resize",p);const e=document.querySelector("#wpbody");e&&(e.style.marginTop=null)}),[u]);const p=()=>{clearTimeout(d),d=setTimeout((function(){const e=document.querySelector("#wpbody");e&&r.current&&(e.style.marginTop=r.current.offsetHeight+"px")}),200)};Object(i.useEffect)(()=>{if(!t){const t=e.map(e=>Array.isArray(e)?e[1]:e).reverse().join(" ‹ "),n=Object(re.decodeEntities)(Object(H.sprintf)(Object(H.__)("%1$s ‹ %2$s — WooCommerce",'woocommerce'),t,o));document.title!==n&&(document.title=n)}},[t,e,o]);const f=()=>{s({android_app_banner_dismissed:"yes"})},h=nt()?"with-back-button":"";return Object(i.createElement)("div",{className:m,ref:r},!u&&Object(i.createElement)(Ie,{onDismiss:f,onInstall:f}),Object(i.createElement)("div",{className:"woocommerce-layout__header-wrapper"},window.wcAdminFeatures.navigation&&Object(i.createElement)(tt,null),nt(),Object(i.createElement)(oe.Text,{className:"woocommerce-layout__header-heading "+h,as:"h1"},rt(Object(re.decodeEntities)(a))),window.wcAdminFeatures["activity-panels"]&&Object(i.createElement)(Ne,{isEmbedded:t,query:n,userPreferencesData:{...l,updateUserPreferences:s}})))};class it extends i.Component{render(){return Object(i.createElement)("div",{id:"woocommerce-layout__notice-list",className:"woocommerce-layout__notice-list"})}}var at=it,ct=n(159),st=n(256);var lt=function({notices:e,className:t,children:n,onRemove:r=M.noop,onRemove2:a=M.noop}){const s=Object(c.useReducedMotion)(),[l]=Object(i.useState)(()=>new WeakMap),u=Object(ct.useTransition)(e,e=>e.id,{from:{opacity:0,height:0},enter:e=>async t=>await t({opacity:1,height:l.get(e).offsetHeight}),leave:()=>async e=>{await e({opacity:0}),await e({height:0})},immediate:s});t=ne()("components-snackbar-list",t);const d=e=>()=>{r(e.id),a(e.id)};return Object(i.createElement)("div",{className:t},n,u.map(({item:e,key:t,props:n})=>Object(i.createElement)(ct.animated.div,{key:t,style:n},Object(i.createElement)("div",{className:"components-snackbar-list__notice-container",ref:t=>t&&l.set(e,t)},Object(i.createElement)(st.a,o()({},Object(M.omit)(e,["content"]),{onRemove:d(e)}),e.content)))))};n(296);const ut="woocommerce_admin_transient_notices_queue";function dt(e){const{removeNotice:t}=Object(s.useDispatch)("core/notices"),{createNotice:n,removeNotice:r}=Object(s.useDispatch)("core/notices2"),{updateOptions:o}=Object(s.useDispatch)(D.OPTIONS_STORE_NAME),{currentUser:a={},notices:c=[],notices2:l=[],noticesQueue:u={}}=Object(s.useSelect)(e=>({currentUser:e(D.USER_STORE_NAME).getCurrentUser(),notices:e("core/notices").getNotices(),notices2:e("core/notices2").getNotices(),noticesQueue:e(D.OPTIONS_STORE_NAME).getOption(ut)}));Object(i.useEffect)(()=>{Object.values(u).filter(e=>e.user_id===a.id||!e.user_id).forEach(e=>{const t=Object(U.applyFilters)("woocommerce_admin_queued_notice_filter",e);n(t.status,t.content,{onDismiss:()=>{(e=>{const t={...u};delete t[e],o({[ut]:t})})(t.id)},...t.options||{}})})},[]);const{className:d}=e,m=ne()("woocommerce-transient-notices","components-notices__snackbar",d),p=c.concat(l);return Object(i.createElement)(lt,{notices:p,className:m,onRemove:t,onRemove2:r})}dt.propTypes={className:p.a.string,notices:p.a.array};var mt=dt;Object(B.registerPlugin)("wc-admin-navigation",{render:()=>{const{persistedQuery:e}=Object(s.useSelect)(e=>({persistedQuery:e(D.NAVIGATION_STORE_NAME).getPersistedQuery()}));if(!Object($.f)(window.location.href))return null;const t=Object(q.a)().filter(e=>e.navArgs),n=X().filter(e=>e.navArgs).map(e=>"/analytics/settings"===e.path?{...e,breadcrumbs:[Object(H.__)("Analytics",'woocommerce')]}:e);return Object(i.createElement)(i.Fragment,null,n.map(t=>Object(i.createElement)(I.WooNavigationItem,{item:t.navArgs.id,key:t.navArgs.id},Object(i.createElement)(V.Link,{className:"components-button",href:Object(I.getNewPath)(Object(I.pathIsExcluded)(t.path)?{}:e,t.path,{}),type:"wc-admin"},t.breadcrumbs[t.breadcrumbs.length-1]))),t.map(t=>Object(i.createElement)(I.WooNavigationItem,{item:t.navArgs.id,key:t.navArgs.id},Object(i.createElement)(V.Link,{className:"components-button",href:Object(I.getNewPath)(Object(I.pathIsExcluded)(t.report)?{}:e,"/analytics/"+t.report,{}),type:"wc-admin"},t.title))))},scope:"woocommerce-navigation"});const pt=Object(i.lazy)(()=>Promise.all([n.e(1),n.e(49)]).then(n.bind(null,618))),ft=Object(i.lazy)(()=>n.e(53).then(n.bind(null,519)));class ht extends i.Component{render(){const{children:e}=this.props;return Object(i.createElement)("div",{className:"woocommerce-layout__primary",id:"woocommerce-layout__primary"},window.wcAdminFeatures["store-alerts"]&&Object(i.createElement)(i.Suspense,{fallback:Object(i.createElement)(V.Spinner,null)},Object(i.createElement)(pt,null)),Object(i.createElement)(at,null),e)}}class bt extends i.Component{componentDidMount(){this.recordPageViewTrack()}componentDidUpdate(e){const t=Object(M.get)(e,"location.pathname"),n=Object(M.get)(this.props,"location.pathname");t&&n&&t!==n&&this.recordPageViewTrack()}recordPageViewTrack(){const{activePlugins:e,installedPlugins:t,isEmbedded:n,isJetpackConnected:r}=this.props,o={has_navigation:!!window.wcNavigation};if(n){const e=document.location.pathname+document.location.search;return void Object(z.recordPageView)(e,{is_embedded:!0,...o})}const i=Object(M.get)(this.props,"location.pathname");if(!i)return;let a=i.substring(1).replace(/\//g,"_");0===a.length&&(a="home_screen"),Object(z.recordPageView)(a,{jetpack_installed:t.includes("jetpack"),jetpack_active:e.includes("jetpack"),jetpack_connected:r,...o})}getQuery(e){if(!e)return{};const t=e.substring(1);return Object(L.parse)(t)}isWCPaySettingsPage(){const{page:e,section:t,tab:n}=Object(I.getQuery)();return"wc-settings"===e&&"checkout"===n&&"woocommerce_payments"===t}render(){const{isEmbedded:e,...t}=this.props,{location:n,page:r}=this.props,{breadcrumbs:c}=r,s=this.getQuery(n&&n.search);return Object(i.createElement)(a.SlotFillProvider,null,Object(i.createElement)("div",{className:"woocommerce-layout"},Object(i.createElement)(ot,{sections:Object(M.isFunction)(c)?c(this.props):c,isEmbedded:e,query:s}),Object(i.createElement)(mt,null),!e&&Object(i.createElement)(ht,null,Object(i.createElement)("div",{className:"woocommerce-layout__main"},Object(i.createElement)(ee,o()({},t,{query:s})))),e&&this.isWCPaySettingsPage()&&Object(i.createElement)(i.Suspense,{fallback:null},Object(i.createElement)(ft,null))),Object(i.createElement)(B.PluginArea,{scope:'woocommerce'}),window.wcAdminFeatures.navigation&&Object(i.createElement)(B.PluginArea,{scope:"woocommerce-navigation"}),window.wcAdminFeatures.tasks&&Object(i.createElement)(B.PluginArea,{scope:"woocommerce-tasks"}))}}bt.propTypes={isEmbedded:p.a.bool,page:p.a.shape({container:p.a.oneOfType([p.a.func,p.a.object]),path:p.a.string,breadcrumbs:p.a.oneOfType([p.a.func,p.a.arrayOf(p.a.oneOfType([p.a.arrayOf(p.a.string),p.a.string]))]).isRequired,wpOpenMenu:p.a.string}).isRequired};const vt=Object(F.f)("dataEndpoints"),gt=Object(c.compose)(Object(D.withPluginsHydration)({...Object(F.f)("plugins",{}),jetpackStatus:vt&&vt.jetpackStatus||!1}),Object(s.withSelect)((e,{isEmbedded:t})=>{if(t)return;const{getActivePlugins:n,getInstalledPlugins:r,isJetpackConnected:o}=e(D.PLUGINS_STORE_NAME);return{activePlugins:n(),isJetpackConnected:o(),installedPlugins:r()}}))(bt),yt=Object(c.compose)(window.wcSettings.admin?Object(D.withOptionsHydration)({...Object(F.f)("preloadOptions",{})}):M.identity)(()=>{const{currentUserCan:e}=Object(D.useUser)();return Object(i.createElement)(_,{history:Object(I.getHistory)()},Object(i.createElement)(R,null,X().filter(t=>!t.capability||e(t.capability)).map(e=>Object(i.createElement)(S,{key:e.path,path:e.path,exact:!0,render:t=>Object(i.createElement)(gt,o()({page:e},t))}))))}),wt=Object(c.compose)(Object(F.f)("preloadOptions")?Object(D.withOptionsHydration)({...Object(F.f)("preloadOptions")}):M.identity)(()=>Object(i.createElement)(gt,{page:{breadcrumbs:Object(F.f)("embedBreadcrumbs",[])},isEmbedded:!0}))},12:function(e,t){e.exports=window.wc.navigation},120:function(e,t){e.exports=window.wc.number},122:function(e,t){e.exports=window.wc.explat},127:function(e,t){e.exports=window.wp.notices},128:function(e,t){var n,r,o=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function a(){throw new Error("clearTimeout has not been defined")}function c(e){if(n===setTimeout)return setTimeout(e,0);if((n===i||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:i}catch(e){n=i}try{r="function"==typeof clearTimeout?clearTimeout:a}catch(e){r=a}}();var s,l=[],u=!1,d=-1;function m(){u&&s&&(u=!1,s.length?l=s.concat(l):d=-1,l.length&&p())}function p(){if(!u){var e=c(m);u=!0;for(var t=l.length;t;){for(s=l,l=[];++d1)for(var n=1;n(o.includes(t)||(e[t]=i[t]),e),{});Object.keys(i.admin||{}).forEach(e=>{o.includes(e)||(a[e]=i.admin[e])});const c=a.adminUrl,s=(a.countries,a.currency),l=a.locale,u=a.orderStatuses;a.siteTitle,a.wcAssetUrl;function d(e,t=!1,n=(e=>e)){if(o.includes(e))throw new Error(Object(r.__)("Mutable settings should be accessed via data store."));return n(a.hasOwnProperty(e)?a[e]:t,t)}function m(e,t,n=(e=>e)){if(o.includes(e))throw new Error(Object(r.__)("Mutable settings should be mutated via data store."));a[e]=n(t)}function p(e){return(c||"")+e}function f(e){return new Promise((t,n)=>{document.querySelector(`#${e.handle}-js`)&&t();const r=document.createElement("script");r.src=e.src,r.id=e.handle+"-js",r.async=!0,r.onload=t,r.onerror=n,document.body.appendChild(r)})}},14:function(e,t){e.exports=window.wp.compose},15:function(e,t){e.exports=window.wp.url},159:function(e,t,n){"use strict";function r(e){return e&&"object"==typeof e&&"default"in e?e.default:e}Object.defineProperty(t,"__esModule",{value:!0});var o=r(n(35)),i=r(n(292)),a=n(5),c=r(a),s=r(n(293)),l=r(n(295)),u={arr:Array.isArray,obj:function(e){return"[object Object]"===Object.prototype.toString.call(e)},fun:function(e){return"function"==typeof e},str:function(e){return"string"==typeof e},num:function(e){return"number"==typeof e},und:function(e){return void 0===e},nul:function(e){return null===e},set:function(e){return e instanceof Set},map:function(e){return e instanceof Map},equ:function(e,t){if(typeof e!=typeof t)return!1;if(u.str(e)||u.num(e))return e===t;if(u.obj(e)&&u.obj(t)&&Object.keys(e).length+Object.keys(t).length===0)return!0;var n;for(n in e)if(!(n in t))return!1;for(n in t)if(e[n]!==t[n])return!1;return!u.und(n)||e===t}};function d(){var e=a.useState(!1)[1];return a.useCallback((function(){return e((function(e){return!e}))}),[])}function m(e,t){return u.und(e)||u.nul(e)?t:e}function p(e){return u.und(e)?[]:u.arr(e)?e:[e]}function f(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r=n.length)break;i=n[o++]}else{if((o=n.next()).done)break;i=o.value}for(var a=i,c=!1,s=0;s=p.startTime+l.duration;else if(l.decay)b=f+y/(1-.998)*(1-Math.exp(-(1-.998)*(t-p.startTime))),(u=Math.abs(p.lastPosition-b)<.1)&&(h=b);else{d=void 0!==p.lastTime?p.lastTime:t,y=void 0!==p.lastVelocity?p.lastVelocity:l.initialVelocity,t>d+64&&(d=t);for(var w=Math.floor(t-d),O=0;Oh:b=e);++n);return n-1}(e,i);return function(e,t,n,r,o,i,a,c,s){var l=s?s(e):e;if(ln){if("identity"===c)return l;"clamp"===c&&(l=n)}if(r===o)return r;if(t===n)return e<=t?r:o;t===-1/0?l=-l:n===1/0?l-=t:l=(l-t)/(n-t);l=i(l),r===-1/0?l=-l:o===1/0?l+=r:l=l*(o-r)+r;return l}(e,i[t],i[t+1],o[t],o[t+1],s,a,c,r.map)}}var z=function(e){function t(n,r,o,i){var a;return(a=e.call(this)||this).calc=void 0,a.payload=n instanceof y&&!(n instanceof t)?n.getPayload():Array.isArray(n)?n:[n],a.calc=D(r,o,i),a}s(t,e);var n=t.prototype;return n.getValue=function(){return this.calc.apply(this,this.payload.map((function(e){return e.getValue()})))},n.updateConfig=function(e,t,n){this.calc=D(e,t,n)},n.interpolate=function(e,n,r){return new t(this,e,n,r)},t}(y);var B=function(e){function t(t){var n;return(n=e.call(this)||this).animatedStyles=new Set,n.value=void 0,n.startPosition=void 0,n.lastPosition=void 0,n.lastVelocity=void 0,n.startTime=void 0,n.lastTime=void 0,n.done=!1,n.setValue=function(e,t){void 0===t&&(t=!0),n.value=e,t&&n.flush()},n.value=t,n.startPosition=t,n.lastPosition=t,n}s(t,e);var n=t.prototype;return n.flush=function(){0===this.animatedStyles.size&&function e(t,n){"update"in t?n.add(t):t.getChildren().forEach((function(t){return e(t,n)}))}(this,this.animatedStyles),this.animatedStyles.forEach((function(e){return e.update()}))},n.clearStyles=function(){this.animatedStyles.clear()},n.getValue=function(){return this.value},n.interpolate=function(e,t,n){return new z(this,e,t,n)},t}(g),U=function(e){function t(t){var n;return(n=e.call(this)||this).payload=t.map((function(e){return new B(e)})),n}s(t,e);var n=t.prototype;return n.setValue=function(e,t){var n=this;void 0===t&&(t=!0),Array.isArray(e)?e.length===this.payload.length&&e.forEach((function(e,r){return n.payload[r].setValue(e,t)})):this.payload.forEach((function(n){return n.setValue(e,t)}))},n.getValue=function(){return this.payload.map((function(e){return e.getValue()}))},n.interpolate=function(e,t){return new z(this,e,t)},t}(y),H=0,q=function(){function e(){var e=this;this.id=void 0,this.idle=!0,this.hasChanged=!1,this.guid=0,this.local=0,this.props={},this.merged={},this.animations={},this.interpolations={},this.values={},this.configs=[],this.listeners=[],this.queue=[],this.localQueue=void 0,this.getValues=function(){return e.interpolations},this.id=H++}var t=e.prototype;return t.update=function(e){if(!e)return this;var t=h(e),n=t.delay,r=void 0===n?0:n,a=t.to,c=i(t,["delay","to"]);if(u.arr(a)||u.fun(a))this.queue.push(o({},c,{delay:r,to:a}));else if(a){var s={};Object.entries(a).forEach((function(e){var t,n=e[0],i=e[1],a=o({to:(t={},t[n]=i,t),delay:f(r,n)},c),l=s[a.delay]&&s[a.delay].to;s[a.delay]=o({},s[a.delay],a,{to:o({},l,a.to)})})),this.queue=Object.values(s)}return this.queue=this.queue.sort((function(e,t){return e.delay-t.delay})),this.diff(c),this},t.start=function(e){var t,n=this;if(this.queue.length){this.idle=!1,this.localQueue&&this.localQueue.forEach((function(e){var t=e.from,r=void 0===t?{}:t,i=e.to,a=void 0===i?{}:i;u.obj(r)&&(n.merged=o({},r,n.merged)),u.obj(a)&&(n.merged=o({},n.merged,a))}));var r=this.local=++this.guid,a=this.localQueue=this.queue;this.queue=[],a.forEach((function(t,o){var c=t.delay,s=i(t,["delay"]),l=function(t){o===a.length-1&&r===n.guid&&t&&(n.idle=!0,n.props.onRest&&n.props.onRest(n.merged)),e&&e()},d=u.arr(s.to)||u.fun(s.to);c?setTimeout((function(){r===n.guid&&(d?n.runAsync(s,l):n.diff(s).start(l))}),c):d?n.runAsync(s,l):n.diff(s).start(l)}))}else u.fun(e)&&this.listeners.push(e),this.props.onStart&&this.props.onStart(),t=this,I.has(t)||I.add(t),V||(V=!0,E(P||F));return this},t.stop=function(e){return this.listeners.forEach((function(t){return t(e)})),this.listeners=[],this},t.pause=function(e){var t;return this.stop(!0),e&&(t=this,I.has(t)&&I.delete(t)),this},t.runAsync=function(e,t){var n=this,r=(e.delay,i(e,["delay"])),a=this.local,c=Promise.resolve(void 0);if(u.arr(r.to))for(var s=function(e){var t=e,i=o({},r,h(r.to[t]));u.arr(i.config)&&(i.config=i.config[t]),c=c.then((function(){if(a===n.guid)return new Promise((function(e){return n.diff(i).start(e)}))}))},l=0;l=r.length)return"break";a=r[i++]}else{if((i=r.next()).done)return"break";a=i.value}var n=a.key,c=function(e){return e.key!==n};(u.und(t)||t===n)&&(e.current.instances.delete(n),e.current.transitions=e.current.transitions.filter(c),e.current.deleted=e.current.deleted.filter(c))},r=e.current.deleted,o=Array.isArray(r),i=0;for(r=o?r:r[Symbol.iterator]();;){var a;if("break"===n())break}e.current.forceUpdate()}var Z=function(e){function t(t){var n;return void 0===t&&(t={}),n=e.call(this)||this,!t.transform||t.transform instanceof g||(t=b.transform(t)),n.payload=t,n}return s(t,e),t}(w),K={transparent:0,aliceblue:4042850303,antiquewhite:4209760255,aqua:16777215,aquamarine:2147472639,azure:4043309055,beige:4126530815,bisque:4293182719,black:255,blanchedalmond:4293643775,blue:65535,blueviolet:2318131967,brown:2771004159,burlywood:3736635391,burntsienna:3934150143,cadetblue:1604231423,chartreuse:2147418367,chocolate:3530104575,coral:4286533887,cornflowerblue:1687547391,cornsilk:4294499583,crimson:3692313855,cyan:16777215,darkblue:35839,darkcyan:9145343,darkgoldenrod:3095792639,darkgray:2846468607,darkgreen:6553855,darkgrey:2846468607,darkkhaki:3182914559,darkmagenta:2332068863,darkolivegreen:1433087999,darkorange:4287365375,darkorchid:2570243327,darkred:2332033279,darksalmon:3918953215,darkseagreen:2411499519,darkslateblue:1211993087,darkslategray:793726975,darkslategrey:793726975,darkturquoise:13554175,darkviolet:2483082239,deeppink:4279538687,deepskyblue:12582911,dimgray:1768516095,dimgrey:1768516095,dodgerblue:512819199,firebrick:2988581631,floralwhite:4294635775,forestgreen:579543807,fuchsia:4278255615,gainsboro:3705462015,ghostwhite:4177068031,gold:4292280575,goldenrod:3668254975,gray:2155905279,green:8388863,greenyellow:2919182335,grey:2155905279,honeydew:4043305215,hotpink:4285117695,indianred:3445382399,indigo:1258324735,ivory:4294963455,khaki:4041641215,lavender:3873897215,lavenderblush:4293981695,lawngreen:2096890111,lemonchiffon:4294626815,lightblue:2916673279,lightcoral:4034953471,lightcyan:3774873599,lightgoldenrodyellow:4210742015,lightgray:3553874943,lightgreen:2431553791,lightgrey:3553874943,lightpink:4290167295,lightsalmon:4288707327,lightseagreen:548580095,lightskyblue:2278488831,lightslategray:2005441023,lightslategrey:2005441023,lightsteelblue:2965692159,lightyellow:4294959359,lime:16711935,limegreen:852308735,linen:4210091775,magenta:4278255615,maroon:2147483903,mediumaquamarine:1724754687,mediumblue:52735,mediumorchid:3126187007,mediumpurple:2473647103,mediumseagreen:1018393087,mediumslateblue:2070474495,mediumspringgreen:16423679,mediumturquoise:1221709055,mediumvioletred:3340076543,midnightblue:421097727,mintcream:4127193855,mistyrose:4293190143,moccasin:4293178879,navajowhite:4292783615,navy:33023,oldlace:4260751103,olive:2155872511,olivedrab:1804477439,orange:4289003775,orangered:4282712319,orchid:3664828159,palegoldenrod:4008225535,palegreen:2566625535,paleturquoise:2951671551,palevioletred:3681588223,papayawhip:4293907967,peachpuff:4292524543,peru:3448061951,pink:4290825215,plum:3718307327,powderblue:2967529215,purple:2147516671,rebeccapurple:1714657791,red:4278190335,rosybrown:3163525119,royalblue:1097458175,saddlebrown:2336560127,salmon:4202722047,sandybrown:4104413439,seagreen:780883967,seashell:4294307583,sienna:2689740287,silver:3233857791,skyblue:2278484991,slateblue:1784335871,slategray:1887473919,slategrey:1887473919,snow:4294638335,springgreen:16744447,steelblue:1182971135,tan:3535047935,teal:8421631,thistle:3636451583,tomato:4284696575,turquoise:1088475391,violet:4001558271,wheat:4125012991,white:4294967295,whitesmoke:4126537215,yellow:4294902015,yellowgreen:2597139199},J="[-+]?\\d*\\.?\\d+";function X(){for(var e=arguments.length,t=new Array(e),n=0;n1&&(n-=1),n<1/6?e+6*(t-e)*n:n<.5?t:n<2/3?e+(t-e)*(2/3-n)*6:e}function le(e,t,n){var r=n<.5?n*(1+t):n+t-n*t,o=2*n-r,i=se(o,r,e+1/3),a=se(o,r,e),c=se(o,r,e-1/3);return Math.round(255*i)<<24|Math.round(255*a)<<16|Math.round(255*c)<<8}function ue(e){var t=parseInt(e,10);return t<0?0:t>255?255:t}function de(e){return(parseFloat(e)%360+360)%360/360}function me(e){var t=parseFloat(e);return t<0?0:t>1?255:Math.round(255*t)}function pe(e){var t=parseFloat(e);return t<0?0:t>100?1:t/100}function fe(e){var t,n,r="number"==typeof(t=e)?t>>>0===t&&t>=0&&t<=4294967295?t:null:(n=ae.exec(t))?parseInt(n[1]+"ff",16)>>>0:K.hasOwnProperty(t)?K[t]:(n=ee.exec(t))?(ue(n[1])<<24|ue(n[2])<<16|ue(n[3])<<8|255)>>>0:(n=te.exec(t))?(ue(n[1])<<24|ue(n[2])<<16|ue(n[3])<<8|me(n[4]))>>>0:(n=oe.exec(t))?parseInt(n[1]+n[1]+n[2]+n[2]+n[3]+n[3]+"ff",16)>>>0:(n=ce.exec(t))?parseInt(n[1],16)>>>0:(n=ie.exec(t))?parseInt(n[1]+n[1]+n[2]+n[2]+n[3]+n[3]+n[4]+n[4],16)>>>0:(n=ne.exec(t))?(255|le(de(n[1]),pe(n[2]),pe(n[3])))>>>0:(n=re.exec(t))?(le(de(n[1]),pe(n[2]),pe(n[3]))|me(n[4]))>>>0:null;return null===r?e:"rgba("+((4278190080&(r=r||0))>>>24)+", "+((16711680&r)>>>16)+", "+((65280&r)>>>8)+", "+(255&r)/255+")"}var he=/[+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,be=/(#(?:[0-9a-f]{2}){2,4}|(#[0-9a-f]{3})|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))/gi,ve=new RegExp("("+Object.keys(K).join("|")+")","g"),ge={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},ye=["Webkit","Ms","Moz","O"];function we(e,t,n){return null==t||"boolean"==typeof t||""===t?"":n||"number"!=typeof t||0===t||ge.hasOwnProperty(e)&&ge[e]?(""+t).trim():t+"px"}ge=Object.keys(ge).reduce((function(e,t){return ye.forEach((function(n){return e[function(e,t){return e+t.charAt(0).toUpperCase()+t.substring(1)}(n,t)]=e[t]})),e}),ge);var Oe={};R((function(e){return new Z(e)})),T("div"),x((function(e){var t=e.output.map((function(e){return e.replace(be,fe)})).map((function(e){return e.replace(ve,fe)})),n=t[0].match(he).map((function(){return[]}));t.forEach((function(e){e.match(he).forEach((function(e,t){return n[t].push(+e)}))}));var r=t[0].match(he).map((function(t,r){return D(o({},e,{output:n[r]}))}));return function(e){var n=0;return t[0].replace(he,(function(){return r[n++](e)})).replace(/rgba\(([0-9\.-]+), ([0-9\.-]+), ([0-9\.-]+), ([0-9\.-]+)\)/gi,(function(e,t,n,r,o){return"rgba("+Math.round(t)+", "+Math.round(n)+", "+Math.round(r)+", "+o+")"}))}})),j(K),O((function(e,t){if(!e.nodeType||void 0===e.setAttribute)return!1;var n=t.style,r=t.children,o=t.scrollTop,a=t.scrollLeft,c=i(t,["style","children","scrollTop","scrollLeft"]),s="filter"===e.nodeName||e.parentNode&&"filter"===e.parentNode.nodeName;for(var l in void 0!==o&&(e.scrollTop=o),void 0!==a&&(e.scrollLeft=a),void 0!==r&&(e.textContent=r),n)if(n.hasOwnProperty(l)){var u=0===l.indexOf("--"),d=we(l,n[l],u);"float"===l&&(l="cssFloat"),u?e.style.setProperty(l,d):e.style[l]=d}for(var m in c){var p=s?m:Oe[m]||(Oe[m]=m.replace(/([A-Z])/g,(function(e){return"-"+e.toLowerCase()})));void 0!==e.getAttribute(p)&&e.setAttribute(p,c[m])}}),(function(e){return e}));var je,_e,Ee=(je=function(e){return a.forwardRef((function(t,n){var r=d(),s=a.useRef(!0),l=a.useRef(null),m=a.useRef(null),p=a.useCallback((function(e){var t=l.current;l.current=new L(e,(function(){var e=!1;m.current&&(e=b.fn(m.current,l.current.getAnimatedValue())),m.current&&!1!==e||r()})),t&&t.detach()}),[]);a.useEffect((function(){return function(){s.current=!1,l.current&&l.current.detach()}}),[]),a.useImperativeHandle(n,(function(){return N(m,s,r)})),p(t);var f,h=l.current.getValue(),v=(h.scrollTop,h.scrollLeft,i(h,["scrollTop","scrollLeft"])),g=(f=e,!u.fun(f)||f.prototype instanceof c.Component?function(e){return m.current=function(e,t){return t&&(u.fun(t)?t(e):u.obj(t)&&(t.current=e)),e}(e,n)}:void 0);return c.createElement(e,o({},v,{ref:g}))}))},void 0===(_e=!1)&&(_e=!0),function(e){return(u.arr(e)?e:Object.keys(e)).reduce((function(e,t){var n=_e?t[0].toLowerCase()+t.substring(1):t;return e[n]=je(n),e}),je)}),ke=Ee(["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","big","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","menu","menuitem","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title","tr","track","u","ul","var","video","wbr","circle","clipPath","defs","ellipse","foreignObject","g","image","line","linearGradient","mask","path","pattern","polygon","polyline","radialGradient","rect","stop","svg","text","tspan"]);t.apply=Ee,t.config={default:{tension:170,friction:26},gentle:{tension:120,friction:14},wobbly:{tension:180,friction:12},stiff:{tension:210,friction:20},slow:{tension:280,friction:60},molasses:{tension:280,friction:120}},t.update=F,t.animated=ke,t.a=ke,t.interpolate=function(e,t,n){return e&&new z(e,t,n)},t.Globals=M,t.useSpring=function(e){var t=u.fun(e),n=$(1,t?e:[e]),r=n[0],o=n[1],i=n[2];return t?[r[0],o,i]:r},t.useTrail=function(e,t){var n=a.useRef(!1),r=u.fun(t),i=f(t),c=a.useRef(),s=$(e,(function(e,t){return 0===e&&(c.current=[]),c.current.push(t),o({},i,{config:f(i.config,e),attach:e>0&&function(){return c.current[e-1]}})})),l=s[0],d=s[1],m=s[2],p=a.useMemo((function(){return function(e){return d((function(t,n){e.reverse;var r=e.reverse?t+1:t-1,a=c.current[r];return o({},e,{config:f(e.config||i.config,t),attach:a&&function(){return a}})}))}}),[e,i.reverse]);return a.useEffect((function(){n.current&&!r&&p(t)})),a.useEffect((function(){n.current=!0}),[]),r?[l,p,m]:l},t.useTransition=function(e,t,n){var r=o({items:e,keys:t||function(e){return e}},n),c=G(r),s=c.lazy,l=void 0!==s&&s,u=(c.unique,c.reset),m=void 0!==u&&u,p=(c.enter,c.leave,c.update,c.onDestroyed),h=(c.keys,c.items,c.onFrame),b=c.onRest,v=c.onStart,g=c.ref,y=i(c,["lazy","unique","reset","enter","leave","update","onDestroyed","keys","items","onFrame","onRest","onStart","ref"]),w=d(),O=a.useRef(!1),j=a.useRef({mounted:!1,first:!0,deleted:[],current:{},transitions:[],prevProps:{},paused:!!r.ref,instances:!O.current&&new Map,forceUpdate:w});return a.useImperativeHandle(r.ref,(function(){return{start:function(){return Promise.all(Array.from(j.current.instances).map((function(e){var t=e[1];return new Promise((function(e){return t.start(e)}))})))},stop:function(e){return Array.from(j.current.instances).forEach((function(t){return t[1].stop(e)}))},get controllers(){return Array.from(j.current.instances).map((function(e){return e[1]}))}}})),j.current=function(e,t){var n=e.first,r=e.prevProps,a=i(e,["first","prevProps"]),c=G(t),s=c.items,l=c.keys,u=c.initial,d=c.from,m=c.enter,p=c.leave,h=c.update,b=c.trail,v=void 0===b?0:b,g=c.unique,y=c.config,w=c.order,O=void 0===w?["enter","leave","update"]:w,j=G(r),_=j.keys,E=j.items,k=o({},a.current),x=[].concat(a.deleted),S=Object.keys(k),C=new Set(S),T=new Set(l),A=l.filter((function(e){return!C.has(e)})),P=a.transitions.filter((function(e){return!e.destroyed&&!T.has(e.originalKey)})).map((function(e){return e.originalKey})),N=l.filter((function(e){return C.has(e)})),R=-v;for(;O.length;){switch(O.shift()){case"enter":A.forEach((function(e,t){g&&x.find((function(t){return t.originalKey===e}))&&(x=x.filter((function(t){return t.originalKey!==e})));var r=l.indexOf(e),o=s[r],i=n&&void 0!==u?"initial":"enter";k[e]={slot:i,originalKey:e,key:g?String(e):W++,item:o,trail:R+=v,config:f(y,o,i),from:f(n&&void 0!==u?u||{}:d,o),to:f(m,o)}}));break;case"leave":P.forEach((function(e){var t=_.indexOf(e),n=E[t];x.unshift(o({},k[e],{slot:"leave",destroyed:!0,left:_[Math.max(0,t-1)],right:_[Math.min(_.length,t+1)],trail:R+=v,config:f(y,n,"leave"),to:f(p,n)})),delete k[e]}));break;case"update":N.forEach((function(e){var t=l.indexOf(e),n=s[t];k[e]=o({},k[e],{item:n,slot:"update",trail:R+=v,config:f(y,n,"update"),to:f(h,n)})}))}}var M=l.map((function(e){return k[e]}));return x.forEach((function(e){var t,n=e.left,r=(e.right,i(e,["left","right"]));-1!==(t=M.findIndex((function(e){return e.originalKey===n})))&&(t+=1),t=Math.max(0,t),M=[].concat(M.slice(0,t),[r],M.slice(t))})),o({},a,{changed:A.length||P.length||N.length,first:n&&0===A.length,transitions:M,current:k,deleted:x,prevProps:t})}(j.current,r),j.current.changed&&j.current.transitions.forEach((function(e){var t=e.slot,n=e.from,r=e.to,i=e.config,a=e.trail,c=e.key,s=e.item;j.current.instances.has(c)||j.current.instances.set(c,new q);var u=j.current.instances.get(c),d=o({},y,{to:r,from:n,config:i,ref:g,onRest:function(n){j.current.mounted&&(e.destroyed&&(g||l||Y(j,c),p&&p(s)),!Array.from(j.current.instances).some((function(e){return!e[1].idle}))&&(g||l)&&j.current.deleted.length>0&&Y(j),b&&b(s,t,n))},onStart:v&&function(){return v(s,t)},onFrame:h&&function(e){return h(s,t,e)},delay:a,reset:m&&"enter"===t});u.update(d),j.current.paused||u.start()})),a.useEffect((function(){return j.current.mounted=O.current=!0,function(){j.current.mounted=O.current=!1,Array.from(j.current.instances).map((function(e){return e[1].destroy()})),j.current.instances.clear()}}),[]),j.current.transitions.map((function(e){var t=e.item,n=e.slot,r=e.key;return{item:t,key:r,state:n,props:j.current.instances.get(r).getValues()}}))},t.useChain=function(e,t,n){void 0===n&&(n=1e3);var r=a.useRef();a.useEffect((function(){u.equ(e,r.current)?e.forEach((function(e){var t=e.current;return t&&t.start()})):t?e.forEach((function(e,r){var i=e.current;if(i){var a=i.controllers;if(a.length){var c=n*t[r];a.forEach((function(e){e.queue=e.queue.map((function(e){return o({},e,{delay:e.delay+c})})),e.start()}))}}})):e.reduce((function(e,t,n){var r=t.current;return e.then((function(){return r.start()}))}),Promise.resolve()),r.current=e}))},t.useSprings=$},16:function(e,t){e.exports=window.wc.tracks},160:function(e,t,n){"use strict";(function(e){var r=n(5),o=n.n(r),i=n(25),a=n(1),c=n.n(a),s="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:void 0!==e?e:{};function l(e){var t=[];return{on:function(e){t.push(e)},off:function(e){t=t.filter((function(t){return t!==e}))},get:function(){return e},set:function(n,r){e=n,t.forEach((function(t){return t(e,r)}))}}}var u=o.a.createContext||function(e,t){var n,o,a,u="__create-react-context-"+((s[a="__global_unique_id__"]=(s[a]||0)+1)+"__"),d=function(e){function n(){var t;return(t=e.apply(this,arguments)||this).emitter=l(t.props.value),t}Object(i.a)(n,e);var r=n.prototype;return r.getChildContext=function(){var e;return(e={})[u]=this.emitter,e},r.componentWillReceiveProps=function(e){if(this.props.value!==e.value){var n,r=this.props.value,o=e.value;((i=r)===(a=o)?0!==i||1/i==1/a:i!=i&&a!=a)?n=0:(n="function"==typeof t?t(r,o):1073741823,0!==(n|=0)&&this.emitter.set(e.value,n))}var i,a},r.render=function(){return this.props.children},n}(r.Component);d.childContextTypes=((n={})[u]=c.a.object.isRequired,n);var m=function(t){function n(){var e;return(e=t.apply(this,arguments)||this).state={value:e.getValue()},e.onUpdate=function(t,n){0!=((0|e.observedBits)&n)&&e.setState({value:e.getValue()})},e}Object(i.a)(n,t);var r=n.prototype;return r.componentWillReceiveProps=function(e){var t=e.observedBits;this.observedBits=null==t?1073741823:t},r.componentDidMount=function(){this.context[u]&&this.context[u].on(this.onUpdate);var e=this.props.observedBits;this.observedBits=null==e?1073741823:e},r.componentWillUnmount=function(){this.context[u]&&this.context[u].off(this.onUpdate)},r.getValue=function(){return this.context[u]?this.context[u].get():e},r.render=function(){return(e=this.props.children,Array.isArray(e)?e[0]:e)(this.state.value);var e},n}(r.Component);return m.contextTypes=((o={})[u]=c.a.object,o),{Provider:d,Consumer:m}};t.a=u}).call(this,n(78))},161:function(e,t,n){var r=n(297);e.exports=p,e.exports.parse=i,e.exports.compile=function(e,t){return c(i(e,t),t)},e.exports.tokensToFunction=c,e.exports.tokensToRegExp=m;var o=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function i(e,t){for(var n,r=[],i=0,a=0,c="",u=t&&t.delimiter||"/";null!=(n=o.exec(e));){var d=n[0],m=n[1],p=n.index;if(c+=e.slice(a,p),a=p+d.length,m)c+=m[1];else{var f=e[a],h=n[2],b=n[3],v=n[4],g=n[5],y=n[6],w=n[7];c&&(r.push(c),c="");var O=null!=h&&null!=f&&f!==h,j="+"===y||"*"===y,_="?"===y||"*"===y,E=n[2]||u,k=v||g;r.push({name:b||i++,prefix:h||"",delimiter:E,optional:_,repeat:j,partial:O,asterisk:!!w,pattern:k?l(k):w?".*":"[^"+s(E)+"]+?"})}}return a{const{is_deleted:n,date_created_gmt:r,status:o}=e;if(!n){return(!t||!r||new Date(r+"Z").getTime()>t)&&"unactioned"===o}}).length}function i(e){return Object(r.filter)(e,e=>{const{is_deleted:t}=e;return!t}).length>0}},17:function(e,t){e.exports=window.wp.apiFetch},18:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},o=c(n(5)),i=c(n(69)),a=c(n(72));function c(e){return e&&e.__esModule?e:{default:e}}var s=void 0;function l(e,t){var n,a,c,u,d,m,p,f,h=[],b={};for(m=0;m "+s);if("componentClose"===d.type)throw new Error("Missing opening component token: `"+d.value+"`");if("componentOpen"===d.type){n=t[d.value],c=m;break}h.push(t[d.value])}else h.push(d.value);return n&&(u=function(e,t){var n,r,o=t[e],i=0;for(r=e+1;r=0||(o[n]=e[n]);return o}n.d(t,"a",(function(){return r}))},23:function(e,t,n){"use strict";function r(){return(r=Object.assign||function(e){for(var t=1;tObject(r.createElement)(r.Fragment,null,Object(r.createElement)("svg",{className:"woocommerce-layout__activity-panel-tab-icon",width:"24",height:"24",viewBox:"3 3 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(r.createElement)("path",{d:"M13.8053 15.3982C13.8053 15.7965 13.4867 16.1947 13.0089 16.1947H6.79646C6.55752 16.1947 6.39823 16.115 6.23894 15.9558C6.07965 15.7965 6 15.6372 6 15.3982V6.79646C6 6.63717 6.15929 6.39823 6.23894 6.23894C6.39823 6.07965 6.55752 6 6.79646 6H13.0089C13.4071 6 13.8053 6.31858 13.8053 6.79646V15.3982Z",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"}),Object(r.createElement)("path",{d:"M23.9203 10.6195C23.9203 11.0177 23.6017 11.4159 23.1238 11.4159H16.9115C16.6725 11.4159 16.5132 11.3363 16.3539 11.177C16.1946 11.0177 16.115 10.8584 16.115 10.6195V6.79646C16.115 6.39823 16.4336 6 16.9115 6H23.1238C23.5221 6 23.9203 6.31858 23.9203 6.79646V10.6195Z",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"}),Object(r.createElement)("path",{d:"M13.8053 23.2035C13.8053 23.4424 13.7257 23.6017 13.5664 23.761C13.4071 23.9203 13.2478 23.9999 13.0089 23.9999H6.79646C6.39823 23.9999 6 23.6813 6 23.2035V19.3804C6 19.1415 6.07965 18.9822 6.23894 18.8229C6.39823 18.6636 6.55752 18.584 6.79646 18.584H13.0089C13.4071 18.584 13.8053 18.9026 13.8053 19.3804V23.2035Z",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"}),Object(r.createElement)("path",{d:"M16.9912 23.9999C16.7522 23.9999 16.5929 23.9202 16.4336 23.7609C16.2743 23.6016 16.1947 23.4423 16.1947 23.2034V14.6016C16.1947 14.3627 16.2743 14.2034 16.4336 14.0441C16.5929 13.8848 16.7522 13.8052 16.9912 13.8052H23.2036C23.4425 13.8052 23.6018 13.8848 23.7611 14.0441C23.9204 14.2034 24 14.3627 24 14.6016V23.2034C24 23.6016 23.6814 23.9999 23.2036 23.9999H16.9912Z",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})),Object(a.__)("Display",'woocommerce')),{Fill:u,Slot:d}=Object(o.createSlotFill)("DisplayOptions");u.Slot=d;const m=[{value:"single_column",label:Object(r.createElement)(r.Fragment,null,Object(r.createElement)(()=>Object(r.createElement)("svg",{className:"woocommerce-layout__activity-panel-tab-icon",width:"12",height:"14",viewBox:"0 0 12 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(r.createElement)("rect",{x:"0.5",y:"0.5",width:"11",height:"13",strokeWidth:"1"})),null),Object(a.__)("Single column",'woocommerce'))},{value:"two_columns",label:Object(r.createElement)(r.Fragment,null,Object(r.createElement)(()=>Object(r.createElement)("svg",{className:"woocommerce-layout__activity-panel-tab-icon",width:"18",height:"14",viewBox:"0 0 18 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(r.createElement)("rect",{x:"0.5",y:"0.5",width:"7",height:"13",strokeWidth:"1"}),Object(r.createElement)("rect",{x:"9.5",y:"0.5",width:"7",height:"13",strokeWidth:"1"})),null),Object(a.__)("Two columns",'woocommerce'))}],p=()=>{const{defaultHomescreenLayout:e,taskListComplete:t,isTaskListHidden:n}=Object(i.useSelect)(e=>{const{getOption:t}=e(c.OPTIONS_STORE_NAME);return{defaultHomescreenLayout:t("woocommerce_default_homepage_layout")||"single_column",taskListComplete:"yes"===t("woocommerce_task_list_complete"),isTaskListHidden:"yes"===t("woocommerce_task_list_hidden")}}),{updateUserPreferences:u,homepage_layout:p}=Object(c.useUserPreferences)(),f=t||n||window.wcAdminFeatures.analytics;return Object(r.createElement)(d,null,t=>0!==t.length||f?Object(r.createElement)(o.DropdownMenu,{icon:Object(r.createElement)(l,null),label:Object(a.__)("Display options",'woocommerce'),toggleProps:{className:"woocommerce-layout__activity-panel-tab display-options",onClick:()=>Object(s.recordEvent)("homescreen_display_click")},popoverProps:{className:"woocommerce-layout__activity-panel-popover"}},({onClose:n})=>Object(r.createElement)(r.Fragment,null,t,f?Object(r.createElement)(o.MenuGroup,{className:"woocommerce-layout__homescreen-display-options",label:Object(a.__)("Layout",'woocommerce')},Object(r.createElement)(o.MenuItemsChoice,{choices:m,onSelect:e=>{u({homepage_layout:e}),n(),Object(s.recordEvent)("homescreen_display_option",{display_option:e})},value:p||e})):null)):null)}},254:function(e,t,n){"use strict";n.d(t,"a",(function(){return y})),n.d(t,"b",(function(){return w}));var r=n(0),o=n(2),i=n(20),a=n(16),c=n(21),s=n(7),l=n(11),u=n(498),d=n(8),m=Object(r.createElement)(d.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},Object(r.createElement)(d.Path,{d:"M18 4H6c-1.1 0-2 .9-2 2v12.9c0 .6.5 1.1 1.1 1.1.3 0 .5-.1.8-.3L8.5 17H18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm.5 11c0 .3-.2.5-.5.5H7.9l-2.4 2.4V6c0-.3.2-.5.5-.5h12c.3 0 .5.2.5.5v9z"})),p=n(499),f=n(3),h=n(66),b=n(103),v=n(60);const g=()=>Object(r.createElement)("svg",{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},Object(r.createElement)("path",{d:"M0 0h24v24H0z",fill:"none"}),Object(r.createElement)("path",{d:"M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z"})),y="AbbreviatedNotification",w=({thingsToDoNextCount:e})=>{const{ordersToProcessCount:t,reviewsToModerateCount:n,stockNoticesCount:d,isSetupTaskListHidden:w,isExtendedTaskListHidden:O}=Object(s.useSelect)(e=>{const{getOption:t}=e(l.OPTIONS_STORE_NAME),n=Object(h.c)(e);return{ordersToProcessCount:Object(h.d)(e,n),reviewsToModerateCount:Object(b.b)(e),stockNoticesCount:Object(h.a)(e),isSetupTaskListHidden:"yes"===t("woocommerce_task_list_hidden"),isExtendedTaskListHidden:"yes"===t("woocommerce_extended_task_list_hidden")}}),j=e=>{Object(a.recordEvent)("activity_panel_click",{task:e})},{Slot:_}=Object(f.createSlotFill)(y),E=Object(v.f)(window.location.href);return Object(r.createElement)("div",{className:"woocommerce-abbreviated-notifications"},e>0&&!O&&Object(r.createElement)(c.AbbreviatedCard,{className:"woocommerce-abbreviated-notification",icon:Object(r.createElement)(g,null),href:"admin.php?page=wc-admin#extended_task_list",onClick:()=>j("thingsToDoNext"),type:E?"wc-admin":"wp-admin"},Object(r.createElement)(i.Text,{as:"h3"},Object(o.__)("Things to do next",'woocommerce')),Object(r.createElement)(i.Text,{as:"p"},Object(o.sprintf)(Object(o._n)("You have %d new thing to do","You have %d new things to do",e,'woocommerce'),e))),t>0&&w&&Object(r.createElement)(c.AbbreviatedCard,{className:"woocommerce-abbreviated-notification",icon:u.a,href:"admin.php?page=wc-admin&opened_panel=orders-panel",onClick:()=>j("ordersToProcess"),type:E?"wc-admin":"wp-admin"},Object(r.createElement)(i.Text,{as:"h3"},Object(o.__)("Orders to fulfill",'woocommerce')),Object(r.createElement)(i.Text,null,Object(o.sprintf)(Object(o._n)("You have %d order to fulfill","You have %d orders to fulfill",t,'woocommerce'),t))),n>0&&w&&Object(r.createElement)(c.AbbreviatedCard,{className:"woocommerce-abbreviated-notification",icon:m,href:"admin.php?page=wc-admin&opened_panel=reviews-panel",onClick:()=>j("reviewsToModerate"),type:E?"wc-admin":"wp-admin"},Object(r.createElement)(i.Text,{as:"h3"},Object(o.__)("Reviews to moderate",'woocommerce')),Object(r.createElement)(i.Text,null,Object(o.sprintf)(Object(o._n)("You have %d review to moderate","You have %d reviews to moderate",n,'woocommerce'),n))),d>0&&w&&Object(r.createElement)(c.AbbreviatedCard,{className:"woocommerce-abbreviated-notification",icon:p.a,href:"admin.php?page=wc-admin&opened_panel=stock-panel",onClick:()=>j("stockNotices"),type:E?"wc-admin":"wp-admin"},Object(r.createElement)(i.Text,{as:"h3"},Object(o.__)("Inventory to review",'woocommerce')),Object(r.createElement)(i.Text,null,Object(o.__)("You have inventory to review and update",'woocommerce'))),!O&&Object(r.createElement)(_,null))}},255:function(e,t,n){"use strict";n.d(t,"a",(function(){return f})),n.d(t,"b",(function(){return y}));var r=n(0),o=n(2),i=n(30),a=n(18),c=n.n(a),s=n(13),l=n(32),u=n(21),d=n(11),m=n(19);var p=({value:e,onChange:t})=>{const{wcAdminSettings:n}=Object(d.useSettings)("wc_admin",["wcAdminSettings"]),{woocommerce_default_date_range:o}=n,i=Object(l.parse)(e.replace(/&/g,"&")),{period:a,compare:c,before:s,after:p}=Object(m.getDateParamsFromQuery)(i,o),{primary:f,secondary:h}=Object(m.getCurrentDates)(i,o),b={period:a,compare:c,before:s,after:p,primaryDate:f,secondaryDate:h};return Object(r.createElement)(u.DateRangeFilterPicker,{query:i,onRangeSelect:e=>{t({target:{name:"woocommerce_default_date_range",value:Object(l.stringify)(e)}})},dateQuery:b,isoDateFormat:m.isoDateFormat})};const f=["processing","on-hold"],h=["completed","processing","refunded","cancelled","failed","pending","on-hold"],b=Object.keys(s.c).filter(e=>"refunded"!==e).map(e=>({value:e,label:s.c[e],description:Object(o.sprintf)(Object(o.__)("Exclude the %s status from reports",'woocommerce'),s.c[e])})),v=Object(s.f)("unregisteredOrderStatuses",{}),g=[{key:"defaultStatuses",options:b.filter(e=>h.includes(e.value))},{key:"customStatuses",label:Object(o.__)("Custom Statuses",'woocommerce'),options:b.filter(e=>!h.includes(e.value))},{key:"unregisteredStatuses",label:Object(o.__)("Unregistered Statuses",'woocommerce'),options:Object.keys(v).map(e=>({value:e,label:e,description:Object(o.sprintf)(Object(o.__)("Exclude the %s status from reports",'woocommerce'),e)}))}],y=Object(i.applyFilters)("woocommerce_admin_analytics_settings",{woocommerce_excluded_report_order_statuses:{label:Object(o.__)("Excluded statuses:",'woocommerce'),inputType:"checkboxGroup",options:g,helpText:c()({mixedString:Object(o.__)("Orders with these statuses are excluded from the totals in your reports. The {{strong}}Refunded{{/strong}} status can not be excluded.",'woocommerce'),components:{strong:Object(r.createElement)("strong",null)}}),defaultValue:["pending","cancelled","failed"]},woocommerce_actionable_order_statuses:{label:Object(o.__)("Actionable statuses:",'woocommerce'),inputType:"checkboxGroup",options:g,helpText:Object(o.__)("Orders with these statuses require action on behalf of the store admin. These orders will show up in the Home Screen - Orders task.",'woocommerce'),defaultValue:f},woocommerce_default_date_range:{name:"woocommerce_default_date_range",label:Object(o.__)("Default date range:",'woocommerce'),inputType:"component",component:p,helpText:Object(o.__)("Select a default date range. When no range is selected, reports will be viewed by the default date range.",'woocommerce'),defaultValue:"period=month&compare=previous_year"}})},256:function(e,t,n){"use strict";(function(e){var r=n(0),o=n(4),i=n(6),a=n.n(i),c=n(257),s=n(2),l=(n(258),n(3));t.a=Object(r.forwardRef)((function({className:t,children:n,spokenMessage:i=n,politeness:u="polite",actions:d=[],onRemove:m=o.noop,icon:p=null,explicitDismiss:f=!1,onDismiss:h=null},b){function v(e){e&&e.preventDefault&&e.preventDefault(),h(),m()}h=h||o.noop,function(e,t){const n="string"==typeof e?e:Object(r.renderToString)(e);Object(r.useEffect)(()=>{n&&Object(c.speak)(n,t)},[n,t])}(i,u),Object(r.useEffect)(()=>{const e=setTimeout(()=>{f||(h(),m())},1e4);return()=>clearTimeout(e)},[f,h,m]);const g=a()(t,"components-snackbar",{"components-snackbar-explicit-dismiss":!!f});d&&d.length>1&&(void 0!==e&&e.env,d=[d[0]]);const y=a()("components-snackbar__content",{"components-snackbar__content-with-icon":!!p});return Object(r.createElement)("div",{ref:b,className:g,onClick:f?o.noop:v,tabIndex:"0",role:f?"":"button",onKeyPress:f?o.noop:v,"aria-label":f?"":Object(s.__)("Dismiss this notice")},Object(r.createElement)("div",{className:y},p&&Object(r.createElement)("div",{className:"components-snackbar__icon"},p),n,d.map(({label:e,onClick:t,url:n},o)=>Object(r.createElement)(l.Button,{key:o,href:n,isTertiary:!0,onClick:e=>function(e,t){e.stopPropagation(),m(),t&&t(e)}(e,t),className:"components-snackbar__action"},e)),f&&Object(r.createElement)("span",{role:"button","aria-label":"Dismiss this notice",tabIndex:"0",className:"components-snackbar__dismiss-button",onClick:v,onKeyPress:v},"✕")))}))}).call(this,n(128))},257:function(e,t){e.exports=window.wp.a11y},258:function(e,t){e.exports=window.wp.warning},259:function(e,t){e.exports=window.wc.customerEffortScore},269:function(e,t,n){"use strict";n.d(t,"a",(function(){return A}));var r={};n.r(r),n.d(r,"setCesSurveyQueue",(function(){return O})),n.d(r,"addCesSurvey",(function(){return j})),n.d(r,"addCesSurveyForAnalytics",(function(){return _})),n.d(r,"addCesSurveyForCustomerSearch",(function(){return E}));var o={};n.r(o),n.d(o,"getCesSurveyQueue",(function(){return k}));var i={};n.r(i),n.d(i,"getCesSurveyQueue",(function(){return x}));var a=n(0),c=n(1),s=n.n(c),l=n(16),u=n(259),d=n.n(u),m=n(14),p=n(7),f=n(11),h=n(2);function b({action:e,trackProps:t,label:n,onSubmitLabel:r=Object(h.__)("Thank you for your feedback!",'woocommerce'),cesShownForActions:o,allowTracking:i,resolving:c,storeAgeInWeeks:s,updateOptions:u,createNotice:m}){const[p,f]=Object(a.useState)(!1);if(c)return null;if(!i)return null;if(-1!==o.indexOf(e)&&!p)return null;const b=()=>{u({woocommerce_ces_shown_for_actions:[e,...o]})};return Object(a.createElement)(d.a,{recordScoreCallback:(n,o)=>{Object(l.recordEvent)("ces_feedback",{action:e,score:n,comments:o||"",store_age:s,...t}),m("success",r)},label:n,onNoticeShownCallback:()=>{Object(l.recordEvent)("ces_snackbar_view",{action:e,store_age:s,...t})},onNoticeDismissedCallback:()=>{Object(l.recordEvent)("ces_snackbar_dismiss",{action:e,store_age:s,...t}),b()},onModalShownCallback:()=>{f(!0),Object(l.recordEvent)("ces_view",{action:e,store_age:s,...t}),b()},icon:Object(a.createElement)("span",{style:{height:21,width:21},role:"img","aria-label":Object(h.__)("Pencil icon",'woocommerce')},"✏️")})}b.propTypes={action:s.a.string.isRequired,trackProps:s.a.object,label:s.a.string.isRequired,onSubmitLabel:s.a.string,cesShownForActions:s.a.arrayOf(s.a.string).isRequired,allowTracking:s.a.bool,resolving:s.a.bool.isRequired,storeAgeInWeeks:s.a.number,updateOptions:s.a.func,createNotice:s.a.func};var v=Object(m.compose)(Object(p.withSelect)(e=>{const{getOption:t,isResolving:n}=e(f.OPTIONS_STORE_NAME),r=t("woocommerce_ces_shown_for_actions")||[],o=function(e){if(0===e)return null;const t=Date.now()-1e3*e;return Math.round(t/f.WEEK)}(t("woocommerce_admin_install_timestamp")||0);return{cesShownForActions:r,allowTracking:"yes"===(t("woocommerce_allow_tracking")||"no"),storeAgeInWeeks:o,resolving:n("getOption",["woocommerce_ces_shown_for_actions"])||null===o||n("getOption",["woocommerce_admin_install_timestamp"])||n("getOption",["woocommerce_allow_tracking"])}}),Object(p.withDispatch)(e=>{const{updateOptions:t}=e(f.OPTIONS_STORE_NAME),{createNotice:n}=e("core/notices");return{updateOptions:t,createNotice:n}}))(b),g=n(55),y=n(10);var w={SET_CES_SURVEY_QUEUE:"SET_CES_SURVEY_QUEUE",ADD_CES_SURVEY:"ADD_CES_SURVEY"};function O(e){return{type:w.SET_CES_SURVEY_QUEUE,queue:e}}function j(e,t,n=window.pagenow,r=window.adminpage,o,i={}){return{type:w.ADD_CES_SURVEY,action:e,label:t,pageNow:n,adminPage:r,onsubmit_label:o,props:i}}function _(){return j("analytics_filtered",Object(h.__)("How easy was it to filter your store analytics?",'woocommerce'),"woocommerce_page_wc-admin","woocommerce_page_wc-admin")}function E(){return j("ces_search",Object(h.__)("How easy was it to use search?",'woocommerce'),"woocommerce_page_wc-admin","woocommerce_page_wc-admin",void 0,{search_area:"customer"})}function*k(){const e=yield Object(y.apiFetch)({path:`${g.a}/options?options=${g.b}`});if(!e)throw new Error;yield O(e[g.b]||[])}function x(e){return e.queue}const S={queue:[]};var C=(e=S,t)=>{switch(t.type){case w.SET_CES_SURVEY_QUEUE:return{...e,queue:t.queue};case w.ADD_CES_SURVEY:if(e.queue.filter(e=>e.action===t.action).length)return e;const n={action:t.action,label:t.label,pagenow:t.pageNow,adminpage:t.adminPage,onSubmitLabel:t.onSubmitLabel,props:t.props};return{...e,queue:[...e.queue,n]};default:return e}};Object(p.registerStore)(g.c,{actions:r,selectors:i,resolvers:o,controls:y.controls,reducer:C});function T({queue:e,resolving:t,clearQueue:n}){if(t)return null;const r=e.filter(e=>e.pagenow===window.pagenow&&e.adminpage===window.adminpage);return r.length&&n(),Object(a.createElement)(a.Fragment,null,r.map((e,t)=>Object(a.createElement)(v,{key:t,action:e.action,label:e.label,onSubmitLabel:e.onsubmit_label,trackProps:e.props||{}})))}T.propTypes={queue:s.a.arrayOf(s.a.object),resolving:s.a.bool,clearQueue:s.a.func};var A=Object(m.compose)(Object(p.withSelect)(e=>{const{getCesSurveyQueue:t,isResolving:n}=e(g.c);return{queue:t(),resolving:n("getOption",[g.b])}}),Object(p.withDispatch)(e=>{const{updateOptions:t}=e(f.OPTIONS_STORE_NAME);return{clearQueue:()=>{t({woocommerce_clear_ces_tracks_queue_for_page:{pagenow:window.pagenow,adminpage:window.adminpage}})}}}))(T)},27:function(e,t){e.exports=window.wp.keycodes},270:function(e,t,n){"use strict";n.d(t,"a",(function(){return f}));var r=n(35),o=n.n(r),i=n(0),a=n(30),c=n(32),s=n(7),l=n(11);var u=({children:e})=>{const{currentUserCan:t}=Object(l.useUser)(),n=Object(s.useSelect)(e=>{const{getOption:t,hasFinishedResolution:n}=e(l.OPTIONS_STORE_NAME),r=n("getOption",["woocommerce_show_marketplace_suggestions"]),o="no"!==t("woocommerce_show_marketplace_suggestions");return r&&o});return t("install_plugins")&&n?Object(i.createElement)(i.Fragment,null,e):null};const d=Object(i.lazy)(()=>Promise.all([n.e(3),n.e(45)]).then(n.bind(null,609))),m=Object(i.lazy)(()=>n.e(48).then(n.bind(null,616)));n(298);const p=[({page:e,tab:t,section:n})=>"wc-settings"!==e||"checkout"!==t||n?null:Object(i.createElement)(u,null,Object(i.createElement)(i.Suspense,{fallback:null},Object(i.createElement)(d,null))),({page:e,tab:t,section:n,zone_id:r})=>"wc-settings"!==e||"shipping"!==t||Boolean(n)||Boolean(r)?null:Object(i.createElement)(u,null,Object(i.createElement)(i.Suspense,{fallback:null},Object(i.createElement)(m,null)))],f=()=>{const e=Object(c.parse)(location.search.substring(1));let t={page:"",tab:""};void 0!==e.page&&(t=e);const n=Object(a.applyFilters)("woocommerce_admin_embedded_layout_components",p,t);return Object(i.createElement)("div",{className:"woocommerce-embedded-layout__primary",id:"woocommerce-embedded-layout__primary"},n.map((e,n)=>Object(i.createElement)(e,o()({key:n},t))))}},279:function(e,t,n){"use strict";n.r(t),function(e){var t=n(0),r=(n(127),n(11)),o=n(13),i=(n(281),n(119)),a=n(269),c=n(270);n.p=e.wcAdminAssets.path;const s=document.getElementById("root"),l=document.getElementById("woocommerce-embedded-root"),u=Object(o.f)("currentUserData");if(s){let e=Object(r.withSettingsHydration)("wc_admin",window.wcSettings.admin)(i.b);const n=!!window.wcSettings.admin&&window.wcSettings.admin.preloadSettings;n&&n.general&&(e=Object(r.withSettingsHydration)("general",{general:n.general})(e)),u&&(e=Object(r.withCurrentUserHydration)(u)(e)),Object(t.render)(Object(t.createElement)(e,null),s)}else if(l){let e=Object(r.withSettingsHydration)("wc_admin",window.wcSettings.admin)(i.a);u&&(e=Object(r.withCurrentUserHydration)(u)(e)),Object(t.render)(Object(t.createElement)(e,null),l),l.classList.remove("is-embed-loading");const n=document.getElementById("wpbody-content"),o=n.querySelector(".wrap.woocommerce")||n.querySelector(".wrap"),a=document.createElement("div");Object(t.render)(Object(t.createElement)("div",{className:"woocommerce-layout"},Object(t.createElement)(i.c,null)),n.insertBefore(a,o));const s=document.createElement("div");Object(t.render)(Object(t.createElement)(c.a,null),n.insertBefore(s,o.nextSibling))}window.wcAdminFeatures&&!0===window.wcAdminFeatures["customer-effort-score-tracks"]&&function(){const e=s||l;Object(t.render)(Object(t.createElement)(a.a,null),e.insertBefore(document.createElement("div"),null))}()}.call(this,n(78))},28:function(e,t){e.exports=window.wp.htmlEntities},281:function(e,t,n){},282:function(e,t){e.exports=window.wc.notices},283:function(e,t,n){},284:function(e,t,n){},285:function(e,t,n){},286:function(e,t,n){},287:function(e,t,n){},288:function(e,t,n){},289:function(e,t,n){},29:function(e,t){e.exports=window.ReactDOM},290:function(e,t,n){},291:function(e,t,n){"use strict";var r=n(0),o=n(8),i=Object(r.createElement)(o.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(r.createElement)(o.Path,{d:"M14.6 7l-1.2-1L8 12l5.4 6 1.2-1-4.6-5z"}));t.a=i},292:function(e,t){e.exports=function(e,t){if(null==e)return{};var n,r,o={},i=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o},e.exports.default=e.exports,e.exports.__esModule=!0},293:function(e,t,n){var r=n(294);e.exports=function(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,r(e,t)},e.exports.default=e.exports,e.exports.__esModule=!0},294:function(e,t){function n(t,r){return e.exports=n=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},e.exports.default=e.exports,e.exports.__esModule=!0,n(t,r)}e.exports=n,e.exports.default=e.exports,e.exports.__esModule=!0},295:function(e,t){e.exports=function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e},e.exports.default=e.exports,e.exports.__esModule=!0},296:function(e,t,n){},297:function(e,t){e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},298:function(e,t,n){},3:function(e,t){e.exports=window.wp.components},30:function(e,t){e.exports=window.wp.hooks},32:function(e,t,n){"use strict";var r=n(67),o=n(68),i=n(39);e.exports={formats:i,parse:o,stringify:r}},35:function(e,t){function n(){return e.exports=n=Object.assign||function(e){for(var t=1;t1;){var t=e.pop(),n=t.obj[t.prop];if(i(n)){for(var r=[],o=0;o=48&&u<=57||u>=65&&u<=90||u>=97&&u<=122||i===r.RFC1738&&(40===u||41===u)?s+=c.charAt(l):u<128?s+=a[u]:u<2048?s+=a[192|u>>6]+a[128|63&u]:u<55296||u>=57344?s+=a[224|u>>12]+a[128|u>>6&63]+a[128|63&u]:(l+=1,u=65536+((1023&u)<<10|1023&c.charCodeAt(l)),s+=a[240|u>>18]+a[128|u>>12&63]+a[128|u>>6&63]+a[128|63&u])}return s},isBuffer:function(e){return!(!e||"object"!=typeof e)&&!!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e))},isRegExp:function(e){return"[object RegExp]"===Object.prototype.toString.call(e)},maybeMap:function(e,t){if(i(e)){for(var n=[],r=0;r=0;m--){var p=a[m];"."===p?i(a,m):".."===p?(i(a,m),d++):d&&(i(a,m),d--)}if(!l)for(;d--;d)a.unshift("..");!l||""===a[0]||a[0]&&o(a[0])||a.unshift("");var f=a.join("/");return n&&"/"!==f.substr(-1)&&(f+="/"),f};function c(e){return e.valueOf?e.valueOf():Object.prototype.valueOf.call(e)}var s=function e(t,n){if(t===n)return!0;if(null==t||null==n)return!1;if(Array.isArray(t))return Array.isArray(n)&&t.length===n.length&&t.every((function(t,r){return e(t,n[r])}));if("object"==typeof t||"object"==typeof n){var r=c(t),o=c(n);return r!==t||o!==n?e(r,o):Object.keys(Object.assign({},t,n)).every((function(r){return e(t[r],n[r])}))}return!1},l=n(41);function u(e){return"/"===e.charAt(0)?e:"/"+e}function d(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function m(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,r=e.hash,o=t||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(o+="#"===r.charAt(0)?r:"#"+r),o}function f(e,t,n,o){var i;"string"==typeof e?(i=function(e){var t=e||"/",n="",r="",o=t.indexOf("#");-1!==o&&(r=t.substr(o),t=t.substr(0,o));var i=t.indexOf("?");return-1!==i&&(n=t.substr(i),t=t.substr(0,i)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e)).state=t:(void 0===(i=Object(r.a)({},e)).pathname&&(i.pathname=""),i.search?"?"!==i.search.charAt(0)&&(i.search="?"+i.search):i.search="",i.hash?"#"!==i.hash.charAt(0)&&(i.hash="#"+i.hash):i.hash="",void 0!==t&&void 0===i.state&&(i.state=t));try{i.pathname=decodeURI(i.pathname)}catch(e){throw e instanceof URIError?new URIError('Pathname "'+i.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):e}return n&&(i.key=n),o?i.pathname?"/"!==i.pathname.charAt(0)&&(i.pathname=a(i.pathname,o.pathname)):i.pathname=o.pathname:i.pathname||(i.pathname="/"),i}function h(e,t){return e.pathname===t.pathname&&e.search===t.search&&e.hash===t.hash&&e.key===t.key&&s(e.state,t.state)}function b(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,o){if(null!=e){var i="function"==typeof e?e(t,n):e;"string"==typeof i?"function"==typeof r?r(i,o):o(!0):o(!1!==i)}else o(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter((function(e){return e!==r}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;rt?n.splice(t,n.length-t,r):n.push(r),d({action:"PUSH",location:r,index:t,entries:n})}}))},replace:function(e,t){var r=f(e,t,m(),w.location);u.confirmTransitionTo(r,"REPLACE",n,(function(e){e&&(w.entries[w.index]=r,d({action:"REPLACE",location:r}))}))},go:y,goBack:function(){y(-1)},goForward:function(){y(1)},canGo:function(e){var t=w.index+e;return t>=0&&te.id||e.product)}function l(e,t,n){const r={};r.products=u(t,!0,n,e),r.remainingProducts=u(t,!1,n,e);const o=[...new Set([...r.products,...r.remainingProducts])];return r.uniqueItemsList=o.map(e=>{let t;return t=e.label?{type:"extension",name:e.label}:{type:"theme",name:e.title},t}),r}function u(e,t=!1,n,r){const o=[];if(!r)return o;(e.product_types||[]).forEach(e=>{r[e]&&r[e].product&&(t||!n.includes(r[e].slug))&&o.push(r[e])});const a=Object(i.f)("onboarding",{});let c=null;return a&&a.themes&&(c=a.themes.find(t=>t.slug===e.theme)),c&&c.id&&d(c.price)>0&&(t||!c.is_installed)&&o.push(c),o}function d(e){return Number(Object(r.decodeEntities)(e).replace(/[^0-9.-]+/g,""))}function m(e){return/admin.php\?page=wc-admin/.test(e)}},66:function(e,t,n){"use strict";n.d(t,"d",(function(){return i})),n.d(t,"c",(function(){return a})),n.d(t,"b",(function(){return c})),n.d(t,"a",(function(){return s}));var r=n(11),o=n(255);function i(e,t){const{getItemsTotalCount:n,getItemsError:o,isResolving:i}=e(r.ITEMS_STORE_NAME);if(!t.length)return 0;const a={page:1,per_page:1,status:t,_fields:["id"]},c=n("orders",a,null),s=Boolean(o("orders",a)),l=i("getItemsTotalCount",["orders",a,null]);return s||l?null:c}function a(e){const{getSetting:t}=e(r.SETTINGS_STORE_NAME),{woocommerce_actionable_order_statuses:n=o.a}=t("wc_admin","wcAdminSettings",{});return n}const c={page:1,per_page:1,low_in_stock:!0,status:"publish",_fields:["id"]};function s(e){const{getItemsTotalCount:t,getItemsError:n,isResolving:o}=e(r.ITEMS_STORE_NAME),i=t("products/low-in-stock",c,null),a=Boolean(n("products/low-in-stock",c)),s=o("getItemsTotalCount",["products/low-in-stock",c,null]);return a||s&&null===i?null:i}},67:function(e,t,n){"use strict";var r=n(49),o=n(39),i=Object.prototype.hasOwnProperty,a={brackets:function(e){return e+"[]"},comma:"comma",indices:function(e,t){return e+"["+t+"]"},repeat:function(e){return e}},c=Array.isArray,s=Array.prototype.push,l=function(e,t){s.apply(e,c(t)?t:[t])},u=Date.prototype.toISOString,d=o.default,m={addQueryPrefix:!1,allowDots:!1,charset:"utf-8",charsetSentinel:!1,delimiter:"&",encode:!0,encoder:r.encode,encodeValuesOnly:!1,format:d,formatter:o.formatters[d],indices:!1,serializeDate:function(e){return u.call(e)},skipNulls:!1,strictNullHandling:!1},p=function e(t,n,o,i,a,s,u,d,p,f,h,b,v,g){var y,w=t;if("function"==typeof u?w=u(n,w):w instanceof Date?w=f(w):"comma"===o&&c(w)&&(w=r.maybeMap(w,(function(e){return e instanceof Date?f(e):e}))),null===w){if(i)return s&&!v?s(n,m.encoder,g,"key",h):n;w=""}if("string"==typeof(y=w)||"number"==typeof y||"boolean"==typeof y||"symbol"==typeof y||"bigint"==typeof y||r.isBuffer(w))return s?[b(v?n:s(n,m.encoder,g,"key",h))+"="+b(s(w,m.encoder,g,"value",h))]:[b(n)+"="+b(String(w))];var O,j=[];if(void 0===w)return j;if("comma"===o&&c(w))O=[{value:w.length>0?w.join(",")||null:void 0}];else if(c(u))O=u;else{var _=Object.keys(w);O=d?_.sort(d):_}for(var E=0;E0?g+v:""}},68:function(e,t,n){"use strict";var r=n(49),o=Object.prototype.hasOwnProperty,i=Array.isArray,a={allowDots:!1,allowPrototypes:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decoder:r.decode,delimiter:"&",depth:5,ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictNullHandling:!1},c=function(e){return e.replace(/&#(\d+);/g,(function(e,t){return String.fromCharCode(parseInt(t,10))}))},s=function(e,t){return e&&"string"==typeof e&&t.comma&&e.indexOf(",")>-1?e.split(","):e},l=function(e,t,n,r){if(e){var i=n.allowDots?e.replace(/\.([^.[]+)/g,"[$1]"):e,a=/(\[[^[\]]*])/g,c=n.depth>0&&/(\[[^[\]]*])/.exec(i),l=c?i.slice(0,c.index):i,u=[];if(l){if(!n.plainObjects&&o.call(Object.prototype,l)&&!n.allowPrototypes)return;u.push(l)}for(var d=0;n.depth>0&&null!==(c=a.exec(i))&&d=0;--i){var a,c=e[i];if("[]"===c&&n.parseArrays)a=[].concat(o);else{a=n.plainObjects?Object.create(null):{};var l="["===c.charAt(0)&&"]"===c.charAt(c.length-1)?c.slice(1,-1):c,u=parseInt(l,10);n.parseArrays||""!==l?!isNaN(u)&&c!==l&&String(u)===l&&u>=0&&n.parseArrays&&u<=n.arrayLimit?(a=[])[u]=o:a[l]=o:a={0:o}}o=a}return o}(u,t,n,r)}};e.exports=function(e,t){var n=function(e){if(!e)return a;if(null!==e.decoder&&void 0!==e.decoder&&"function"!=typeof e.decoder)throw new TypeError("Decoder has to be a function.");if(void 0!==e.charset&&"utf-8"!==e.charset&&"iso-8859-1"!==e.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var t=void 0===e.charset?a.charset:e.charset;return{allowDots:void 0===e.allowDots?a.allowDots:!!e.allowDots,allowPrototypes:"boolean"==typeof e.allowPrototypes?e.allowPrototypes:a.allowPrototypes,arrayLimit:"number"==typeof e.arrayLimit?e.arrayLimit:a.arrayLimit,charset:t,charsetSentinel:"boolean"==typeof e.charsetSentinel?e.charsetSentinel:a.charsetSentinel,comma:"boolean"==typeof e.comma?e.comma:a.comma,decoder:"function"==typeof e.decoder?e.decoder:a.decoder,delimiter:"string"==typeof e.delimiter||r.isRegExp(e.delimiter)?e.delimiter:a.delimiter,depth:"number"==typeof e.depth||!1===e.depth?+e.depth:a.depth,ignoreQueryPrefix:!0===e.ignoreQueryPrefix,interpretNumericEntities:"boolean"==typeof e.interpretNumericEntities?e.interpretNumericEntities:a.interpretNumericEntities,parameterLimit:"number"==typeof e.parameterLimit?e.parameterLimit:a.parameterLimit,parseArrays:!1!==e.parseArrays,plainObjects:"boolean"==typeof e.plainObjects?e.plainObjects:a.plainObjects,strictNullHandling:"boolean"==typeof e.strictNullHandling?e.strictNullHandling:a.strictNullHandling}}(t);if(""===e||null==e)return n.plainObjects?Object.create(null):{};for(var u="string"==typeof e?function(e,t){var n,l={},u=t.ignoreQueryPrefix?e.replace(/^\?/,""):e,d=t.parameterLimit===1/0?void 0:t.parameterLimit,m=u.split(t.delimiter,d),p=-1,f=t.charset;if(t.charsetSentinel)for(n=0;n-1&&(b=i(b)?[b]:b),o.call(l,h)?l[h]=r.combine(l[h],b):l[h]=b}return l}(e,n):e,d=n.plainObjects?Object.create(null):{},m=Object.keys(u),p=0;pa:focus,.woocommerce-page #toplevel_page_woocommerce.menu-top>a:focus{padding-bottom:1px}}#wpbody,.woocommerce-layout *{box-sizing:border-box}#wpbody{display:inline-block;width:100%;padding-top:0;margin-top:60px}#wpfooter{display:none}.woocommerce_page_wc-admin .woocommerce-filters-date__content:not(.is-mobile){z-index:2}@media(max-width:600px){#wpadminbar{position:fixed}html.wp-toolbar{padding-top:46px}}@media(max-width:782px){.jetpack-masterbar #wpadminbar #wp-admin-bar-menu-toggle{margin-top:-10px}.jetpack-masterbar #wpwrap .woocommerce-layout__header-heading{padding-right:60px}.jetpack-masterbar.wp-admin .wrap h1,.jetpack-masterbar.wp-admin .wrap h2{padding-right:0}}.woocommerce-page .wp-has-current-submenu:after{left:0;content:" ";height:0;width:0;position:absolute;pointer-events:none;border:8px solid transparent;border-left-color:#f1f1f1;top:0;margin-top:10px}@media(max-width:960px){.woocommerce-page .wp-has-current-submenu:after{border-width:4px;margin-top:14px}}:root{--large-gap:40px;--main-gap:24px}@media(max-width:960px){:root{--large-gap:24px}}@media(max-width:782px){:root{--large-gap:16px;--main-gap:16px}}@keyframes loading-fade{0%{opacity:.7}50%{opacity:1}to{opacity:.7}}.woocommerce-layout select:hover{color:#1e1e1e}.woocommerce-layout select.components-select-control__input{max-width:100%;line-height:1}body.woocommerce-page .components-button.is-primary:not(:disabled),body.woocommerce-page .components-snackbar .components-button.is-tertiary,body.woocommerce-page .components-snackbar .components-button.is-tertiary:not(:disabled):not([aria-disabled=true]):hover{color:#fff}.woocommerce-embed-page #wpbody .woocommerce-layout,.woocommerce-embed-page .woocommerce-layout__notice-list-hide+.wrap{padding-top:10px}.woocommerce-embed-page #wpbody-content,.woocommerce-embed-page #wpcontent{overflow-x:initial!important}.woocommerce-embed-page #wpbody-content{padding-top:0}.woocommerce-embed-page #wpbody-content .notice{margin-top:15px}.woocommerce-embed-page .wrap{padding:0 20px}@media(max-width:782px){.woocommerce-embed-page .wrap p.search-box{width:calc(100% - 40px)}}.woocommerce-embed-page .wrap .wrap{padding:0}.woocommerce-embed-page #screen-meta{border-left:0;margin:0}.woocommerce-embed-page #screen-meta-links{position:relative}.woocommerce-embed-page .notice{padding:1px 12px}.woocommerce-embed-page .woocommerce-layout__header.is-scrolled{box-shadow:0 8px 16px 0 rgba(85,93,102,.3)}.woocommerce-embed-page .woocommerce-layout__header .woocommerce-layout__header-heading{margin-top:0;margin-bottom:0}.woocommerce-embed-page #screen-meta-links.is-hidden-by-notices,.woocommerce-embed-page #screen-meta.is-hidden-by-notices{display:none!important}.woocommerce-embed-page .woocommerce-layout__primary{margin:0}@media(max-width:782px){.woocommerce-embed-page .woocommerce-layout__primary{padding-top:10px}}@keyframes isLoaded{0%{opacity:0}to{opacity:1}}.woocommerce-embed-page .woocommerce-layout__activity-panel-tabs{animation:isLoaded;animation-duration:2s}.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:10px;margin-bottom:16px}@media(max-width:600px){.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:80px;margin-bottom:-16px}}@media(min-width:601px)and (max-width:782px){.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:32px}}.woocommerce-embed-page .woocommerce-activity-card__actions a.components-button:not(.is-primary){color:#757575}.woocommerce-layout{margin:0;padding:0}.woocommerce-layout__primary{margin:var(--large-gap) var(--large-gap) 0 0}@media(max-width:782px){.woocommerce-layout__primary{margin-top:20px}}.woocommerce-layout .woocommerce-layout__main{padding-left:40px;padding-left:var(--large-gap);max-width:100%}.woocommerce-admin-is-loading #adminmenumain,.woocommerce-admin-is-loading #wpadminbar,.woocommerce-admin-is-loading #wpbody-content,.woocommerce-admin-is-loading #wpcontent,.woocommerce-admin-is-loading #wpfooter,.woocommerce-admin-is-loading .components-modal__screen-overlay,.woocommerce-admin-is-loading .error,.woocommerce-admin-is-loading .notice,.woocommerce-admin-is-loading .update-nag,.woocommerce-admin-is-loading .updated,.woocommerce-admin-is-loading .woocommerce-layout__header,.woocommerce-admin-is-loading .woocommerce-message,.woocommerce-admin-is-loading .woocommerce-store-alerts,.woocommerce-page .update-nag{display:none}.woocommerce-admin-full-screen{background:#f6f7f7;color:#50575e;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}.woocommerce-admin-full-screen #wpwrap{top:0}.woocommerce-admin-full-screen #wpbody-content{min-height:100vh!important}.woocommerce-admin-full-screen #adminmenumain,.woocommerce-admin-full-screen #wpcontent>*,.woocommerce-admin-full-screen .error,.woocommerce-admin-full-screen .notice,.woocommerce-admin-full-screen .update-nag,.woocommerce-admin-full-screen .updated,.woocommerce-admin-full-screen .woocommerce-layout__header,.woocommerce-admin-full-screen .woocommerce-message,.woocommerce-admin-full-screen .woocommerce-store-alerts{display:none}.woocommerce-admin-full-screen #wpcontent{margin-right:0!important}.woocommerce-admin-full-screen #wpcontent>#wpbody{display:block;margin-top:0!important}.woocommerce-admin-full-screen.has-woocommerce-navigation #wpbody{padding-right:0}.is-wp-toolbar-disabled #wpadminbar{display:none!important}.wp-toolbar .is-wp-toolbar-disabled{margin-top:-32px}@media(max-width:600px){.wp-toolbar .is-wp-toolbar-disabled{margin-top:-46px}}.woocommerce-onboarding .muriel-component{margin-top:16px;margin-bottom:16px}.woocommerce-onboarding .components-base-control.has-error{margin-bottom:32px!important;border-color:#d63638}@media(max-width:782px){.woocommerce-onboarding .components-base-control.has-error{margin-bottom:44px!important}}.woocommerce-onboarding .components-base-control.has-error .components-base-control__help{top:100%;right:12px;position:absolute;margin-top:4px;font-size:12px;font-style:normal;color:#d63638}.woocommerce-onboarding .components-form-toggle{display:inline-block}.woocommerce-onboarding .components-form-toggle label{font-size:14px}.woocommerce-onboarding .components-form-toggle .components-base-control{display:inline-block}.woocommerce-onboarding .components-form-toggle .components-base-control__field{margin-bottom:0}.woocommerce-page .components-modal__frame .components-button.is-button,.woocommerce-profile-wizard__body .components-button.is-button,.woocommerce-task-dashboard__container .components-button.is-button{height:48px;padding-right:25px;padding-left:25px;text-align:center;font-size:14px;line-height:36px;font-weight:500;align-items:center}.woocommerce-page .components-modal__frame .components-button.is-button:disabled,.woocommerce-profile-wizard__body .components-button.is-button:disabled,.woocommerce-task-dashboard__container .components-button.is-button:disabled{cursor:not-allowed}.components-modal__header .components-button svg+span{display:none}.components-modal__frame.woocommerce-usage-modal{width:600px;max-width:100%}.components-modal__frame.woocommerce-usage-modal .components-modal__header{border-bottom:0;margin-bottom:0}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper{flex-grow:1;display:flex;flex-direction:column}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper a{color:#50575e}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper button.is-primary{align-self:flex-end}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__actions{display:flex;justify-content:flex-end;margin-top:16px}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__actions button{margin-right:16px}.woocommerce-payments__usage-modal .components-modal__header{height:auto;padding:24px 24px 0}.woocommerce-payments__usage-modal .components-modal__header .components-modal__header-heading{font-size:24px;line-height:32px;margin:0 0 24px}.woocommerce-payments__usage-modal .woocommerce-payments__usage-modal-message{padding:16px 0;font-size:16px;line-height:24px}.woocommerce-payments__usage-modal .woocommerce-payments__usage-footer{display:flex;justify-content:flex-end;padding:16px 0}.woocommerce-payments__usage-modal .woocommerce-payments__usage-footer button{margin-right:16px}.components-modal__frame.woocommerce-cart-modal{width:600px;max-width:100%}.components-modal__frame.woocommerce-cart-modal .components-modal__header{border-bottom:0;margin-bottom:16px;margin-top:16px}.components-modal__frame.woocommerce-cart-modal .components-modal__header button{display:none}.components-modal__frame.woocommerce-cart-modal .components-modal__header-heading{font-style:normal;font-weight:400;font-size:24px;line-height:32px}.components-modal__frame.woocommerce-cart-modal .woocommerce-list{margin-top:24px}.components-modal__frame.woocommerce-cart-modal .woocommerce-list .woocommerce-list__item:first-child{border-top:1px solid #dcdcde}.components-modal__frame.woocommerce-cart-modal .woocommerce-list__item{border-bottom:1px solid #dcdcde}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__help-text{font-size:16px;line-height:24px}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions{text-align:left}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions button.is-link{margin-left:16px;text-decoration:none;font-weight:600;font-size:14px}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions button.is-primary{align-self:flex-end}.woocommerce-layout__header{background:#fff;box-sizing:border-box;padding:0;position:fixed;width:calc(100% - 160px);top:32px;z-index:1001}.woocommerce-layout__header.is-scrolled{box-shadow:0 8px 8px 0 rgba(85,93,102,.3)}.woocommerce-layout__header .woocommerce-layout__header-wrapper{display:flex;align-items:center;min-height:60px}.woocommerce-layout__header .woocommerce-layout__header-back-button{cursor:pointer;margin-right:var(--large-gap);display:flex}.woocommerce-layout__header .woocommerce-layout__header-back-button:focus{box-shadow:inset 1px -1px 0 #757575,inset -1px 1px 0 #757575}@media(max-width:782px){.woocommerce-layout__header{flex-flow:row wrap;top:46px;width:100%}}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__header{width:calc(100% - 36px)}}.woocommerce-layout__header .woocommerce-layout__header-breadcrumbs-wrapper{display:flex;justify-content:space-between;flex-direction:row}.woocommerce-layout__header .woocommerce-layout__header-heading{display:flex;align-items:center;padding:0 40px 0 0;padding:0 var(--large-gap) 0 0;flex:1 auto;height:60px;background:#fff;font-weight:600;font-size:14px}.woocommerce-layout__header .woocommerce-layout__header-heading.with-back-button{padding-right:24px}.folded .woocommerce-layout__header{width:calc(100% - 36px)}@media(max-width:782px){.folded .woocommerce-layout__header{width:100%}}.is-wp-toolbar-disabled .woocommerce-layout__header{top:0}.has-woocommerce-navigation .woocommerce-layout__header{right:0;width:100%}.woocommerce-page #contextual-help-link-wrap,.woocommerce-page #screen-options-link-wrap{margin-top:-1px}.wp-responsive-open .woocommerce-layout__header{margin-right:2px}.woocommerce-layout__activity-panel{display:flex;flex-direction:row;align-items:center;height:60px}.woocommerce-layout__activity-panel-tabs{width:100%;display:flex;height:60px;justify-content:flex-end}.woocommerce-layout__activity-panel-tabs .dashicon,.woocommerce-layout__activity-panel-tabs .gridicon{width:100%}.woocommerce-layout__activity-panel-tabs svg{width:24px;height:24px}.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon{fill:none}.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon path,.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon rect{stroke:currentColor}.woocommerce-layout__activity-panel-tabs svg.setup-progress path:first-child{stroke:"#DCDCDE"}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__homescreen-display-options svg.woocommerce-layout__activity-panel-tab-icon{height:14px}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__homescreen-extension-tasklist-toggle{min-width:205px}.woocommerce-layout__activity-panel-tabs .components-icon-button{display:initial;text-indent:0;border-radius:0}.woocommerce-layout__activity-panel-tabs .components-icon-button.has-text svg{margin:0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab{display:flex;flex-direction:column;justify-content:center;align-items:center;position:relative;border:none;outline:none;cursor:pointer;background-color:#fff;max-width:min-content;min-width:80px;width:100%;height:60px;color:#757575;white-space:nowrap}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:before{background-color:#007cba;background-color:var(--wp-admin-theme-color);bottom:0;content:"";height:0;opacity:0;transition-property:height,opacity;transition-duration:.3s;transition-timing-function:ease-in-out;right:0;position:absolute;left:0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-active,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-opened{color:#1e1e1e;box-shadow:none}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-active:before,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-opened:before{height:3px;opacity:1}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{content:" ";position:absolute;padding:1px;background:#d94f4f;border:2px solid #fff;width:4px;height:4px;display:inline-block;border-radius:50%;top:8px;right:50%}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{left:18px;right:auto;margin-right:0}}@media(min-width:961px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{left:28px;right:auto;margin-right:0}}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover{background-color:#f0f0f0;box-shadow:none}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover.woocommerce-layout__activity-panel-tab-wordpress-notices:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover.woocommerce-layout__activity-panel-tab-wordpress-notices:after{border-color:#e0e0e0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):focus,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:focus{box-shadow:inset 1px -1px 0 #757575,inset -1px 1px 0 #757575}@media(max-width:782px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.display-options{display:none}}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-popover{margin-top:0;z-index:1001}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-popover .components-menu-group{padding:12px}.woocommerce-layout__activity-panel-toggle-bubble.has-unread:after{content:" ";position:absolute;padding:1px;background:#ca4a1f;border:2px solid #fff;width:4px;height:4px;display:inline-block;border-radius:50%;top:6px;left:4px}@keyframes tabSwitch{0%,to{transform:translateX(0)}50%{transform:translateX(-100px)}}.woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 106px);background:#f0f0f0;width:430px;transform:translateX(-100%);transition-property:transform box-shadow;transition-duration:.3s;transition-timing-function:ease-in-out;position:fixed;left:0;top:106px;z-index:1000;overflow-x:hidden;overflow-y:auto}@media(max-width:782px){.woocommerce-layout__activity-panel-wrapper{width:100%}}@media screen and (prefers-reduced-motion:reduce){.woocommerce-layout__activity-panel-wrapper{transition-duration:1ms}}@media(min-width:783px){.woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 92px);top:92px}}.has-woocommerce-navigation .woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 60px);top:60px}.woocommerce-layout__activity-panel-wrapper.is-open{transform:none;box-shadow:0 12px 12px 0 rgba(85,93,102,.3)}.woocommerce-layout__activity-panel-wrapper.is-switching{animation:tabSwitch;animation-duration:.3s}@media screen and (prefers-reduced-motion:reduce){.woocommerce-layout__activity-panel-wrapper.is-switching{animation:none}}.woocommerce-layout__activity-panel-wrapper .woocommerce-empty-content{padding-right:24px;padding-left:24px}.woocommerce-layout__activity-panel-avatar-flag-overlay{position:relative;top:-12px}.woocommerce-layout__activity-panel-avatar-flag-overlay .woocommerce-flag{position:relative;top:16px;border:2px solid #fff}.woocommerce-layout__notice-list-hide{display:none}.highlight-tooltip__container{position:absolute;width:0;height:0}.highlight-tooltip__container.highlight-tooltip__show{top:0;right:0;width:100%;height:100%}.highlight-tooltip__portal{width:100%;height:100%;position:relative}.highlight-tooltip__portal .highlight-tooltip__overlay{position:fixed;top:0;left:0;bottom:0;right:0;background-color:rgba(0,0,0,.35);z-index:100000;animation:edit-post__fade-in-animation .2s ease-out 0s;animation-fill-mode:forwards}@media(prefers-reduced-motion:reduce){.highlight-tooltip__portal .highlight-tooltip__overlay{animation-duration:1ms}}.highlight-tooltip__popover .components-card{min-width:360px}.highlight-tooltip__popover .components-card__header{font-size:16px;font-size:1rem;font-weight:600;box-sizing:border-box}.highlight-tooltip__popover .components-card__footer{justify-content:flex-end;box-sizing:border-box}.woocommerce-layout__show-app-banner{padding-top:56px}@media(min-width:783px){.woocommerce-layout__show-app-banner{padding-top:0}}.woocommerce-mobile-app-banner{background-color:#3c2861;width:100%;display:flex;height:56px;align-items:center;padding:0 4px 0 6px}@media(min-width:401px){.woocommerce-mobile-app-banner{padding:0 10px 0 13px}}@media(min-width:783px){.woocommerce-mobile-app-banner{display:none}}.woocommerce-mobile-app-banner .gridicon{fill:#fff;margin-left:10px}.woocommerce-mobile-app-banner .components-button.is-secondary{margin-right:auto;color:#fff;box-shadow:inset 0 0 0 1px #fff}.woocommerce-mobile-app-banner .components-button.is-secondary:active,.woocommerce-mobile-app-banner .components-button.is-secondary:hover{color:#fff;box-shadow:inset 0 0 0 1px #fff;background:none}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description{color:#fff;margin-right:8px}@media(min-width:401px){.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description{margin-right:13px}}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text{margin:0;font-size:10px}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text:first-child{font-weight:700}@media(min-width:401px){.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text{margin-right:13px;font-size:13px}}.woocommerce-navigation{display:grid;grid-template-rows:min-content 1fr;height:100%}.woocommerce-navigation .woocommerce-navigation__wrapper h2>span{width:100%}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__menu{overflow-y:auto;margin-bottom:0;padding-bottom:24px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__group+.components-navigation__group{margin-top:24px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item{margin-bottom:0}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item .components-button{opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:not(:hover) .components-button{color:#949494}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:hover .components-button{color:#ddd}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item.is-active .components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item a.components-button{padding:6px 16px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:not(:hover) a.components-button{color:#949494}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item.is-active a.components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation{height:100%}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation>div{height:100%;display:grid;grid-template-rows:1fr min-content}.woocommerce-navigation .woocommerce-navigation__wrapper.is-root .components-navigation__menu-secondary{border-top:1px solid #2c3338;margin:0 -8px;padding:16px 8px 12px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__group-title,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__menu-title{color:#f0f0f0;opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button{color:#949494;opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button span{font-size:13px;line-height:normal}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button:hover,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button:hover:not(:disabled){color:#ddd}.woocommerce-navigation-header{display:flex;align-items:center;border:none;border-radius:0;height:auto}.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button{padding:12px;height:60px;color:#fff}.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:focus,.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:hover,.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:not([aria-disabled=true]):active{color:#fff}.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button{padding-right:0;color:#ccc;font-weight:600}.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:active,.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:focus,.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:hover{color:#e0e0e0}.woocommerce-navigation{position:relative;width:240px;box-sizing:border-box;background-color:#1e1e1e;z-index:1100}@media(max-width:960px){.woocommerce-navigation{width:60px;height:60px}}.woocommerce-navigation .components-navigation{box-sizing:border-box}.woocommerce-navigation .components-navigation__menu-title{overflow:visible}.woocommerce-navigation__wrapper{background-color:#1e1e1e;position:absolute;top:60px;width:100%;height:calc(100vh - 92px);overflow-y:auto}.is-wp-toolbar-disabled .woocommerce-navigation__wrapper{height:calc(100vh - 60px)}body.is-wc-nav-expanded .woocommerce-navigation{width:240px;height:100%}body.is-wc-nav-expanded font>.xdebug-error{margin-right:256px}body.is-wc-nav-folded .woocommerce-navigation{width:60px;height:60px;overflow:hidden}body.is-wc-nav-folded .woocommerce-navigation .woocommerce-navigation-header>*{display:none}body.is-wc-nav-folded .woocommerce-navigation .woocommerce-navigation-header__site-icon{display:block}body.is-wc-nav-folded .woocommerce-navigation .components-navigation{display:none}body.is-wc-nav-folded .woocommerce-transient-notices{right:16px}body.is-wc-nav-folded #wpbody{padding-right:0}.has-woocommerce-navigation #adminmenuback,.has-woocommerce-navigation #adminmenuwrap{display:none!important}.has-woocommerce-navigation.woocommerce_page_wc-reports .woo-nav-tab-wrapper,.has-woocommerce-navigation.woocommerce_page_wc-settings .woo-nav-tab-wrapper,.has-woocommerce-navigation.woocommerce_page_wc-status .woo-nav-tab-wrapper{display:none}.has-woocommerce-navigation.woocommerce_page_wc-reports .woocommerce .subsubsub,.has-woocommerce-navigation.woocommerce_page_wc-settings .woocommerce .subsubsub,.has-woocommerce-navigation.woocommerce_page_wc-status .woocommerce .subsubsub{font-size:14px;margin:5px 0}.has-woocommerce-navigation #wpcontent,.has-woocommerce-navigation #wpfooter{margin-right:0}@media(max-width:960px){.has-woocommerce-navigation #wpcontent,.has-woocommerce-navigation #wpfooter{margin-right:0}}.has-woocommerce-navigation #wpbody{padding-right:240px}@media(max-width:960px){.has-woocommerce-navigation #wpbody{padding-right:0}}.has-woocommerce-navigation .woocommerce-layout__header.is-embed-loading:before{content:"";position:fixed;width:240px;height:100%;background:#1e1e1e}@media(max-width:960px){.has-woocommerce-navigation .woocommerce-layout__header.is-embed-loading:before{width:60px;height:60px}}.has-woocommerce-navigation #woocommerce-embedded-root.is-embed-loading{margin-bottom:-32px}.has-woocommerce-navigation:not(.is-wp-toolbar-disabled) #wpbody-content{margin-top:32px}.has-woocommerce-navigation font>.xdebug-error{margin-top:60px}.woocommerce-navigation-category-title{display:flex;align-items:center;font-size:20px;line-height:28px}.woocommerce-navigation-category-title .woocommerce-navigation-favorite-button{margin-right:auto}.woocommerce-navigation-favorite-button.components-button .star-empty-icon{color:#949494}.woocommerce-navigation-favorite-button.components-button .star-filled-icon{color:#ffb900}.woocommerce-transient-notices{position:fixed;bottom:12px;right:176px;z-index:99999;width:auto}@media(max-width:960px){.woocommerce-transient-notices{right:50px}}@media(max-width:782px){.woocommerce-transient-notices{right:16px}}.woocommerce-profile-wizard__body .woocommerce-transient-notices{right:unset}.woocommerce-profile-wizard__body .woocommerce-transient-notices .components-snackbar{margin-right:auto;margin-left:auto}.has-woocommerce-navigation .woocommerce-transient-notices{right:256px}.components-snackbar.components-snackbar-explicit-dismiss{cursor:default}.components-snackbar .components-snackbar__content-with-icon{margin-right:32px}.components-snackbar .components-snackbar__icon{position:absolute;top:24px;right:26px}.components-snackbar .components-snackbar__dismiss-button{margin-right:32px;cursor:pointer}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-embedded-layout__primary{padding:0 20px}.woocommerce-embedded-layout__primary .components-card__footer,.woocommerce-embedded-layout__primary .components-card__header{box-sizing:border-box}@media(max-width:782px){.woocommerce-embedded-layout__primary{padding:0}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/app/style.css b/packages/woocommerce-admin/dist/app/style.css new file mode 100644 index 0000000..3365a40 --- /dev/null +++ b/packages/woocommerce-admin/dist/app/style.css @@ -0,0 +1 @@ +.woocommerce-page .wrap{margin:0}.woocommerce-page #wpcontent,.woocommerce-page.woocommerce_page_wc-admin #wpbody-content{padding:0;overflow-x:hidden!important;min-height:calc(100vh - 32px)}@media(min-width:783px){.woocommerce-page #wpbody-content{padding-left:0}}@media(max-width:782px){.woocommerce-page .wp-responsive-open #woocommerce-embedded-root,.woocommerce-page .wp-responsive-open #wpbody{position:relative;right:-14.5em}.woocommerce-page #wpbody-content,.woocommerce-page #wpcontent{min-height:calc(100vh - 46px)}}@media(min-width:961px){.woocommerce-page #toplevel_page_wcadmin--analytics.menu-top>a:focus,.woocommerce-page #toplevel_page_woocommerce.menu-top>a:focus{padding-bottom:1px}}#wpbody,.woocommerce-layout *{box-sizing:border-box}#wpbody{display:inline-block;width:100%;padding-top:0;margin-top:60px}#wpfooter{display:none}.woocommerce_page_wc-admin .woocommerce-filters-date__content:not(.is-mobile){z-index:2}@media(max-width:600px){#wpadminbar{position:fixed}html.wp-toolbar{padding-top:46px}}@media(max-width:782px){.jetpack-masterbar #wpadminbar #wp-admin-bar-menu-toggle{margin-top:-10px}.jetpack-masterbar #wpwrap .woocommerce-layout__header-heading{padding-left:60px}.jetpack-masterbar.wp-admin .wrap h1,.jetpack-masterbar.wp-admin .wrap h2{padding-left:0}}.woocommerce-page .wp-has-current-submenu:after{right:0;content:" ";height:0;width:0;position:absolute;pointer-events:none;border:8px solid transparent;border-right-color:#f1f1f1;top:0;margin-top:10px}@media(max-width:960px){.woocommerce-page .wp-has-current-submenu:after{border-width:4px;margin-top:14px}}:root{--large-gap:40px;--main-gap:24px}@media(max-width:960px){:root{--large-gap:24px}}@media(max-width:782px){:root{--large-gap:16px;--main-gap:16px}}@keyframes loading-fade{0%{opacity:.7}50%{opacity:1}to{opacity:.7}}.woocommerce-layout select:hover{color:#1e1e1e}.woocommerce-layout select.components-select-control__input{max-width:100%;line-height:1}body.woocommerce-page .components-button.is-primary:not(:disabled),body.woocommerce-page .components-snackbar .components-button.is-tertiary,body.woocommerce-page .components-snackbar .components-button.is-tertiary:not(:disabled):not([aria-disabled=true]):hover{color:#fff}.woocommerce-embed-page #wpbody .woocommerce-layout,.woocommerce-embed-page .woocommerce-layout__notice-list-hide+.wrap{padding-top:10px}.woocommerce-embed-page #wpbody-content,.woocommerce-embed-page #wpcontent{overflow-x:initial!important}.woocommerce-embed-page #wpbody-content{padding-top:0}.woocommerce-embed-page #wpbody-content .notice{margin-top:15px}.woocommerce-embed-page .wrap{padding:0 20px}@media(max-width:782px){.woocommerce-embed-page .wrap p.search-box{width:calc(100% - 40px)}}.woocommerce-embed-page .wrap .wrap{padding:0}.woocommerce-embed-page #screen-meta{border-right:0;margin:0}.woocommerce-embed-page #screen-meta-links{position:relative}.woocommerce-embed-page .notice{padding:1px 12px}.woocommerce-embed-page .woocommerce-layout__header.is-scrolled{box-shadow:0 8px 16px 0 rgba(85,93,102,.3)}.woocommerce-embed-page .woocommerce-layout__header .woocommerce-layout__header-heading{margin-top:0;margin-bottom:0}.woocommerce-embed-page #screen-meta-links.is-hidden-by-notices,.woocommerce-embed-page #screen-meta.is-hidden-by-notices{display:none!important}.woocommerce-embed-page .woocommerce-layout__primary{margin:0}@media(max-width:782px){.woocommerce-embed-page .woocommerce-layout__primary{padding-top:10px}}@keyframes isLoaded{0%{opacity:0}to{opacity:1}}.woocommerce-embed-page .woocommerce-layout__activity-panel-tabs{animation:isLoaded;animation-duration:2s}.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:10px;margin-bottom:16px}@media(max-width:600px){.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:80px;margin-bottom:-16px}}@media(min-width:601px)and (max-width:782px){.woocommerce-embed-page .woocommerce-layout__notice-list-show{margin-top:32px}}.woocommerce-embed-page .woocommerce-activity-card__actions a.components-button:not(.is-primary){color:#757575}.woocommerce-layout{margin:0;padding:0}.woocommerce-layout__primary{margin:var(--large-gap) 0 0 var(--large-gap)}@media(max-width:782px){.woocommerce-layout__primary{margin-top:20px}}.woocommerce-layout .woocommerce-layout__main{padding-right:40px;padding-right:var(--large-gap);max-width:100%}.woocommerce-admin-is-loading #adminmenumain,.woocommerce-admin-is-loading #wpadminbar,.woocommerce-admin-is-loading #wpbody-content,.woocommerce-admin-is-loading #wpcontent,.woocommerce-admin-is-loading #wpfooter,.woocommerce-admin-is-loading .components-modal__screen-overlay,.woocommerce-admin-is-loading .error,.woocommerce-admin-is-loading .notice,.woocommerce-admin-is-loading .update-nag,.woocommerce-admin-is-loading .updated,.woocommerce-admin-is-loading .woocommerce-layout__header,.woocommerce-admin-is-loading .woocommerce-message,.woocommerce-admin-is-loading .woocommerce-store-alerts,.woocommerce-page .update-nag{display:none}.woocommerce-admin-full-screen{background:#f6f7f7;color:#50575e;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif}.woocommerce-admin-full-screen #wpwrap{top:0}.woocommerce-admin-full-screen #wpbody-content{min-height:100vh!important}.woocommerce-admin-full-screen #adminmenumain,.woocommerce-admin-full-screen #wpcontent>*,.woocommerce-admin-full-screen .error,.woocommerce-admin-full-screen .notice,.woocommerce-admin-full-screen .update-nag,.woocommerce-admin-full-screen .updated,.woocommerce-admin-full-screen .woocommerce-layout__header,.woocommerce-admin-full-screen .woocommerce-message,.woocommerce-admin-full-screen .woocommerce-store-alerts{display:none}.woocommerce-admin-full-screen #wpcontent{margin-left:0!important}.woocommerce-admin-full-screen #wpcontent>#wpbody{display:block;margin-top:0!important}.woocommerce-admin-full-screen.has-woocommerce-navigation #wpbody{padding-left:0}.is-wp-toolbar-disabled #wpadminbar{display:none!important}.wp-toolbar .is-wp-toolbar-disabled{margin-top:-32px}@media(max-width:600px){.wp-toolbar .is-wp-toolbar-disabled{margin-top:-46px}}.woocommerce-onboarding .muriel-component{margin-top:16px;margin-bottom:16px}.woocommerce-onboarding .components-base-control.has-error{margin-bottom:32px!important;border-color:#d63638}@media(max-width:782px){.woocommerce-onboarding .components-base-control.has-error{margin-bottom:44px!important}}.woocommerce-onboarding .components-base-control.has-error .components-base-control__help{top:100%;left:12px;position:absolute;margin-top:4px;font-size:12px;font-style:normal;color:#d63638}.woocommerce-onboarding .components-form-toggle{display:inline-block}.woocommerce-onboarding .components-form-toggle label{font-size:14px}.woocommerce-onboarding .components-form-toggle .components-base-control{display:inline-block}.woocommerce-onboarding .components-form-toggle .components-base-control__field{margin-bottom:0}.woocommerce-page .components-modal__frame .components-button.is-button,.woocommerce-profile-wizard__body .components-button.is-button,.woocommerce-task-dashboard__container .components-button.is-button{height:48px;padding-left:25px;padding-right:25px;text-align:center;font-size:14px;line-height:36px;font-weight:500;align-items:center}.woocommerce-page .components-modal__frame .components-button.is-button:disabled,.woocommerce-profile-wizard__body .components-button.is-button:disabled,.woocommerce-task-dashboard__container .components-button.is-button:disabled{cursor:not-allowed}.components-modal__header .components-button svg+span{display:none}.components-modal__frame.woocommerce-usage-modal{width:600px;max-width:100%}.components-modal__frame.woocommerce-usage-modal .components-modal__header{border-bottom:0;margin-bottom:0}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper{flex-grow:1;display:flex;flex-direction:column}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper a{color:#50575e}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__wrapper button.is-primary{align-self:flex-end}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__actions{display:flex;justify-content:flex-end;margin-top:16px}.components-modal__frame.woocommerce-usage-modal .woocommerce-usage-modal__actions button{margin-left:16px}.woocommerce-payments__usage-modal .components-modal__header{height:auto;padding:24px 24px 0}.woocommerce-payments__usage-modal .components-modal__header .components-modal__header-heading{font-size:24px;line-height:32px;margin:0 0 24px}.woocommerce-payments__usage-modal .woocommerce-payments__usage-modal-message{padding:16px 0;font-size:16px;line-height:24px}.woocommerce-payments__usage-modal .woocommerce-payments__usage-footer{display:flex;justify-content:flex-end;padding:16px 0}.woocommerce-payments__usage-modal .woocommerce-payments__usage-footer button{margin-left:16px}.components-modal__frame.woocommerce-cart-modal{width:600px;max-width:100%}.components-modal__frame.woocommerce-cart-modal .components-modal__header{border-bottom:0;margin-bottom:16px;margin-top:16px}.components-modal__frame.woocommerce-cart-modal .components-modal__header button{display:none}.components-modal__frame.woocommerce-cart-modal .components-modal__header-heading{font-style:normal;font-weight:400;font-size:24px;line-height:32px}.components-modal__frame.woocommerce-cart-modal .woocommerce-list{margin-top:24px}.components-modal__frame.woocommerce-cart-modal .woocommerce-list .woocommerce-list__item:first-child{border-top:1px solid #dcdcde}.components-modal__frame.woocommerce-cart-modal .woocommerce-list__item{border-bottom:1px solid #dcdcde}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__help-text{font-size:16px;line-height:24px}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions{text-align:right}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions button.is-link{margin-right:16px;text-decoration:none;font-weight:600;font-size:14px}.components-modal__frame.woocommerce-cart-modal .woocommerce-cart-modal__actions button.is-primary{align-self:flex-end}.woocommerce-layout__header{background:#fff;box-sizing:border-box;padding:0;position:fixed;width:calc(100% - 160px);top:32px;z-index:1001}.woocommerce-layout__header.is-scrolled{box-shadow:0 8px 8px 0 rgba(85,93,102,.3)}.woocommerce-layout__header .woocommerce-layout__header-wrapper{display:flex;align-items:center;min-height:60px}.woocommerce-layout__header .woocommerce-layout__header-back-button{cursor:pointer;margin-left:var(--large-gap);display:flex}.woocommerce-layout__header .woocommerce-layout__header-back-button:focus{box-shadow:inset -1px -1px 0 #757575,inset 1px 1px 0 #757575}@media(max-width:782px){.woocommerce-layout__header{flex-flow:row wrap;top:46px;width:100%}}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__header{width:calc(100% - 36px)}}.woocommerce-layout__header .woocommerce-layout__header-breadcrumbs-wrapper{display:flex;justify-content:space-between;flex-direction:row}.woocommerce-layout__header .woocommerce-layout__header-heading{display:flex;align-items:center;padding:0 0 0 40px;padding:0 0 0 var(--large-gap);flex:1 auto;height:60px;background:#fff;font-weight:600;font-size:14px}.woocommerce-layout__header .woocommerce-layout__header-heading.with-back-button{padding-left:24px}.folded .woocommerce-layout__header{width:calc(100% - 36px)}@media(max-width:782px){.folded .woocommerce-layout__header{width:100%}}.is-wp-toolbar-disabled .woocommerce-layout__header{top:0}.has-woocommerce-navigation .woocommerce-layout__header{left:0;width:100%}.woocommerce-page #contextual-help-link-wrap,.woocommerce-page #screen-options-link-wrap{margin-top:-1px}.wp-responsive-open .woocommerce-layout__header{margin-left:2px}.woocommerce-layout__activity-panel{display:flex;flex-direction:row;align-items:center;height:60px}.woocommerce-layout__activity-panel-tabs{width:100%;display:flex;height:60px;justify-content:flex-end}.woocommerce-layout__activity-panel-tabs .dashicon,.woocommerce-layout__activity-panel-tabs .gridicon{width:100%}.woocommerce-layout__activity-panel-tabs svg{width:24px;height:24px}.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon{fill:none}.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon path,.woocommerce-layout__activity-panel-tabs svg.woocommerce-layout__activity-panel-tab-icon rect{stroke:currentColor}.woocommerce-layout__activity-panel-tabs svg.setup-progress path:first-child{stroke:"#DCDCDE"}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__homescreen-display-options svg.woocommerce-layout__activity-panel-tab-icon{height:14px}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__homescreen-extension-tasklist-toggle{min-width:205px}.woocommerce-layout__activity-panel-tabs .components-icon-button{display:initial;text-indent:0;border-radius:0}.woocommerce-layout__activity-panel-tabs .components-icon-button.has-text svg{margin:0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab{display:flex;flex-direction:column;justify-content:center;align-items:center;position:relative;border:none;outline:none;cursor:pointer;background-color:#fff;max-width:min-content;min-width:80px;width:100%;height:60px;color:#757575;white-space:nowrap}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:before{background-color:#007cba;background-color:var(--wp-admin-theme-color);bottom:0;content:"";height:0;opacity:0;transition-property:height,opacity;transition-duration:.3s;transition-timing-function:ease-in-out;left:0;position:absolute;right:0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-active,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-opened{color:#1e1e1e;box-shadow:none}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-active:before,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.is-opened:before{height:3px;opacity:1}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{content:" ";position:absolute;padding:1px;background:#d94f4f;border:2px solid #fff;width:4px;height:4px;display:inline-block;border-radius:50%;top:8px;left:50%}@media(min-width:783px)and (max-width:960px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{right:18px;left:auto;margin-left:0}}@media(min-width:961px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.woocommerce-layout__activity-panel-tab-wordpress-notices:after{right:28px;left:auto;margin-left:0}}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover{background-color:#f0f0f0;box-shadow:none}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):hover.woocommerce-layout__activity-panel-tab-wordpress-notices:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover.has-unread:after,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:hover.woocommerce-layout__activity-panel-tab-wordpress-notices:after{border-color:#e0e0e0}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.components-button:not(:disabled):not([aria-disabled=true]):focus,.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab:focus{box-shadow:inset -1px -1px 0 #757575,inset 1px 1px 0 #757575}@media(max-width:782px){.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-tab.display-options{display:none}}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-popover{margin-top:0;z-index:1001}.woocommerce-layout__activity-panel-tabs .woocommerce-layout__activity-panel-popover .components-menu-group{padding:12px}.woocommerce-layout__activity-panel-toggle-bubble.has-unread:after{content:" ";position:absolute;padding:1px;background:#ca4a1f;border:2px solid #fff;width:4px;height:4px;display:inline-block;border-radius:50%;top:6px;right:4px}@keyframes tabSwitch{0%,to{transform:translateX(0)}50%{transform:translateX(100px)}}.woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 106px);background:#f0f0f0;width:430px;transform:translateX(100%);transition-property:transform box-shadow;transition-duration:.3s;transition-timing-function:ease-in-out;position:fixed;right:0;top:106px;z-index:1000;overflow-x:hidden;overflow-y:auto}@media(max-width:782px){.woocommerce-layout__activity-panel-wrapper{width:100%}}@media screen and (prefers-reduced-motion:reduce){.woocommerce-layout__activity-panel-wrapper{transition-duration:1ms}}@media(min-width:783px){.woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 92px);top:92px}}.has-woocommerce-navigation .woocommerce-layout__activity-panel-wrapper{height:calc(100vh - 60px);top:60px}.woocommerce-layout__activity-panel-wrapper.is-open{transform:none;box-shadow:0 12px 12px 0 rgba(85,93,102,.3)}.woocommerce-layout__activity-panel-wrapper.is-switching{animation:tabSwitch;animation-duration:.3s}@media screen and (prefers-reduced-motion:reduce){.woocommerce-layout__activity-panel-wrapper.is-switching{animation:none}}.woocommerce-layout__activity-panel-wrapper .woocommerce-empty-content{padding-left:24px;padding-right:24px}.woocommerce-layout__activity-panel-avatar-flag-overlay{position:relative;top:-12px}.woocommerce-layout__activity-panel-avatar-flag-overlay .woocommerce-flag{position:relative;top:16px;border:2px solid #fff}.woocommerce-layout__notice-list-hide{display:none}.highlight-tooltip__container{position:absolute;width:0;height:0}.highlight-tooltip__container.highlight-tooltip__show{top:0;left:0;width:100%;height:100%}.highlight-tooltip__portal{width:100%;height:100%;position:relative}.highlight-tooltip__portal .highlight-tooltip__overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:rgba(0,0,0,.35);z-index:100000;animation:edit-post__fade-in-animation .2s ease-out 0s;animation-fill-mode:forwards}@media(prefers-reduced-motion:reduce){.highlight-tooltip__portal .highlight-tooltip__overlay{animation-duration:1ms}}.highlight-tooltip__popover .components-card{min-width:360px}.highlight-tooltip__popover .components-card__header{font-size:16px;font-size:1rem;font-weight:600;box-sizing:border-box}.highlight-tooltip__popover .components-card__footer{justify-content:flex-end;box-sizing:border-box}.woocommerce-layout__show-app-banner{padding-top:56px}@media(min-width:783px){.woocommerce-layout__show-app-banner{padding-top:0}}.woocommerce-mobile-app-banner{background-color:#3c2861;width:100%;display:flex;height:56px;align-items:center;padding:0 6px 0 4px}@media(min-width:401px){.woocommerce-mobile-app-banner{padding:0 13px 0 10px}}@media(min-width:783px){.woocommerce-mobile-app-banner{display:none}}.woocommerce-mobile-app-banner .gridicon{fill:#fff;margin-right:10px}.woocommerce-mobile-app-banner .components-button.is-secondary{margin-left:auto;color:#fff;box-shadow:inset 0 0 0 1px #fff}.woocommerce-mobile-app-banner .components-button.is-secondary:active,.woocommerce-mobile-app-banner .components-button.is-secondary:hover{color:#fff;box-shadow:inset 0 0 0 1px #fff;background:none}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description{color:#fff;margin-left:8px}@media(min-width:401px){.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description{margin-left:13px}}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text{margin:0;font-size:10px}.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text:first-child{font-weight:700}@media(min-width:401px){.woocommerce-mobile-app-banner .woocommerce-mobile-app-banner__description .woocommerce-mobile-app-banner__description__text{margin-left:13px;font-size:13px}}.woocommerce-navigation{display:grid;grid-template-rows:min-content 1fr;height:100%}.woocommerce-navigation .woocommerce-navigation__wrapper h2>span{width:100%}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__menu{overflow-y:auto;margin-bottom:0;padding-bottom:24px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__group+.components-navigation__group{margin-top:24px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item{margin-bottom:0}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item .components-button{opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:not(:hover) .components-button{color:#949494}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:hover .components-button{color:#ddd}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item.is-active .components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item a.components-button{padding:6px 16px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item:not(:hover) a.components-button{color:#949494}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__item.is-active a.components-button{color:#fff}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation{height:100%}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation>div{height:100%;display:grid;grid-template-rows:1fr min-content}.woocommerce-navigation .woocommerce-navigation__wrapper.is-root .components-navigation__menu-secondary{border-top:1px solid #2c3338;margin:0 -8px;padding:16px 8px 12px}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__group-title,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__menu-title{color:#f0f0f0;opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button{color:#949494;opacity:1}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button span{font-size:13px;line-height:normal}.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button:hover,.woocommerce-navigation .woocommerce-navigation__wrapper .components-navigation__back-button:hover:not(:disabled){color:#ddd}.woocommerce-navigation-header{display:flex;align-items:center;border:none;border-radius:0;height:auto}.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button{padding:12px;height:60px;color:#fff}.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:focus,.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:hover,.woocommerce-navigation-header .woocommerce-navigation-header__site-icon.components-button:not([aria-disabled=true]):active{color:#fff}.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button{padding-left:0;color:#ccc;font-weight:600}.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:active,.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:focus,.woocommerce-navigation-header .woocommerce-navigation-header__site-title.components-button:hover{color:#e0e0e0}.woocommerce-navigation{position:relative;width:240px;box-sizing:border-box;background-color:#1e1e1e;z-index:1100}@media(max-width:960px){.woocommerce-navigation{width:60px;height:60px}}.woocommerce-navigation .components-navigation{box-sizing:border-box}.woocommerce-navigation .components-navigation__menu-title{overflow:visible}.woocommerce-navigation__wrapper{background-color:#1e1e1e;position:absolute;top:60px;width:100%;height:calc(100vh - 92px);overflow-y:auto}.is-wp-toolbar-disabled .woocommerce-navigation__wrapper{height:calc(100vh - 60px)}body.is-wc-nav-expanded .woocommerce-navigation{width:240px;height:100%}body.is-wc-nav-expanded font>.xdebug-error{margin-left:256px}body.is-wc-nav-folded .woocommerce-navigation{width:60px;height:60px;overflow:hidden}body.is-wc-nav-folded .woocommerce-navigation .woocommerce-navigation-header>*{display:none}body.is-wc-nav-folded .woocommerce-navigation .woocommerce-navigation-header__site-icon{display:block}body.is-wc-nav-folded .woocommerce-navigation .components-navigation{display:none}body.is-wc-nav-folded .woocommerce-transient-notices{left:16px}body.is-wc-nav-folded #wpbody{padding-left:0}.has-woocommerce-navigation #adminmenuback,.has-woocommerce-navigation #adminmenuwrap{display:none!important}.has-woocommerce-navigation.woocommerce_page_wc-reports .woo-nav-tab-wrapper,.has-woocommerce-navigation.woocommerce_page_wc-settings .woo-nav-tab-wrapper,.has-woocommerce-navigation.woocommerce_page_wc-status .woo-nav-tab-wrapper{display:none}.has-woocommerce-navigation.woocommerce_page_wc-reports .woocommerce .subsubsub,.has-woocommerce-navigation.woocommerce_page_wc-settings .woocommerce .subsubsub,.has-woocommerce-navigation.woocommerce_page_wc-status .woocommerce .subsubsub{font-size:14px;margin:5px 0}.has-woocommerce-navigation #wpcontent,.has-woocommerce-navigation #wpfooter{margin-left:0}@media(max-width:960px){.has-woocommerce-navigation #wpcontent,.has-woocommerce-navigation #wpfooter{margin-left:0}}.has-woocommerce-navigation #wpbody{padding-left:240px}@media(max-width:960px){.has-woocommerce-navigation #wpbody{padding-left:0}}.has-woocommerce-navigation .woocommerce-layout__header.is-embed-loading:before{content:"";position:fixed;width:240px;height:100%;background:#1e1e1e}@media(max-width:960px){.has-woocommerce-navigation .woocommerce-layout__header.is-embed-loading:before{width:60px;height:60px}}.has-woocommerce-navigation #woocommerce-embedded-root.is-embed-loading{margin-bottom:-32px}.has-woocommerce-navigation:not(.is-wp-toolbar-disabled) #wpbody-content{margin-top:32px}.has-woocommerce-navigation font>.xdebug-error{margin-top:60px}.woocommerce-navigation-category-title{display:flex;align-items:center;font-size:20px;line-height:28px}.woocommerce-navigation-category-title .woocommerce-navigation-favorite-button{margin-left:auto}.woocommerce-navigation-favorite-button.components-button .star-empty-icon{color:#949494}.woocommerce-navigation-favorite-button.components-button .star-filled-icon{color:#ffb900}.woocommerce-transient-notices{position:fixed;bottom:12px;left:176px;z-index:99999;width:auto}@media(max-width:960px){.woocommerce-transient-notices{left:50px}}@media(max-width:782px){.woocommerce-transient-notices{left:16px}}.woocommerce-profile-wizard__body .woocommerce-transient-notices{left:unset}.woocommerce-profile-wizard__body .woocommerce-transient-notices .components-snackbar{margin-left:auto;margin-right:auto}.has-woocommerce-navigation .woocommerce-transient-notices{left:256px}.components-snackbar.components-snackbar-explicit-dismiss{cursor:default}.components-snackbar .components-snackbar__content-with-icon{margin-left:32px}.components-snackbar .components-snackbar__icon{position:absolute;top:24px;left:26px}.components-snackbar .components-snackbar__dismiss-button{margin-left:32px;cursor:pointer}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-embedded-layout__primary{padding:0 20px}.woocommerce-embedded-layout__primary .components-card__footer,.woocommerce-embedded-layout__primary .components-card__header{box-sizing:border-box}@media(max-width:782px){.woocommerce-embedded-layout__primary{padding:0}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/beta-features-tracking-modal/style-rtl.css b/packages/woocommerce-admin/dist/beta-features-tracking-modal/style-rtl.css new file mode 100644 index 0000000..f5673b0 --- /dev/null +++ b/packages/woocommerce-admin/dist/beta-features-tracking-modal/style-rtl.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-beta-features-tracking-modal__actions{text-align:left;margin-top:24px}.woocommerce-beta-features-tracking-modal__actions .components-button.is-primary{margin-right:16px}.woocommerce-beta-features-tracking-modal__checkbox{padding:16px 0} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/beta-features-tracking-modal/style.css b/packages/woocommerce-admin/dist/beta-features-tracking-modal/style.css new file mode 100644 index 0000000..a33fe92 --- /dev/null +++ b/packages/woocommerce-admin/dist/beta-features-tracking-modal/style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-beta-features-tracking-modal__actions{text-align:right;margin-top:24px}.woocommerce-beta-features-tracking-modal__actions .components-button.is-primary{margin-left:16px}.woocommerce-beta-features-tracking-modal__checkbox{padding:16px 0} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/0.js b/packages/woocommerce-admin/dist/chunks/0.js new file mode 100644 index 0000000..a9ea151 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/0.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[0],{501:function(e,t,r){"use strict";r.d(t,"b",(function(){return l})),r.d(t,"a",(function(){return d}));var a=r(0),s=r(30),n=r(89),o=r.n(n),i=r(13);const c=o()(i.a),l=e=>{const t=c.getCurrencyConfig(),r=Object(s.applyFilters)("woocommerce_admin_report_currency",t,e);return o()(r)},d=Object(a.createContext)(c)},504:function(e,t,r){"use strict";var a=r(0),s=r(2),n=r(1),o=r.n(n),i=r(21);function c({className:e}){const t=Object(s.__)("There was an error getting your stats. Please try again.",'woocommerce'),r=Object(s.__)("Reload",'woocommerce');return Object(a.createElement)(i.EmptyContent,{className:e,title:t,actionLabel:r,actionCallback:()=>{window.location.reload()}})}c.propTypes={className:o.a.string},t.a=c},505:function(e,t,r){"use strict";var a=r(0),s=r(14),n=r(1),o=r.n(n),i=r(4),c=r(7),l=r(21),d=r(13),m=r(11),u=r(19),p=r(16),b=r(501),y=r(55);class g extends a.Component{constructor(){super(),this.onDateSelect=this.onDateSelect.bind(this),this.onFilterSelect=this.onFilterSelect.bind(this),this.onAdvancedFilterAction=this.onAdvancedFilterAction.bind(this)}onDateSelect(e){const{report:t,addCesSurveyForAnalytics:r}=this.props;r(),Object(p.recordEvent)("datepicker_update",{report:t,...Object(i.omitBy)(e,i.isUndefined)})}onFilterSelect(e){const{report:t,addCesSurveyForAnalytics:r}=this.props,a=e.filter||e["filter-variations"];["single_product","single_category","single_coupon","single_variation"].includes(a)&&r();const s={report:t,filter:e.filter||"all"};"single_product"===e.filter&&(s.filter_variation=e["filter-variations"]||"all"),Object(p.recordEvent)("analytics_filter",s)}onAdvancedFilterAction(e,t){const{report:r,addCesSurveyForAnalytics:a}=this.props;switch(e){case"add":Object(p.recordEvent)("analytics_filters_add",{report:r,filter:t.key});break;case"remove":Object(p.recordEvent)("analytics_filters_remove",{report:r,filter:t.key});break;case"filter":const e=Object.keys(t).reduce((e,r)=>(e[Object(i.snakeCase)(r)]=t[r],e),{});a(),Object(p.recordEvent)("analytics_filters_filter",{report:r,...e});break;case"clear_all":Object(p.recordEvent)("analytics_filters_clear_all",{report:r});break;case"match":Object(p.recordEvent)("analytics_filters_all_any",{report:r,value:t.match})}}render(){const{advancedFilters:e,filters:t,path:r,query:s,showDatePicker:n,defaultDateRange:o}=this.props,{period:i,compare:c,before:m,after:p}=Object(u.getDateParamsFromQuery)(s,o),{primary:b,secondary:y}=Object(u.getCurrentDates)(s,o),g={period:i,compare:c,before:m,after:p,primaryDate:b,secondaryDate:y},h=this.context;return Object(a.createElement)(l.ReportFilters,{query:s,siteLocale:d.b.siteLocale,currency:h.getCurrencyConfig(),path:r,filters:t,advancedFilters:e,showDatePicker:n,onDateSelect:this.onDateSelect,onFilterSelect:this.onFilterSelect,onAdvancedFilterAction:this.onAdvancedFilterAction,dateQuery:g,isoDateFormat:u.isoDateFormat})}}g.contextType=b.a,t.a=Object(s.compose)(Object(c.withSelect)(e=>{const{woocommerce_default_date_range:t}=e(m.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");return{defaultDateRange:t}}),Object(c.withDispatch)(e=>{const{addCesSurveyForAnalytics:t}=e(y.c);return{addCesSurveyForAnalytics:t}}))(g),g.propTypes={advancedFilters:o.a.object,filters:o.a.array,path:o.a.string.isRequired,query:o.a.object,showDatePicker:o.a.bool,report:o.a.string.isRequired}},506:function(e,t,r){"use strict";var a=r(35),s=r.n(a),n=r(0),o=r(3),i=r(30),c=r(14),l=r(91),d=r(7),m=r(4),u=r(2),p=r(1),b=r.n(p),y=r(21),g=r(12),h=r(474),f=r(11),_=r(16),O=()=>Object(n.createElement)("svg",{role:"img","aria-hidden":"true",focusable:"false",version:"1.1",xmlns:"http://www.w3.org/2000/svg",x:"0px",y:"0px",viewBox:"0 0 24 24"},Object(n.createElement)("path",{d:"M18,9c-0.009,0-0.017,0.002-0.025,0.003C17.72,5.646,14.922,3,11.5,3C7.91,3,5,5.91,5,9.5c0,0.524,0.069,1.031,0.186,1.519 C5.123,11.016,5.064,11,5,11c-2.209,0-4,1.791-4,4c0,1.202,0.541,2.267,1.38,3h18.593C22.196,17.089,23,15.643,23,14 C23,11.239,20.761,9,18,9z M12,16l-4-5h3V8h2v3h3L12,16z"})),j=r(504);var v=r(55);r(515);const C=e=>{const{getHeadersContent:t,getRowsContent:r,getSummary:a,isRequesting:c,primaryData:d,tableData:p,endpoint:b,itemIdField:v,tableQuery:C,compareBy:w,compareParam:R,searchBy:S,labels:D={},...E}=e,{query:q,columnPrefsKey:F}=e,{items:T,query:k}=p,x=q[R]?Object(g.getIdsFromQuery)(q[w]):[],[P,A]=Object(n.useState)(x),N=Object(n.useRef)(null),{updateUserPreferences:I,...Q}=Object(f.useUserPreferences)();if(p.isError||d.isError)return Object(n.createElement)(j.a,null);let B=[];F&&(B=Q&&Q[F]?Q[F]:B);const L=(e,s,n)=>{const o=a?a(s,n):null;return Object(i.applyFilters)("woocommerce_admin_report_table",{endpoint:b,headers:t(),rows:r(e),totals:s,summary:o,items:T})},V=t=>{const{ids:r}=e;A(t?r:[])},M=(t,r)=>{const{ids:a}=e;if(r)A(Object(m.uniq)([a[t],...P]));else{const e=P.indexOf(a[t]);A([...P.slice(0,e),...P.slice(e+1)])}},H=t=>{const{ids:r=[]}=e,a=-1!==P.indexOf(r[t]);return{display:Object(n.createElement)(o.CheckboxControl,{onChange:Object(m.partial)(M,t),checked:a}),value:!1}},Y=()=>{const{ids:t=[]}=e,r=t.length>0,a=r&&t.length===P.length;return{cellClassName:"is-checkbox-column",key:"compare",label:Object(n.createElement)(o.CheckboxControl,{onChange:V,"aria-label":Object(u.__)("Select All"),checked:a,disabled:!r}),required:!0}},U=c||p.isRequesting||d.isRequesting,G=Object(m.get)(d,["data","totals"],{}),$=T.totalResults||0,z=$>0,J=Object(g.getSearchWords)(q).map(e=>({key:e,label:e})),{data:K}=T,W=L(K,G,$);let{headers:X,rows:Z}=W;const{summary:ee}=W;w&&(Z=Z.map((e,t)=>[H(t),...e]),X=[Y(),...X]);const te=((e,t)=>t?e.map(e=>({...e,visible:e.required||!t.includes(e.key)})):e.map(e=>({...e,visible:e.required||!e.hiddenByDefault})))(X,B);return Object(n.createElement)(n.Fragment,null,Object(n.createElement)("div",{className:"woocommerce-report-table__scroll-point",ref:N,"aria-hidden":!0}),Object(n.createElement)(y.TableCard,s()({className:"woocommerce-report-table",hasSearch:!!S,actions:[w&&Object(n.createElement)(y.CompareButton,{key:"compare",className:"woocommerce-table__compare",count:P.length,helpText:D.helpText||Object(u.__)("Check at least two items below to compare",'woocommerce'),onClick:()=>{w&&Object(g.onQueryChange)("compare")(w,R,P.join(","))},disabled:!z},D.compareButton||Object(u.__)("Compare",'woocommerce')),S&&Object(n.createElement)(y.Search,{allowFreeTextSearch:!0,inlineTags:!0,key:"search",onChange:t=>{const{baseSearchQuery:r,addCesSurveyForCustomerSearch:a}=e,s=t.map(e=>e.label.replace(",","%2C"));s.length?(Object(g.updateQueryString)({filter:void 0,[R]:void 0,[S]:void 0,...r,search:Object(m.uniq)(s).join(",")}),a()):Object(g.updateQueryString)({search:void 0}),Object(_.recordEvent)("analytics_table_filter",{report:b})},placeholder:D.placeholder||Object(u.__)("Search by item name",'woocommerce'),selected:J,showClearButton:!0,type:S,disabled:!z}),z&&Object(n.createElement)(o.Button,{key:"download",className:"woocommerce-table__download-button",disabled:U,onClick:()=>{const{createNotice:t,startExport:r,title:a}=e,s=Object.assign({},q),{data:n,totalResults:o}=T;let i="browser";if(delete s.extended_info,s.search&&delete s[S],n&&n.length===o){const{headers:e,rows:t}=L(n,o);Object(h.downloadCSVFile)(Object(h.generateCSVFileName)(a,s),Object(h.generateCSVDataFromTable)(e,t))}else i="email",r(b,k).then(()=>t("success",Object(u.sprintf)(Object(u.__)("Your %s Report will be emailed to you.",'woocommerce'),a))).catch(e=>t("error",e.message||Object(u.sprintf)(Object(u.__)("There was a problem exporting your %s Report. Please try again.",'woocommerce'),a)));Object(_.recordEvent)("analytics_table_download",{report:b,rows:o,download_type:i})}},Object(n.createElement)(O,null),Object(n.createElement)("span",{className:"woocommerce-table__download-button__label"},D.downloadButton||Object(u.__)("Download",'woocommerce')))],headers:te,isLoading:U,onQueryChange:g.onQueryChange,onColumnsChange:(e,t)=>{const r=X.map(e=>e.key).filter(t=>!e.includes(t));if(F){I({[F]:r})}if(t){const r={report:b,column:t,status:e.includes(t)?"on":"off"};Object(_.recordEvent)("analytics_table_header_toggle",r)}},onSort:(e,t)=>{Object(g.onQueryChange)("sort")(e,t);const r={report:b,column:e,direction:t};Object(_.recordEvent)("analytics_table_sort",r)},onPageChange:(e,t)=>{N.current.scrollIntoView();const r=N.current.nextSibling.querySelector(".woocommerce-table__table"),a=l.focus.focusable.find(r);a.length&&a[0].focus(),t&&("goto"===t?Object(_.recordEvent)("analytics_table_go_to_page",{report:b,page:e}):Object(_.recordEvent)("analytics_table_page_click",{report:b,direction:t}))},rows:Z,rowsPerPage:parseInt(k.per_page,10)||f.QUERY_DEFAULTS.pageSize,summary:ee,totalRows:$},E)))};C.propTypes={baseSearchQuery:b.a.object,compareBy:b.a.string,compareParam:b.a.string,columnPrefsKey:b.a.string,endpoint:b.a.string,extendItemsMethodNames:b.a.shape({getError:b.a.string,isRequesting:b.a.string,load:b.a.string}),extendedItemsStoreName:b.a.string,getHeadersContent:b.a.func.isRequired,getRowsContent:b.a.func.isRequired,getSummary:b.a.func,itemIdField:b.a.string,labels:b.a.shape({compareButton:b.a.string,downloadButton:b.a.string,helpText:b.a.string,placeholder:b.a.string}),primaryData:b.a.object,searchBy:b.a.string,summaryFields:b.a.arrayOf(b.a.string),tableData:b.a.object.isRequired,tableQuery:b.a.object,title:b.a.string.isRequired},C.defaultProps={primaryData:{},tableData:{items:{data:[],totalResults:0},query:{}},tableQuery:{},compareParam:"filter",downloadable:!1,onSearch:m.noop,baseSearchQuery:{}};const w=[],R={};t.a=Object(c.compose)(Object(d.withSelect)((e,t)=>{const{endpoint:r,getSummary:a,isRequesting:s,itemIdField:n,query:o,tableData:i,tableQuery:c,filters:l,advancedFilters:d,summaryFields:u,extendedItemsStoreName:p}=t,b=e(f.REPORTS_STORE_NAME),y=p?e(p):null,{woocommerce_default_date_range:g}=e(f.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");if(s)return R;const h="categories"===r?"products":r,_=a?Object(f.getReportChartData)({endpoint:h,selector:b,dataType:"primary",query:o,filters:l,advancedFilters:d,defaultDateRange:g,fields:u}):R,O=i||Object(f.getReportTableData)({endpoint:r,query:o,selector:b,tableQuery:c,filters:l,advancedFilters:d,defaultDateRange:g}),j=y?function(e,t,r){const{extendItemsMethodNames:a,itemIdField:s}=t,n=r.items.data;if(!(Array.isArray(n)&&n.length&&a&&s))return r;const{[a.getError]:o,[a.isRequesting]:i,[a.load]:c}=e,l={include:n.map(e=>e[s]).join(","),per_page:n.length},d=c(l),u=!!i&&i(l),p=!!o&&o(l),b=n.map(e=>{const t=Object(m.first)(d.filter(t=>e.id===t.id));return{...e,...t}}),y=r.isRequesting||u,g=r.isError||p;return{...r,isRequesting:y,isError:g,items:{...r.items,data:b}}}(y,t,O):O;return{primaryData:_,ids:n&&j.items.data?j.items.data.map(e=>e[n]):w,tableData:j,query:o}}),Object(d.withDispatch)(e=>{const{startExport:t}=e(f.EXPORT_STORE_NAME),{createNotice:r}=e("core/notices"),{addCesSurveyForCustomerSearch:a}=e(v.c);return{createNotice:r,startExport:t,addCesSurveyForCustomerSearch:a}}))(C)},508:function(e,t,r){"use strict";var a=r(0),s=r(2),n=r(14),o=r(59),i=r(7),c=r(4),l=r(1),d=r.n(l),m=r(21),u=r(11),p=r(19),b=r(501),y=r(504),g=r(12);class h extends a.Component{shouldComponentUpdate(e){return e.isRequesting!==this.props.isRequesting||e.primaryData.isRequesting!==this.props.primaryData.isRequesting||e.secondaryData.isRequesting!==this.props.secondaryData.isRequesting||!Object(c.isEqual)(e.query,this.props.query)}getItemChartData(){const{primaryData:e,selectedChart:t}=this.props;return e.data.intervals.map((function(e){const r={};return e.subtotals.segments.forEach((function(e){if(e.segment_label){const a=r[e.segment_label]?e.segment_label+" (#"+e.segment_id+")":e.segment_label;r[e.segment_id]={label:a,value:e.subtotals[t.key]||0}}})),{date:Object(o.format)("Y-m-d\\TH:i:s",e.date_start),...r}}))}getTimeChartData(){const{query:e,primaryData:t,secondaryData:r,selectedChart:a,defaultDateRange:s}=this.props,n=Object(p.getIntervalForQuery)(e),{primary:i,secondary:c}=Object(p.getCurrentDates)(e,s);return t.data.intervals.map((function(t,s){const l=Object(p.getPreviousDate)(t.date_start,i.after,c.after,e.compare,n),d=r.data.intervals[s];return{date:Object(o.format)("Y-m-d\\TH:i:s",t.date_start),primary:{label:`${i.label} (${i.range})`,labelDate:t.date_start,value:t.subtotals[a.key]||0},secondary:{label:`${c.label} (${c.range})`,labelDate:l.format("YYYY-MM-DD HH:mm:ss"),value:d&&d.subtotals[a.key]||0}}}))}getTimeChartTotals(){const{primaryData:e,secondaryData:t,selectedChart:r}=this.props;return{primary:Object(c.get)(e,["data","totals",r.key],null),secondary:Object(c.get)(t,["data","totals",r.key],null)}}renderChart(e,t,r,n){const{emptySearchResults:o,filterParam:i,interactiveLegend:c,itemsLabel:l,legendPosition:d,path:b,query:y,selectedChart:g,showHeaderControls:h,primaryData:f}=this.props,_=Object(p.getIntervalForQuery)(y),O=Object(p.getAllowedIntervalsForQuery)(y),j=Object(p.getDateFormatsForInterval)(_,f.data.intervals.length),v=o?Object(s.__)("No data for the current search",'woocommerce'):Object(s.__)("No data for the selected date range",'woocommerce'),{formatAmount:C,getCurrencyConfig:w}=this.context;return Object(a.createElement)(m.Chart,{allowedIntervals:O,data:r,dateParser:"%Y-%m-%dT%H:%M:%S",emptyMessage:v,filterParam:i,interactiveLegend:c,interval:_,isRequesting:t,itemsLabel:l,legendPosition:d,legendTotals:n,mode:e,path:b,query:y,screenReaderFormat:j.screenReaderFormat,showHeaderControls:h,title:g.label,tooltipLabelFormat:j.tooltipLabelFormat,tooltipTitle:"time-comparison"===e&&g.label||null,tooltipValueFormat:Object(u.getTooltipValueFormat)(g.type,C),chartType:Object(p.getChartTypeForQuery)(y),valueType:g.type,xFormat:j.xFormat,x2Format:j.x2Format,currency:w()})}renderItemComparison(){const{isRequesting:e,primaryData:t}=this.props;if(t.isError)return Object(a.createElement)(y.a,null);const r=e||t.isRequesting,s=this.getItemChartData();return this.renderChart("item-comparison",r,s)}renderTimeComparison(){const{isRequesting:e,primaryData:t,secondaryData:r}=this.props;if(!t||t.isError||r.isError)return Object(a.createElement)(y.a,null);const s=e||t.isRequesting||r.isRequesting,n=this.getTimeChartData(),o=this.getTimeChartTotals();return this.renderChart("time-comparison",s,n,o)}render(){const{mode:e}=this.props;return"item-comparison"===e?this.renderItemComparison():this.renderTimeComparison()}}h.contextType=b.a,h.propTypes={filters:d.a.array,isRequesting:d.a.bool,itemsLabel:d.a.string,limitProperties:d.a.array,mode:d.a.string,path:d.a.string.isRequired,primaryData:d.a.object,query:d.a.object.isRequired,secondaryData:d.a.object,selectedChart:d.a.shape({key:d.a.string.isRequired,label:d.a.string.isRequired,order:d.a.oneOf(["asc","desc"]),orderby:d.a.string,type:d.a.oneOf(["average","number","currency"]).isRequired}).isRequired},h.defaultProps={isRequesting:!1,primaryData:{data:{intervals:[]},isError:!1,isRequesting:!1},secondaryData:{data:{intervals:[]},isError:!1,isRequesting:!1}};t.a=Object(n.compose)(Object(i.withSelect)((e,t)=>{const{charts:r,endpoint:a,filters:s,isRequesting:n,limitProperties:o,query:i,advancedFilters:l}=t,d=o||[a],m=function e(t,r,a={}){if(!t||0===t.length)return null;const s=t.slice(0),n=s.pop();if(n.showFilters(r,a)){const e=Object(g.flattenFilters)(n.filters),t=r[n.param]||n.defaultValue||"all";return Object(c.find)(e,{value:t})}return e(s,r,a)}(s,i),p=Object(c.get)(m,["settings","param"]),b=t.mode||function(e,t){if(e&&t){const r=Object(c.get)(e,["settings","param"]);if(!r||Object.keys(t).includes(r))return Object(c.get)(e,["chartMode"])}return null}(m,i)||"time-comparison",{woocommerce_default_date_range:y}=e(u.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings"),h=e(u.REPORTS_STORE_NAME),f={mode:b,filterParam:p,defaultDateRange:y};if(n)return f;const _=d.some(e=>i[e]&&i[e].length);if(i.search&&!_)return{...f,emptySearchResults:!0};const O=r&&r.map(e=>e.key),j=Object(u.getReportChartData)({endpoint:a,dataType:"primary",query:i,selector:h,limitBy:d,filters:s,advancedFilters:l,defaultDateRange:y,fields:O});if("item-comparison"===b)return{...f,primaryData:j};const v=Object(u.getReportChartData)({endpoint:a,dataType:"secondary",query:i,selector:h,limitBy:d,filters:s,advancedFilters:l,defaultDateRange:y,fields:O});return{...f,primaryData:j,secondaryData:v}}))(h)},510:function(e,t,r){"use strict";r.d(t,"a",(function(){return s}));var a=r(4);function s(e,t=[]){const r=Object(a.find)(t,{key:e});return r||t[0]}},511:function(e,t,r){"use strict";var a=r(0),s=r(2),n=r(14),o=r(7),i=r(1),c=r.n(i),l=r(12),d=r(21),m=r(120),u=r(11),p=r(19),b=r(16),y=r(504),g=r(501);class h extends a.Component{formatVal(e,t){const{formatAmount:r,getCurrencyConfig:a}=this.context;return"currency"===t?r(e):Object(m.formatValue)(a(),t,e)}getValues(e,t){const{emptySearchResults:r,summaryData:a}=this.props,{totals:s}=a,n=s.primary?s.primary[e]:0,o=s.secondary?s.secondary[e]:0,i=r?0:n,c=r?0:o;return{delta:Object(m.calculateDelta)(i,c),prevValue:this.formatVal(c,t),value:this.formatVal(i,t)}}render(){const{charts:e,query:t,selectedChart:r,summaryData:n,endpoint:o,report:i,defaultDateRange:c}=this.props,{isError:m,isRequesting:u}=n;if(m)return Object(a.createElement)(y.a,null);if(u)return Object(a.createElement)(d.SummaryListPlaceholder,{numberOfItems:e.length});const{compare:g}=Object(p.getDateParamsFromQuery)(t,c);return Object(a.createElement)(d.SummaryList,null,({onToggle:t})=>e.map(e=>{const{key:n,order:c,orderby:m,label:u,type:p,isReverseTrend:y}=e,h={chart:n};m&&(h.orderby=m),c&&(h.order=c);const f=Object(l.getNewPath)(h),_=r.key===n,{delta:O,prevValue:j,value:v}=this.getValues(n,p);return Object(a.createElement)(d.SummaryNumber,{key:n,delta:O,href:f,label:u,reverseTrend:y,prevLabel:"previous_period"===g?Object(s.__)("Previous period:",'woocommerce'):Object(s.__)("Previous year:",'woocommerce'),prevValue:j,selected:_,value:v,onLinkClickCallback:()=>{t&&t(),Object(b.recordEvent)("analytics_chart_tab_click",{report:i||o,key:n})}})}))}}h.propTypes={charts:c.a.array.isRequired,endpoint:c.a.string.isRequired,limitProperties:c.a.array,query:c.a.object.isRequired,selectedChart:c.a.shape({key:c.a.string.isRequired,label:c.a.string.isRequired,order:c.a.oneOf(["asc","desc"]),orderby:c.a.string,type:c.a.oneOf(["average","number","currency"]).isRequired}).isRequired,summaryData:c.a.object,report:c.a.string},h.defaultProps={summaryData:{totals:{primary:{},secondary:{}},isError:!1}},h.contextType=g.a,t.a=Object(n.compose)(Object(o.withSelect)((e,t)=>{const{charts:r,endpoint:a,limitProperties:s,query:n,filters:o,advancedFilters:i}=t,c=s||[a],l=c.some(e=>n[e]&&n[e].length);if(n.search&&!l)return{emptySearchResults:!0};const d=r&&r.map(e=>e.key),{woocommerce_default_date_range:m}=e(u.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");return{summaryData:Object(u.getSummaryNumbers)({endpoint:a,query:n,select:e,limitBy:c,filters:o,advancedFilters:i,defaultDateRange:m,fields:d}),defaultDateRange:m}}))(h)},515:function(e,t,r){}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/0.style.css b/packages/woocommerce-admin/dist/chunks/0.style.css new file mode 100644 index 0000000..f363d4f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/0.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-report-table__scroll-point{position:relative;top:-48px}@media(max-width:782px){.woocommerce-report-table__scroll-point{top:-62px}}.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-108px}@media(max-width:782px){.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-122px}}.woocommerce-report-table .woocommerce-search{flex-grow:1}.woocommerce-report-table .components-card__header{display:grid;grid-gap:12px;grid-template-columns:min-content 1fr min-content}.woocommerce-report-table .woocommerce-table__compare.components-button{padding:8px}.woocommerce-report-table .woocommerce-ellipsis-menu{justify-self:flex-end}button.woocommerce-table__download-button{padding:6px 12px;color:#000;text-decoration:none;align-items:center}button.woocommerce-table__download-button svg{margin-right:8px;height:24px;width:24px}@media(max-width:782px){button.woocommerce-table__download-button svg{margin-right:0}button.woocommerce-table__download-button .woocommerce-table__download-button__label{display:none}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/1.js b/packages/woocommerce-admin/dist/chunks/1.js new file mode 100644 index 0000000..6d3c4c0 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/1.js @@ -0,0 +1,2 @@ +/*! For license information please see 1.js.LICENSE.txt */ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[1],{53:function(e,t,n){e.exports=function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,o=Object.getOwnPropertyDescriptor,i=Object.freeze,a=Object.seal,l=Object.create,c="undefined"!=typeof Reflect&&Reflect,s=c.apply,u=c.construct;s||(s=function(e,t,n){return e.apply(t,n)}),i||(i=function(e){return e}),a||(a=function(e){return e}),u||(u=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?n-1:0),o=1;o/gm),H=a(/^data-[\-\w.\u00B7-\uFFFF]/),j=a(/^aria-[\-\w]+$/),P=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),B=a(/^(?:\w+script|data):/i),W=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),G="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function q(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:K(),n=function(t){return e(t)};if(n.version="2.2.9",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,o=t.document,a=t.DocumentFragment,l=t.HTMLTemplateElement,c=t.Node,s=t.Element,u=t.NodeFilter,f=t.NamedNodeMap,w=void 0===f?t.NamedNodeMap||t.MozNamedAttrMap:f,Y=t.Text,J=t.Comment,X=t.DOMParser,$=t.trustedTypes,Z=s.prototype,Q=k(Z,"cloneNode"),ee=k(Z,"nextSibling"),te=k(Z,"childNodes"),ne=k(Z,"parentNode");if("function"==typeof l){var re=o.createElement("template");re.content&&re.content.ownerDocument&&(o=re.content.ownerDocument)}var oe=V($,r),ie=oe&&Ce?oe.createHTML(""):"",ae=o,le=ae.implementation,ce=ae.createNodeIterator,se=ae.createDocumentFragment,ue=r.importNode,fe={};try{fe=S(o).documentMode?o.documentMode:{}}catch(e){}var me={};n.isSupported="function"==typeof ne&&le&&void 0!==le.createHTMLDocument&&9!==fe;var pe=z,de=U,ge=H,he=j,ye=B,ve=W,be=P,Ae=null,Te=x({},[].concat(q(_),q(E),q(R),q(N),q(L))),we=null,xe=x({},[].concat(q(M),q(F),q(I),q(C))),Se=null,ke=null,_e=!0,Ee=!0,Re=!1,De=!1,Ne=!1,Oe=!1,Le=!1,Me=!1,Fe=!1,Ie=!0,Ce=!1,ze=!0,Ue=!0,He=!1,je={},Pe=x({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Be=null,We=x({},["audio","video","img","source","image","track"]),Ge=null,qe=x({},["alt","class","for","id","label","name","pattern","placeholder","summary","title","value","style","xmlns"]),Ke="http://www.w3.org/1998/Math/MathML",Ve="http://www.w3.org/2000/svg",Ye="http://www.w3.org/1999/xhtml",Je=Ye,Xe=!1,$e=null,Ze=o.createElement("form"),Qe=function(e){$e&&$e===e||(e&&"object"===(void 0===e?"undefined":G(e))||(e={}),e=S(e),Ae="ALLOWED_TAGS"in e?x({},e.ALLOWED_TAGS):Te,we="ALLOWED_ATTR"in e?x({},e.ALLOWED_ATTR):xe,Ge="ADD_URI_SAFE_ATTR"in e?x(S(qe),e.ADD_URI_SAFE_ATTR):qe,Be="ADD_DATA_URI_TAGS"in e?x(S(We),e.ADD_DATA_URI_TAGS):We,Se="FORBID_TAGS"in e?x({},e.FORBID_TAGS):{},ke="FORBID_ATTR"in e?x({},e.FORBID_ATTR):{},je="USE_PROFILES"in e&&e.USE_PROFILES,_e=!1!==e.ALLOW_ARIA_ATTR,Ee=!1!==e.ALLOW_DATA_ATTR,Re=e.ALLOW_UNKNOWN_PROTOCOLS||!1,De=e.SAFE_FOR_TEMPLATES||!1,Ne=e.WHOLE_DOCUMENT||!1,Me=e.RETURN_DOM||!1,Fe=e.RETURN_DOM_FRAGMENT||!1,Ie=!1!==e.RETURN_DOM_IMPORT,Ce=e.RETURN_TRUSTED_TYPE||!1,Le=e.FORCE_BODY||!1,ze=!1!==e.SANITIZE_DOM,Ue=!1!==e.KEEP_CONTENT,He=e.IN_PLACE||!1,be=e.ALLOWED_URI_REGEXP||be,Je=e.NAMESPACE||Ye,De&&(Ee=!1),Fe&&(Me=!0),je&&(Ae=x({},[].concat(q(L))),we=[],!0===je.html&&(x(Ae,_),x(we,M)),!0===je.svg&&(x(Ae,E),x(we,F),x(we,C)),!0===je.svgFilters&&(x(Ae,R),x(we,F),x(we,C)),!0===je.mathMl&&(x(Ae,N),x(we,I),x(we,C))),e.ADD_TAGS&&(Ae===Te&&(Ae=S(Ae)),x(Ae,e.ADD_TAGS)),e.ADD_ATTR&&(we===xe&&(we=S(we)),x(we,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&x(Ge,e.ADD_URI_SAFE_ATTR),Ue&&(Ae["#text"]=!0),Ne&&x(Ae,["html","head","body"]),Ae.table&&(x(Ae,["tbody"]),delete Se.tbody),i&&i(e),$e=e)},et=x({},["mi","mo","mn","ms","mtext"]),tt=x({},["foreignobject","desc","title","annotation-xml"]),nt=x({},E);x(nt,R),x(nt,D);var rt=x({},N);x(rt,O);var ot=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Ye,tagName:"template"});var n=g(e.tagName),r=g(t.tagName);if(e.namespaceURI===Ve)return t.namespaceURI===Ye?"svg"===n:t.namespaceURI===Ke?"svg"===n&&("annotation-xml"===r||et[r]):Boolean(nt[n]);if(e.namespaceURI===Ke)return t.namespaceURI===Ye?"math"===n:t.namespaceURI===Ve?"math"===n&&tt[r]:Boolean(rt[n]);if(e.namespaceURI===Ye){if(t.namespaceURI===Ve&&!tt[r])return!1;if(t.namespaceURI===Ke&&!et[r])return!1;var o=x({},["title","style","font","a","script"]);return!rt[n]&&(o[n]||!nt[n])}return!1},it=function(e){d(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ie}catch(t){e.remove()}}},at=function(e,t){try{d(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){d(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!we[e])if(Me||Fe)try{it(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},lt=function(e){var t=void 0,n=void 0;if(Le)e=""+e;else{var r=h(e,/^[\r\n\t ]+/);n=r&&r[0]}var i=oe?oe.createHTML(e):e;if(Je===Ye)try{t=(new X).parseFromString(i,"text/html")}catch(e){}if(!t||!t.documentElement){t=le.createDocument(Je,"template",null);try{t.documentElement.innerHTML=Xe?"":i}catch(e){}}var a=t.body||t.documentElement;return e&&n&&a.insertBefore(o.createTextNode(n),a.childNodes[0]||null),Ne?t.documentElement:a},ct=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},st=function(e){return!(e instanceof Y||e instanceof J||"string"==typeof e.nodeName&&"string"==typeof e.textContent&&"function"==typeof e.removeChild&&e.attributes instanceof w&&"function"==typeof e.removeAttribute&&"function"==typeof e.setAttribute&&"string"==typeof e.namespaceURI&&"function"==typeof e.insertBefore)},ut=function(e){return"object"===(void 0===c?"undefined":G(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":G(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},ft=function(e,t,r){me[e]&&m(me[e],(function(e){e.call(n,t,r,$e)}))},mt=function(e){var t=void 0;if(ft("beforeSanitizeElements",e,null),st(e))return it(e),!0;if(h(e.nodeName,/[\u0080-\uFFFF]/))return it(e),!0;var r=g(e.nodeName);if(ft("uponSanitizeElement",e,{tagName:r,allowedTags:Ae}),!ut(e.firstElementChild)&&(!ut(e.content)||!ut(e.content.firstElementChild))&&A(/<[/\w]/g,e.innerHTML)&&A(/<[/\w]/g,e.textContent))return it(e),!0;if(!Ae[r]||Se[r]){if(Ue&&!Pe[r]){var o=ne(e)||e.parentNode,i=te(e)||e.childNodes;if(i&&o)for(var a=i.length-1;a>=0;--a)o.insertBefore(Q(i[a],!0),ee(e))}return it(e),!0}return e instanceof s&&!ot(e)?(it(e),!0):"noscript"!==r&&"noembed"!==r||!A(/<\/no(script|embed)/i,e.innerHTML)?(De&&3===e.nodeType&&(t=e.textContent,t=y(t,pe," "),t=y(t,de," "),e.textContent!==t&&(d(n.removed,{element:e.cloneNode()}),e.textContent=t)),ft("afterSanitizeElements",e,null),!1):(it(e),!0)},pt=function(e,t,n){if(ze&&("id"===t||"name"===t)&&(n in o||n in Ze))return!1;if(Ee&&A(ge,t));else if(_e&&A(he,t));else{if(!we[t]||ke[t])return!1;if(Ge[t]);else if(A(be,y(n,ve,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==v(n,"data:")||!Be[e])if(Re&&!A(ye,y(n,ve,"")));else if(n)return!1}return!0},dt=function(e){var t=void 0,r=void 0,o=void 0,i=void 0;ft("beforeSanitizeAttributes",e,null);var a=e.attributes;if(a){var l={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we};for(i=a.length;i--;){var c=t=a[i],s=c.name,u=c.namespaceURI;if(r=b(t.value),o=g(s),l.attrName=o,l.attrValue=r,l.keepAttr=!0,l.forceKeepAttr=void 0,ft("uponSanitizeAttribute",e,l),r=l.attrValue,!l.forceKeepAttr&&(at(s,e),l.keepAttr))if(A(/\/>/i,r))at(s,e);else{De&&(r=y(r,pe," "),r=y(r,de," "));var f=e.nodeName.toLowerCase();if(pt(f,o,r))try{u?e.setAttributeNS(u,s,r):e.setAttribute(s,r),p(n.removed)}catch(e){}}}ft("afterSanitizeAttributes",e,null)}},gt=function e(t){var n=void 0,r=ct(t);for(ft("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)ft("uponSanitizeShadowNode",n,null),mt(n)||(n.content instanceof a&&e(n.content),dt(n));ft("afterSanitizeShadowDOM",t,null)};return n.sanitize=function(e,o){var i=void 0,l=void 0,s=void 0,u=void 0,f=void 0;if((Xe=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!ut(e)){if("function"!=typeof e.toString)throw T("toString is not a function");if("string"!=typeof(e=e.toString()))throw T("dirty is not a string, aborting")}if(!n.isSupported){if("object"===G(t.toStaticHTML)||"function"==typeof t.toStaticHTML){if("string"==typeof e)return t.toStaticHTML(e);if(ut(e))return t.toStaticHTML(e.outerHTML)}return e}if(Oe||Qe(o),n.removed=[],"string"==typeof e&&(He=!1),He);else if(e instanceof c)1===(l=(i=lt("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===l.nodeName||"HTML"===l.nodeName?i=l:i.appendChild(l);else{if(!Me&&!De&&!Ne&&-1===e.indexOf("<"))return oe&&Ce?oe.createHTML(e):e;if(!(i=lt(e)))return Me?null:ie}i&&Le&&it(i.firstChild);for(var m=ct(He?e:i);s=m.nextNode();)3===s.nodeType&&s===u||mt(s)||(s.content instanceof a&>(s.content),dt(s),u=s);if(u=null,He)return e;if(Me){if(Fe)for(f=se.call(i.ownerDocument);i.firstChild;)f.appendChild(i.firstChild);else f=i;return Ie&&(f=ue.call(r,f,!0)),f}var p=Ne?i.outerHTML:i.innerHTML;return De&&(p=y(p,pe," "),p=y(p,de," ")),oe&&Ce?oe.createHTML(p):p},n.setConfig=function(e){Qe(e),Oe=!0},n.clearConfig=function(){$e=null,Oe=!1},n.isValidAttribute=function(e,t,n){$e||Qe({});var r=g(e),o=g(t);return pt(r,o,n)},n.addHook=function(e,t){"function"==typeof t&&(me[e]=me[e]||[],d(me[e],t))},n.removeHook=function(e){me[e]&&p(me[e])},n.removeHooks=function(e){me[e]&&(me[e]=[])},n.removeAllHooks=function(){me={}},n}()}()}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/1.js.LICENSE.txt b/packages/woocommerce-admin/dist/chunks/1.js.LICENSE.txt new file mode 100644 index 0000000..9e64a22 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/1.js.LICENSE.txt @@ -0,0 +1 @@ +/*! @license DOMPurify | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.2.2/LICENSE */ diff --git a/packages/woocommerce-admin/dist/chunks/11.style.css b/packages/woocommerce-admin/dist/chunks/11.style.css new file mode 100644 index 0000000..7c6e64f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/11.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-orders-table__status{flex-direction:row-reverse}.woocommerce-orders-table__status .woocommerce-order-status__indicator{margin-right:0;margin-left:8px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/12.style.css b/packages/woocommerce-admin/dist/chunks/12.style.css new file mode 100644 index 0000000..45294f2 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/12.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-table__product-categories>.woocommerce-table__breadcrumbs{display:inline-block;margin-right:12px}.woocommerce-table__product-categories .components-popover__content{padding:0 16px;text-align:left}.woocommerce-table__product-categories .components-popover__content .woocommerce-table__breadcrumbs{margin-top:12px;margin-bottom:12px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/14.style.css b/packages/woocommerce-admin/dist/chunks/14.style.css new file mode 100644 index 0000000..f363d4f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/14.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-report-table__scroll-point{position:relative;top:-48px}@media(max-width:782px){.woocommerce-report-table__scroll-point{top:-62px}}.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-108px}@media(max-width:782px){.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-122px}}.woocommerce-report-table .woocommerce-search{flex-grow:1}.woocommerce-report-table .components-card__header{display:grid;grid-gap:12px;grid-template-columns:min-content 1fr min-content}.woocommerce-report-table .woocommerce-table__compare.components-button{padding:8px}.woocommerce-report-table .woocommerce-ellipsis-menu{justify-self:flex-end}button.woocommerce-table__download-button{padding:6px 12px;color:#000;text-decoration:none;align-items:center}button.woocommerce-table__download-button svg{margin-right:8px;height:24px;width:24px}@media(max-width:782px){button.woocommerce-table__download-button svg{margin-right:0}button.woocommerce-table__download-button .woocommerce-table__download-button__label{display:none}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/17.style.css b/packages/woocommerce-admin/dist/chunks/17.style.css new file mode 100644 index 0000000..a6e79e9 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/17.style.css @@ -0,0 +1 @@ +@media(min-width:783px){.woocommerce-settings__wrapper{padding:0 13px}}.woocommerce-settings__actions{margin-bottom:40px}@media(min-width:1281px){.woocommerce-settings__actions{margin-left:15%}}.woocommerce-settings__actions button{margin-right:16px}.woocommerce-setting{display:flex;margin-bottom:24px}@media(max-width:1280px){.woocommerce-setting{flex-direction:column}}.woocommerce-setting__label{font-size:16px;font-size:1rem;margin-bottom:16px;padding-right:16px;font-weight:700}@media(min-width:1281px){.woocommerce-setting__label{width:15%}}.woocommerce-setting__input{display:flex;flex-direction:column}@media(min-width:1281px){.woocommerce-setting__input{width:35%}.woocommerce-setting__input .woocommerce-filters-filter{width:100%}}.woocommerce-setting__input label{width:100%;display:block;margin-bottom:12px;color:#757575}.woocommerce-setting__input .woocommerce-filters-filter label{margin-bottom:0}.woocommerce-setting__input button:not(.components-tab-panel__tabs-item){margin-bottom:12px;align-self:flex-start}.woocommerce-setting__input .components-base-control__field{display:flex}.woocommerce-setting__input .woocommerce-filters-date__content-controls{padding-bottom:0}.woocommerce-setting__options-group-label{display:block;font-weight:700;margin-bottom:12px}.woocommerce-setting__help{font-style:italic;color:#757575}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-settings-historical-data__columns{display:grid;grid-column-gap:24px;grid-template-columns:calc(50% - 12px) calc(50% - 12px)}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column{align-self:end;margin-top:12px}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column:first-child{grid-column-start:1;grid-column-end:2;grid-row-start:1;grid-row-end:2}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column:nth-child(2){grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:2}@media(max-width:960px){.woocommerce-settings-historical-data__columns{grid-template-columns:100%}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column:first-child{grid-column-start:1;grid-column-end:2;grid-row-start:1;grid-row-end:2}.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column:nth-child(2){grid-column-start:1;grid-column-end:2;grid-row-start:2;grid-row-end:3}}.woocommerce-settings-historical-data__columns .components-base-control__label,.woocommerce-settings-historical-data__columns .woocommerce-settings-historical-data__column-label{margin-bottom:12px}.woocommerce-settings-historical-data__columns .components-select-control__input{height:38px;padding:8px 2px}.woocommerce-settings-historical-data__columns .components-base-control__field{margin-bottom:0}.woocommerce-settings-historical-data__skip-checkbox{margin-top:24px}.woocommerce-settings-historical-data__skip-checkbox>.components-base-control__field{margin-bottom:0}.woocommerce-settings-historical-data__skip-checkbox>.components-base-control__field>.components-checkbox-control__label{display:inline-block;margin-bottom:0;width:auto}.woocommerce-settings-historical-data__progress-label{display:inline-block;font-weight:700;margin-bottom:12px;margin-top:24px}.woocommerce-settings-historical-data__progress-label+.woocommerce-settings-historical-data__progress-label{margin-left:.25em}.woocommerce-settings-historical-data__progress-bar{-webkit-appearance:none;appearance:none;border:0;height:8px;width:100%;background-color:#c4c4c4}.woocommerce-settings-historical-data__progress-bar::-moz-progress-bar{background-color:#0085ba}.woocommerce-settings-historical-data__progress-bar::-webkit-progress-bar{background-color:#c4c4c4}.woocommerce-settings-historical-data__progress-bar::-webkit-progress-value{background-color:#0085ba}.woocommerce-settings-historical-data__status{display:block;font-weight:700;margin-top:24px}.woocommerce-settings-historical-data__status>.components-spinner{float:none;height:12px;margin-left:6px;margin-right:6px;width:12px}.woocommerce-settings-historical-data__status>.components-spinner:before{left:2px;height:3px;top:2px;transform-origin:4px 4px;width:3px}.woocommerce-settings-historical-data__actions{align-items:center;display:flex} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/2.js b/packages/woocommerce-admin/dist/chunks/2.js new file mode 100644 index 0000000..7c30d74 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/2.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[2],{169:function(e,t,n){"use strict";var i=n(23),s=n(22),r=n(25);n(1);function a(e,t){return e.replace(new RegExp("(^|\\s)"+t+"(?:\\s|$)","g"),"$1").replace(/\s+/g," ").replace(/^\s*|\s*$/g,"")}var o=n(5),l=n.n(o),c=n(65),u=function(e,t){return e&&t&&t.split(" ").forEach((function(t){return i=t,void((n=e).classList?n.classList.remove(i):"string"==typeof n.className?n.className=a(n.className,i):n.setAttribute("class",a(n.className&&n.className.baseVal||"",i)));var n,i}))},p=function(e){function t(){for(var t,n=arguments.length,i=new Array(n),s=0;sdiv:first-child{grid-column-start:1;grid-column-end:2;grid-row-start:1;grid-row-end:2}.woocommerce-dashboard__columns>div:nth-child(2){grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:2}.woocommerce-dashboard__columns>div:nth-child(3){grid-column-start:1;grid-column-end:2;grid-row-start:2;grid-row-end:3}.woocommerce-dashboard__columns>div:nth-child(4){grid-column-start:2;grid-column-end:3;grid-row-start:2;grid-row-end:3}.woocommerce-dashboard__columns>div:nth-child(5){grid-column-start:1;grid-column-end:2;grid-row-start:3;grid-row-end:4}.woocommerce-dashboard__columns>div:nth-child(6){grid-column-start:2;grid-column-end:3;grid-row-start:3;grid-row-end:4}.woocommerce-dashboard__columns>div:nth-child(7){grid-column-start:1;grid-column-end:2;grid-row-start:4;grid-row-end:5}.woocommerce-dashboard__columns>div:nth-child(8){grid-column-start:2;grid-column-end:3;grid-row-start:4;grid-row-end:5}.woocommerce-dashboard__columns>div:nth-child(9){grid-column-start:1;grid-column-end:2;grid-row-start:5;grid-row-end:6}.woocommerce-dashboard__columns>div:nth-child(10){grid-column-start:2;grid-column-end:3;grid-row-start:5;grid-row-end:6}.woocommerce-dashboard__columns>div:nth-child(11){grid-column-start:1;grid-column-end:2;grid-row-start:6;grid-row-end:7}.woocommerce-dashboard__columns>div:nth-child(12){grid-column-start:2;grid-column-end:3;grid-row-start:6;grid-row-end:7}.woocommerce-dashboard__columns>div:nth-child(13){grid-column-start:1;grid-column-end:2;grid-row-start:7;grid-row-end:8}.woocommerce-dashboard__columns>div:nth-child(14){grid-column-start:2;grid-column-end:3;grid-row-start:7;grid-row-end:8}@media(max-width:960px){.woocommerce-dashboard__columns{grid-template-columns:100%}.woocommerce-dashboard__columns>div:first-child{grid-column-start:1;grid-column-end:2;grid-row-start:1;grid-row-end:2}.woocommerce-dashboard__columns>div:nth-child(2){grid-column-start:1;grid-column-end:2;grid-row-start:2;grid-row-end:3}.woocommerce-dashboard__columns>div:nth-child(3){grid-column-start:1;grid-column-end:2;grid-row-start:3;grid-row-end:4}.woocommerce-dashboard__columns>div:nth-child(4){grid-column-start:1;grid-column-end:2;grid-row-start:4;grid-row-end:5}.woocommerce-dashboard__columns>div:nth-child(5){grid-column-start:1;grid-column-end:2;grid-row-start:5;grid-row-end:6}.woocommerce-dashboard__columns>div:nth-child(6){grid-column-start:1;grid-column-end:2;grid-row-start:6;grid-row-end:7}.woocommerce-dashboard__columns>div:nth-child(7){grid-column-start:1;grid-column-end:2;grid-row-start:7;grid-row-end:8}.woocommerce-dashboard__columns>div:nth-child(8){grid-column-start:1;grid-column-end:2;grid-row-start:8;grid-row-end:9}.woocommerce-dashboard__columns>div:nth-child(9){grid-column-start:1;grid-column-end:2;grid-row-start:9;grid-row-end:10}.woocommerce-dashboard__columns>div:nth-child(10){grid-column-start:1;grid-column-end:2;grid-row-start:10;grid-row-end:11}.woocommerce-dashboard__columns>div:nth-child(11){grid-column-start:1;grid-column-end:2;grid-row-start:11;grid-row-end:12}.woocommerce-dashboard__columns>div:nth-child(12){grid-column-start:1;grid-column-end:2;grid-row-start:12;grid-row-end:13}.woocommerce-dashboard__columns>div:nth-child(13){grid-column-start:1;grid-column-end:2;grid-row-start:13;grid-row-end:14}.woocommerce-dashboard__columns>div:nth-child(14){grid-column-start:1;grid-column-end:2;grid-row-start:14;grid-row-end:15}}.woocommerce-dashboard__widget{display:flex;align-items:center;text-align:center}.woocommerce-dashboard__widget-item{flex:1}.woocommerce-dashboard-section__add-more{margin:0 auto;width:84px;padding:0 24px 24px}.woocommerce-dashboard-section__add-more .components-popover__content{padding:0 16px 8px}.woocommerce-dashboard-section__add-more>button svg{fill:#757575}.woocommerce-dashboard-section__add-more-choices{display:flex;justify-content:center}.woocommerce-dashboard-section__add-more-btn{display:flex;flex-direction:column;align-items:center;padding:16px;margin:8px}.woocommerce-dashboard-section__add-more-btn.components-button{height:auto}.woocommerce-dashboard-section__add-more-btn .store-performance__icon{transform:rotate(-45deg)}.woocommerce-dashboard-section__add-more-btn-title{color:#757575;padding-top:8px}.woocommerce-dashboard-section-controls{border-top:1px solid #f0f0f0;padding-top:8px}.woocommerce-dashboard-section-controls .icon-control{margin:0 8px 0 0;vertical-align:bottom;fill:#757575}.woocommerce-dashboard-section-controls .woocommerce-ellipsis-menu__item{padding-bottom:10px}.components-card .woocommerce-ellipsis-menu__toggle{padding:0}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card{max-width:680px;margin-left:auto;margin-right:auto;margin-bottom:24px}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card .components-card__header.is-size-large{padding-bottom:12px}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card .components-card__header.is-size-large .woocommerce-card__menu{margin-top:8px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/26.style.css b/packages/woocommerce-admin/dist/chunks/26.style.css new file mode 100644 index 0000000..f1c1638 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/26.style.css @@ -0,0 +1 @@ +.woocommerce-dashboard__chart-block-wrapper{cursor:pointer}.woocommerce-dashboard__chart-block-wrapper:hover .woocommerce-card__header,.woocommerce-dashboard__chart-block-wrapper:hover .woocommerce-chart{background:#f8f9f9}.woocommerce-dashboard__chart-block-wrapper:hover .woocommerce-legend__item button{background:transparent}.woocommerce-dashboard__chart-block-wrapper .woocommerce-chart{margin-top:0;margin-bottom:0;border:0}.woocommerce-dashboard__chart-block-wrapper .woocommerce-chart__footer{position:relative}.woocommerce-dashboard__chart-block-wrapper .woocommerce-chart__footer:after{content:"";position:absolute;width:100%;height:100%;left:0;top:0;cursor:pointer;z-index:1}.woocommerce-dashboard__chart-block .woocommerce-card__body{padding:0;position:relative}.woocommerce-dashboard__chart-block .woocommerce-card__body .woocommerce-chart{border:none;margin:0}.woocommerce-dashboard__chart-block .woocommerce-card__body .woocommerce-chart .woocommerce-legend__item>button{cursor:default}.woocommerce-dashboard__chart-block .woocommerce-card__body .woocommerce-chart .woocommerce-legend__item>button:hover{background:#f0f0f0}.woocommerce-dashboard__chart-block .woocommerce-card__body .woocommerce-chart .woocommerce-legend__item>button .woocommerce-legend__item-container{cursor:default}.woocommerce-dashboard__chart-block .woocommerce-card__body .woocommerce-chart .woocommerce-legend__item>button .woocommerce-legend__item-container .woocommerce-legend__item-checkmark.woocommerce-legend__item-checkmark-checked:after{display:none}.woocommerce-dashboard__chart-block .woocommerce-card__body .woocommerce-chart:hover,.woocommerce-dashboard__chart-block .woocommerce-card__body .woocommerce-chart:hover .woocommerce-legend__item>button,.woocommerce-dashboard__chart-block:hover{background:#f0f0f0}.woocommerce-dashboard__chart-block .screen-reader-text:focus{clip:auto;-webkit-clip-path:none;clip-path:none;z-index:1;left:6px;top:7px;height:auto;width:auto;display:block;font-size:14px;font-size:.875rem;font-weight:600;padding:15px 23px 14px;background:#f1f1f1;color:#0073aa;text-decoration:none;box-shadow:0 0 2px 2px rgba(0,0,0,.6)}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-dashboard__dashboard-charts{border-bottom:0;border-right:0}.woocommerce-dashboard__dashboard-charts .woocommerce-section-header__actions{flex-grow:0}.woocommerce-dashboard__dashboard-charts .woocommerce-card__body{padding:0}.woocommerce-dashboard__dashboard-charts .woocommerce-summary{margin:0} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/3.js b/packages/woocommerce-admin/dist/chunks/3.js new file mode 100644 index 0000000..53a2adf --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/3.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[3,53],{271:function(C,e,H){"use strict";H.r(e),H.d(e,"WCPayCardHeader",(function(){return f})),H.d(e,"WCPayCardBody",(function(){return O})),H.d(e,"WCPayCardFooter",(function(){return j})),H.d(e,"WCPayCard",(function(){return M})),H.d(e,"RecommendedRibbon",(function(){return E})),H.d(e,"SetupRequired",(function(){return v})),H.d(e,"WCPayAcceptedMethods",(function(){return b})),H.d(e,"Visa",(function(){return i})),H.d(e,"MasterCard",(function(){return o})),H.d(e,"Amex",(function(){return r})),H.d(e,"ApplePay",(function(){return d})),H.d(e,"GooglePay",(function(){return g})),H.d(e,"WCPayLogo",(function(){return h})),H.d(e,"WooPaymentGatewaySetup",(function(){return R})),H.d(e,"WooPaymentGatewayConfigure",(function(){return _})),H.d(e,"WooOnboardingTask",(function(){return y})),H.d(e,"WooOnboardingTaskListItem",(function(){return k}));var L=H(3),t=H(20),l=H(0),n=H(21),c=H(2),i=()=>Object(l.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"0.5",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{d:"M22.6435 24.004H19.248L21.3718 11.7534H24.7671L22.6435 24.004Z",fill:"#15195A"}),Object(l.createElement)("path",{d:"M34.952 12.0528C34.2823 11.8049 33.22 11.5312 31.9066 11.5312C28.5534 11.5312 26.1922 13.1993 26.1777 15.5842C26.1499 17.3437 27.8683 18.321 29.1536 18.9077C30.4672 19.5072 30.9138 19.8985 30.9138 20.4329C30.9004 21.2536 29.8522 21.6319 28.8747 21.6319C27.5191 21.6319 26.7927 21.4369 25.6889 20.9803L25.2417 20.7845L24.7666 23.5345C25.563 23.873 27.0302 24.1733 28.5534 24.1865C32.1162 24.1865 34.4356 22.5442 34.4631 20.0028C34.4767 18.6082 33.5693 17.5396 31.613 16.6665C30.4254 16.1059 29.6981 15.728 29.6981 15.1544C29.7121 14.6331 30.3133 14.099 31.6539 14.099C32.7577 14.0729 33.5687 14.3204 34.1831 14.5681L34.4902 14.6982L34.952 12.0528Z",fill:"#15195A"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M41.0301 11.7534H43.6565L46.3957 24.0039H43.2519C43.2519 24.0039 42.9442 22.5963 42.8467 22.1662H38.4873C38.3612 22.4919 37.7747 24.0039 37.7747 24.0039H34.2119L39.2554 12.7699C39.6049 11.9748 40.2202 11.7534 41.0301 11.7534ZM40.8208 16.2365C40.8208 16.2365 39.7448 18.9603 39.4652 19.6641H42.2875C42.1478 19.0516 41.5048 16.1192 41.5048 16.1192L41.2676 15.0636C41.1676 15.3355 41.0231 15.7092 40.9256 15.9612C40.8596 16.1321 40.8151 16.2471 40.8208 16.2365Z",fill:"#15195A"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4.53636 11.7534H9.99929C10.7398 11.7792 11.3406 12.0008 11.5361 12.7832L12.7233 18.4113C12.7234 18.4118 12.7236 18.4124 12.7238 18.4129L13.0871 20.1072L16.4124 11.7534H20.0028L14.6657 23.991H11.0752L8.04881 13.3464C7.00461 12.7769 5.81289 12.3188 4.48047 12.0009L4.53636 11.7534Z",fill:"#15195A"})),o=()=>Object(l.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"0.5",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M18.6846 27.0292V28.3215V29.6137H18.1154V29.2999C17.9349 29.5327 17.661 29.6787 17.2886 29.6787C16.5546 29.6787 15.9791 29.1112 15.9791 28.3215C15.9791 27.5324 16.5546 26.9642 17.2886 26.9642C17.661 26.9642 17.9349 27.1103 18.1154 27.343V27.0292H18.6846ZM17.3594 27.494C16.8667 27.494 16.5652 27.8672 16.5652 28.3215C16.5652 28.7757 16.8667 29.1489 17.3594 29.1489C17.8302 29.1489 18.148 28.7918 18.148 28.3215C18.148 27.8511 17.8302 27.494 17.3594 27.494ZM37.9186 28.3215C37.9186 27.8672 38.2201 27.494 38.7128 27.494C39.1842 27.494 39.5014 27.8511 39.5014 28.3215C39.5014 28.7918 39.1842 29.1489 38.7128 29.1489C38.2201 29.1489 37.9186 28.7757 37.9186 28.3215ZM40.0386 25.9913V28.3215V29.6137H39.4688V29.2999C39.2882 29.5327 39.0143 29.6787 38.642 29.6787C37.9079 29.6787 37.3325 29.1112 37.3325 28.3215C37.3325 27.5324 37.9079 26.9642 38.642 26.9642C39.0143 26.9642 39.2882 27.1103 39.4688 27.343V25.9913H40.0386ZM25.7496 27.4674C26.1163 27.4674 26.352 27.6945 26.4122 28.0943H25.0538C25.1146 27.7211 25.3441 27.4674 25.7496 27.4674ZM24.4571 28.3215C24.4571 27.5157 24.9937 26.9642 25.7609 26.9642C26.4943 26.9642 26.9983 27.5157 27.0039 28.3215C27.0039 28.397 26.9983 28.4675 26.9926 28.5375L25.0488 28.5375C25.1309 29.0029 25.465 29.1706 25.8317 29.1706C26.0944 29.1706 26.374 29.0728 26.5933 28.9001L26.8723 29.3167C26.5545 29.5815 26.1934 29.6787 25.7991 29.6787C25.0156 29.6787 24.4571 29.1434 24.4571 28.3215ZM32.6337 28.3215C32.6337 27.8672 32.9353 27.494 33.4279 27.494C33.8987 27.494 34.2165 27.8511 34.2165 28.3215C34.2165 28.7918 33.8987 29.1489 33.4279 29.1489C32.9353 29.1489 32.6337 28.7757 32.6337 28.3215ZM34.7529 27.0292V28.3215V29.6137H34.1837V29.2999C34.0026 29.5327 33.7293 29.6787 33.3569 29.6787C32.6229 29.6787 32.0475 29.1112 32.0475 28.3215C32.0475 27.5324 32.6229 26.9642 33.3569 26.9642C33.7293 26.9642 34.0026 27.1103 34.1837 27.343V27.0292H34.7529ZM29.4191 28.3215C29.4191 29.1056 29.972 29.6787 30.8157 29.6787C31.21 29.6787 31.4726 29.5921 31.7572 29.3705L31.4839 28.9162C31.2701 29.0679 31.0457 29.1489 30.7988 29.1489C30.3443 29.1434 30.0102 28.8191 30.0102 28.3215C30.0102 27.8239 30.3443 27.4996 30.7988 27.494C31.0457 27.494 31.2701 27.5751 31.4839 27.7267L31.7572 27.2724C31.4726 27.0509 31.21 26.9642 30.8157 26.9642C29.972 26.9642 29.4191 27.5373 29.4191 28.3215ZM36.0674 27.3431C36.2153 27.1159 36.4291 26.9643 36.7575 26.9643C36.8729 26.9643 37.0371 26.986 37.1631 27.0349L36.9876 27.5646C36.8672 27.5157 36.7469 27.4997 36.6315 27.4997C36.2592 27.4997 36.073 27.7373 36.073 28.165V29.6138H35.5032V27.0293H36.0674V27.3431ZM21.4996 27.2347C21.2257 27.0564 20.8483 26.9642 20.4321 26.9642C19.7689 26.9642 19.342 27.278 19.342 27.7917C19.342 28.2132 19.6599 28.4731 20.2453 28.5542L20.5142 28.5919C20.8264 28.6352 20.9737 28.7163 20.9737 28.8624C20.9737 29.0623 20.7656 29.1762 20.377 29.1762C19.9827 29.1762 19.6981 29.0518 19.5063 28.9057L19.238 29.3433C19.5502 29.5704 19.9444 29.6787 20.3713 29.6787C21.1273 29.6787 21.5654 29.3272 21.5654 28.8352C21.5654 28.3809 21.2207 28.1432 20.6509 28.0621L20.3826 28.0238C20.1363 27.9916 19.9388 27.9433 19.9388 27.77C19.9388 27.5806 20.125 27.4674 20.4371 27.4674C20.7712 27.4674 21.0947 27.5918 21.2533 27.689L21.4996 27.2347ZM28.1542 27.3431C28.3015 27.1159 28.5152 26.9643 28.8437 26.9643C28.959 26.9643 29.1233 26.986 29.2493 27.0349L29.0738 27.5646C28.9534 27.5157 28.833 27.4997 28.7177 27.4997C28.3454 27.4997 28.1592 27.7373 28.1592 28.165V29.6138H27.59V27.0293L28.1542 27.0293V27.3431ZM23.9862 27.0292H23.0553V26.2451H22.4799V27.0292H21.949V27.5429H22.4799V28.7219C22.4799 29.3216 22.7156 29.6787 23.3888 29.6787C23.6358 29.6787 23.9204 29.6032 24.1009 29.4788L23.9367 28.9973C23.7668 29.0945 23.5806 29.1434 23.4327 29.1434C23.1481 29.1434 23.0553 28.9701 23.0553 28.7108V27.5429H23.9862V27.0292ZM15.4758 27.9917V29.6138H14.9003V28.1755C14.9003 27.7373 14.7142 27.4941 14.3255 27.4941C13.9475 27.4941 13.6849 27.7324 13.6849 28.1811V29.6138H13.1095V28.1755C13.1095 27.7373 12.9183 27.4941 12.5403 27.4941C12.151 27.4941 11.899 27.7324 11.899 28.1811V29.6138H11.3242V27.0293H11.894V27.348C12.1078 27.0454 12.3811 26.9643 12.6606 26.9643C13.0606 26.9643 13.3451 27.1376 13.5257 27.4242C13.767 27.0615 14.1118 26.9587 14.4459 26.9643C15.0815 26.9699 15.4758 27.3808 15.4758 27.9917Z",fill:"#231F20"}),Object(l.createElement)("path",{d:"M29.9381 22.6376H21.3115V7.33105H29.9381V22.6376Z",fill:"#FF5F00"}),Object(l.createElement)("path",{d:"M21.8586 14.9846C21.8586 11.8796 23.331 9.11372 25.624 7.33129C23.9472 6.02789 21.831 5.24994 19.5311 5.24994C14.0864 5.24994 9.67285 9.60822 9.67285 14.9846C9.67285 20.361 14.0864 24.7192 19.5311 24.7192C21.831 24.7192 23.9472 23.9413 25.624 22.6379C23.331 20.8555 21.8586 18.0896 21.8586 14.9846Z",fill:"#EB001B"}),Object(l.createElement)("path",{d:"M41.5758 14.9846C41.5758 20.361 37.1622 24.7192 31.7175 24.7192C29.4177 24.7192 27.3014 23.9413 25.624 22.6379C27.9176 20.8555 29.3901 18.0896 29.3901 14.9846C29.3901 11.8796 27.9176 9.11372 25.624 7.33129C27.3014 6.02789 29.4177 5.24994 31.7175 5.24994C37.1622 5.24994 41.5758 9.60822 41.5758 14.9846Z",fill:"#F79E1B"})),a=()=>Object(l.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"0.5",y:"0.5",width:"49.6897",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{d:"M29.9708 22.8244H21.3047V7.35352H29.9708V22.8244Z",fill:"#6C6BBD"}),Object(l.createElement)("path",{d:"M21.8549 15.0891C21.8549 11.9507 23.3341 9.15521 25.6375 7.35365C23.9531 6.03626 21.8272 5.24995 19.5168 5.24995C14.0471 5.24995 9.61328 9.65501 9.61328 15.0891C9.61328 20.5232 14.0471 24.9282 19.5168 24.9282C21.8272 24.9282 23.9531 24.1419 25.6375 22.8245C23.3341 21.023 21.8549 18.2274 21.8549 15.0891Z",fill:"#EB001B"}),Object(l.createElement)("path",{d:"M41.6626 15.0891C41.6626 20.5232 37.2288 24.9282 31.7591 24.9282C29.4487 24.9282 27.3228 24.1419 25.6377 22.8245C27.9418 21.023 29.421 18.2274 29.421 15.0891C29.421 11.9507 27.9418 9.15521 25.6377 7.35365C27.3228 6.03626 29.4487 5.24995 31.7591 5.24995C37.2288 5.24995 41.6626 9.65501 41.6626 15.0891Z",fill:"#0099DF"}),Object(l.createElement)("path",{d:"M32.9036 27.1956C33.0188 27.1956 33.1845 27.2175 33.311 27.2669L33.1347 27.8024C33.0138 27.753 32.8929 27.7367 32.777 27.7367C32.403 27.7367 32.216 27.9769 32.216 28.4085V29.8735H31.6436V27.2613H32.2103V27.5784C32.3589 27.3489 32.5736 27.1956 32.9036 27.1956Z",fill:"#231F20"}),Object(l.createElement)("path",{d:"M30.7887 27.7807H29.8536V28.9611C29.8536 29.2232 29.9468 29.3984 30.2333 29.3984C30.382 29.3984 30.569 29.3489 30.739 29.2507L30.904 29.7368C30.7226 29.8625 30.4367 29.9395 30.1893 29.9395C29.5123 29.9395 29.2762 29.5785 29.2762 28.9717V27.7807H28.7422V27.2615H29.2762V26.469H29.8536V27.2615H30.7887V27.7807Z",fill:"#231F20"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M24.1754 27.1958C24.9128 27.1958 25.4191 27.7532 25.4247 28.5676C25.4247 28.6433 25.4192 28.7135 25.4135 28.7842L25.4134 28.7859H23.4607C23.5432 29.2557 23.8788 29.4252 24.2472 29.4252C24.511 29.4252 24.7919 29.327 25.0116 29.1519L25.2925 29.5729C24.9732 29.8406 24.6105 29.9388 24.2144 29.9388C23.4273 29.9388 22.8662 29.3977 22.8662 28.5676C22.8662 27.7532 23.4052 27.1958 24.1754 27.1958ZM24.1648 27.7036C23.7574 27.7036 23.5269 27.9607 23.4658 28.3379H24.8304C24.77 27.9332 24.5332 27.7036 24.1648 27.7036Z",fill:"#231F20"}),Object(l.createElement)("path",{d:"M27.9386 27.9283C27.7793 27.8295 27.455 27.7038 27.1193 27.7038C26.8057 27.7038 26.6187 27.8189 26.6187 28.0103C26.6187 28.1848 26.8164 28.2342 27.0639 28.2668L27.3334 28.3049C27.9058 28.3875 28.2522 28.6277 28.2522 29.0868C28.2522 29.5841 27.812 29.9395 27.0532 29.9395C26.6237 29.9395 26.2277 29.83 25.9141 29.6004L26.1836 29.1575C26.3763 29.3052 26.6628 29.4309 27.0589 29.4309C27.4493 29.4309 27.6584 29.3164 27.6584 29.1137C27.6584 28.9667 27.5098 28.8842 27.1962 28.841L26.9266 28.8028C26.3379 28.7203 26.0186 28.4582 26.0186 28.0322C26.0186 27.513 26.4481 27.1958 27.1137 27.1958C27.5318 27.1958 27.9115 27.289 28.1861 27.4692L27.9386 27.9283Z",fill:"#231F20"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M35.561 27.3015C35.3872 27.2308 35.1982 27.1958 34.9942 27.1958C34.7902 27.1958 34.6013 27.2308 34.4275 27.3015C34.2537 27.3716 34.1044 27.4685 33.9785 27.5918C33.8526 27.715 33.7537 27.8608 33.6819 28.0284C33.6101 28.1967 33.5742 28.3793 33.5742 28.5764C33.5742 28.7734 33.6101 28.9561 33.6819 29.1243C33.7537 29.292 33.8526 29.4384 33.9785 29.5616C34.1044 29.6848 34.2537 29.7812 34.4275 29.8519C34.6013 29.9219 34.7902 29.9569 34.9942 29.9569C35.1982 29.9569 35.3872 29.9219 35.561 29.8519C35.7348 29.7812 35.8853 29.6848 36.0118 29.5616C36.139 29.4384 36.2379 29.292 36.3097 29.1243C36.3815 28.9561 36.4174 28.7734 36.4174 28.5764C36.4174 28.3793 36.3815 28.1967 36.3097 28.0284C36.2379 27.8608 36.139 27.715 36.0118 27.5918C35.8853 27.4685 35.7348 27.3716 35.561 27.3015ZM34.666 27.7969C34.7674 27.7563 34.8763 27.7356 34.9941 27.7356C35.1118 27.7356 35.2214 27.7563 35.3221 27.7969C35.4235 27.8382 35.5117 27.8958 35.5854 27.9696C35.6603 28.0434 35.7182 28.1322 35.761 28.2354C35.8032 28.3386 35.824 28.4525 35.824 28.5763C35.824 28.7008 35.8032 28.8141 35.761 28.9173C35.7182 29.0205 35.6603 29.1093 35.5854 29.1831C35.5117 29.2569 35.4235 29.3145 35.3221 29.3558C35.2214 29.3971 35.1118 29.4171 34.9941 29.4171C34.8763 29.4171 34.7674 29.3971 34.666 29.3558C34.5652 29.3145 34.4777 29.2569 34.404 29.1831C34.3303 29.1093 34.2724 29.0205 34.2302 28.9173C34.188 28.8141 34.1672 28.7008 34.1672 28.5763C34.1672 28.4525 34.188 28.3386 34.2302 28.2354C34.2724 28.1322 34.3303 28.0434 34.404 27.9696C34.4777 27.8958 34.5652 27.8382 34.666 27.7969Z",fill:"#231F20"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M22.2524 27.2615V28.5676V29.8737H21.6806V29.5566C21.4986 29.7918 21.224 29.9394 20.85 29.9394C20.1126 29.9394 19.5352 29.3652 19.5352 28.5676C19.5352 27.7694 20.1126 27.1958 20.85 27.1958C21.224 27.1958 21.4986 27.3434 21.6806 27.5786V27.2615H22.2524ZM20.9211 27.7312C20.4262 27.7312 20.1233 28.1084 20.1233 28.5675C20.1233 29.0267 20.4262 29.4033 20.9211 29.4033C21.394 29.4033 21.7133 29.0429 21.7133 28.5675C21.7133 28.0921 21.394 27.7312 20.9211 27.7312Z",fill:"#231F20"}),Object(l.createElement)("path",{d:"M19.0293 29.8735V28.234C19.0293 27.6166 18.6332 27.2012 17.9953 27.1956C17.6597 27.19 17.3127 27.2938 17.0709 27.6604C16.8896 27.3707 16.603 27.1956 16.2013 27.1956C15.9211 27.1956 15.6459 27.2775 15.4312 27.5834V27.2613H14.8594V29.8735H15.4368V28.4254C15.4368 27.9719 15.69 27.7311 16.0804 27.7311C16.4601 27.7311 16.6528 27.9769 16.6528 28.4198V29.8735H17.2302V28.4254C17.2302 27.9719 17.4947 27.7311 17.8738 27.7311C18.2649 27.7311 18.4519 27.9769 18.4519 28.4198V29.8735H19.0293V29.8735Z",fill:"#231F20"})),r=()=>Object(l.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"1.18945",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"#006FCF",stroke:"#F3F3F3"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M11.1205 25.2823V18.0771H19.3189L20.1985 19.1441L21.1072 18.0771H50.8653V24.7854C50.8653 24.7854 50.0871 25.2751 49.187 25.2823H32.7093L31.7176 24.1465V25.2823H28.4679V23.3435C28.4679 23.3435 28.0239 23.6141 27.0642 23.6141H25.9581V25.2823H21.0376L20.1593 24.1924L19.2675 25.2823H11.1205ZM1.56836 12.6465L3.41294 8.63574H6.60294L7.64976 10.8824V8.63574H11.6152L12.2384 10.2596L12.8425 8.63574H30.6434V9.4521C30.6434 9.4521 31.5792 8.63574 33.1171 8.63574L38.8928 8.65457L39.9215 10.8718V8.63574H43.24L44.1534 9.90939V8.63574H47.5023V15.841H44.1534L43.2781 14.5632V15.841H38.4025L37.9121 14.7052H36.6014L36.1191 15.841H32.8126C31.4893 15.841 30.6434 15.0413 30.6434 15.0413V15.841H25.658L24.6685 14.7052V15.841H6.13036L5.64039 14.7052H4.33383L3.84732 15.841H1.56836V12.6465ZM1.5779 14.9189L4.06583 9.52391H5.95199L8.43755 14.9189H6.7821L6.32542 13.8386H3.65672L3.19767 14.9189H1.5779ZM5.79982 12.6674L4.98636 10.7795L4.17053 12.6674H5.79982ZM8.60869 14.9182V9.52317L10.9105 9.53115L12.2493 13.0095L13.556 9.52317H15.8394V14.9182H14.3933V10.9429L12.8603 14.9182H11.592L10.0548 10.9429V14.9182H8.60869ZM16.8289 14.9182V9.52317H21.5479V10.73H18.2902V11.6528H21.4717V12.7886H18.2902V13.7469H21.5479V14.9182H16.8289ZM22.3851 14.9189V9.52391H25.6033C26.6696 9.52391 27.625 10.1389 27.625 11.2742C27.625 12.2447 26.8195 12.8698 26.0385 12.9313L27.9413 14.9189H26.1741L24.4402 13.0023H23.8313V14.9189H22.3851ZM25.4843 10.7306H23.8313V11.8664H25.5057C25.7956 11.8664 26.1694 11.6569 26.1694 11.2985C26.1694 11.0199 25.8809 10.7306 25.4843 10.7306ZM29.692 14.9182H28.2154V9.52317H29.692V14.9182ZM33.1931 14.9182H32.8744C31.3323 14.9182 30.396 13.7851 30.396 12.2429C30.396 10.6626 31.3218 9.52317 33.2692 9.52317H34.8676V10.8009H33.2108C32.4202 10.8009 31.8611 11.3763 31.8611 12.2562C31.8611 13.301 32.5004 13.7398 33.4215 13.7398H33.802L33.1931 14.9182ZM33.8521 14.9189L36.34 9.52391H38.2262L40.7117 14.9189H39.0563L38.5996 13.8386H35.9309L35.4719 14.9189H33.8521ZM38.074 12.6674L37.2605 10.7795L36.4447 12.6674H38.074ZM40.8805 14.9182V9.52317H42.7191L45.0667 12.9128V9.52317H46.5128V14.9182H44.7337L42.3267 11.4398V14.9182H40.8805ZM12.1099 24.3594V18.9643H16.8289V20.1711H13.5713V21.0939H16.7528V22.2297H13.5713V23.1881H16.8289V24.3594H12.1099ZM35.2329 24.3594V18.9643H39.9519V20.1711H36.6943V21.0939H39.8606V22.2297H36.6943V23.1881H39.9519V24.3594H35.2329ZM17.0121 24.3594L19.3097 21.6951L16.9574 18.9643H18.7793L20.1803 20.6525L21.586 18.9643H23.3366L21.0151 21.6618L23.317 24.3594H21.4953L20.1351 22.6978L18.8079 24.3594H17.0121ZM23.4887 24.3603V18.9653H26.6831C27.9938 18.9653 28.7595 19.7531 28.7595 20.7799C28.7595 22.0193 27.7832 22.6566 26.4952 22.6566H24.9729V24.3603H23.4887ZM26.5761 20.1853H24.973V21.4276H26.5714C26.9937 21.4276 27.2897 21.1665 27.2897 20.8064C27.2897 20.4232 26.9922 20.1853 26.5761 20.1853ZM29.3875 24.3594V18.9643H32.6056C33.672 18.9643 34.6274 19.5793 34.6274 20.7146C34.6274 21.6851 33.8218 22.3102 33.0409 22.3717L34.9437 24.3594H33.1765L31.4426 22.4427H30.8337V24.3594H29.3875ZM32.4867 20.171H30.8337V21.3068H32.5082C32.798 21.3068 33.1718 21.0974 33.1718 20.7389C33.1718 20.4603 32.8833 20.171 32.4867 20.171ZM40.6217 24.3594V23.1881H43.5159C43.9441 23.1881 44.1295 22.9722 44.1295 22.7355C44.1295 22.5087 43.9447 22.2794 43.5159 22.2794H42.208C41.0712 22.2794 40.4381 21.6334 40.4381 20.6636C40.4381 19.7985 41.0178 18.9643 42.7072 18.9643H45.5233L44.9144 20.1782H42.4788C42.0132 20.1782 41.8699 20.4061 41.8699 20.6237C41.8699 20.8473 42.047 21.0939 42.4027 21.0939H43.7727C45.04 21.0939 45.5899 21.7644 45.5899 22.6424C45.5899 23.5863 44.9772 24.3594 43.7038 24.3594H40.6217ZM45.7176 24.3594V23.1881H48.6118C49.04 23.1881 49.2254 22.9722 49.2254 22.7355C49.2254 22.5087 49.0406 22.2794 48.6118 22.2794H47.3039C46.1671 22.2794 45.534 21.6334 45.534 20.6636C45.534 19.7985 46.1138 18.9643 47.8031 18.9643H50.6192L50.0103 20.1782H47.5747C47.1092 20.1782 46.9658 20.4061 46.9658 20.6237C46.9658 20.8473 47.1429 21.0939 47.4986 21.0939H48.8687C50.1359 21.0939 50.6858 21.7644 50.6858 22.6424C50.6858 23.5863 50.0731 24.3594 48.7997 24.3594H45.7176Z",fill:"white"})),d=()=>Object(l.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"0.878906",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M15.8352 13.0607C15.4642 13.5024 14.8707 13.8507 14.2771 13.8009C14.2029 13.2038 14.4935 12.5693 14.8336 12.1774C15.2045 11.7233 15.8537 11.3999 16.3792 11.375C16.4411 11.997 16.1999 12.6066 15.8352 13.0607ZM16.373 13.9192C15.8501 13.8889 15.373 14.0774 14.9876 14.2297C14.7396 14.3277 14.5296 14.4106 14.3698 14.4106C14.1905 14.4106 13.9718 14.3232 13.7263 14.2251C13.4046 14.0965 13.0367 13.9495 12.651 13.9565C11.7669 13.969 10.9446 14.4728 10.4933 15.2753C9.56588 16.8801 10.2522 19.2563 11.1486 20.5626C11.5876 21.2095 12.1131 21.9186 12.8056 21.8937C13.1102 21.8822 13.3294 21.7886 13.5562 21.6918C13.8173 21.5803 14.0885 21.4645 14.512 21.4645C14.9208 21.4645 15.1802 21.5773 15.4292 21.6856C15.6659 21.7885 15.8933 21.8874 16.2308 21.8813C16.948 21.8689 17.3993 21.2344 17.8383 20.5875C18.312 19.8931 18.5202 19.2155 18.5518 19.1127L18.5555 19.1008C18.5547 19.1 18.5488 19.0973 18.5385 19.0926C18.3802 19.0196 17.1698 18.4621 17.1582 16.9672C17.1465 15.7124 18.1182 15.0767 18.2712 14.9766L18.2712 14.9766C18.2805 14.9705 18.2868 14.9664 18.2896 14.9642C17.6713 14.0436 16.7068 13.9441 16.373 13.9192ZM21.3377 21.8128V12.1153H24.9546C26.8217 12.1153 28.1263 13.4091 28.1263 15.3001C28.1263 17.1911 26.797 18.4974 24.9051 18.4974H22.8339V21.8128H21.3377ZM22.8339 13.3841H24.5589C25.8572 13.3841 26.5991 14.0808 26.5991 15.3062C26.5991 16.5317 25.8572 17.2346 24.5527 17.2346H22.8339V13.3841ZM33.0661 20.6496C32.6704 21.4085 31.7986 21.8874 30.8589 21.8874C29.4678 21.8874 28.4971 21.0539 28.4971 19.7974C28.4971 18.5533 29.4368 17.838 31.1742 17.7322L33.0413 17.6203V17.0853C33.0413 16.2953 32.5282 15.8661 31.6131 15.8661C30.8589 15.8661 30.3086 16.258 30.1973 16.8552H28.8495C28.8928 15.5986 30.0675 14.6842 31.6564 14.6842C33.369 14.6842 34.4819 15.5862 34.4819 16.9858V21.8128H33.097V20.6496H33.0661ZM31.2609 20.7368C30.4633 20.7368 29.9563 20.3511 29.9563 19.7602C29.9563 19.1506 30.4448 18.796 31.3784 18.74L33.0415 18.6343V19.1817C33.0415 20.0898 32.2748 20.7368 31.2609 20.7368ZM39.0756 22.1922C38.4759 23.8903 37.7897 24.4502 36.3306 24.4502C36.2193 24.4502 35.8483 24.4377 35.7617 24.4129V23.2496C35.8545 23.2621 36.0832 23.2745 36.2007 23.2745C36.8623 23.2745 37.2332 22.9946 37.462 22.2668L37.598 21.8376L35.0631 14.7775H36.6273L38.3894 20.5065H38.4203L40.1823 14.7775H41.7033L39.0756 22.1922Z",fill:"black"})),V=()=>Object(l.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"1.18945",y:"0.5",width:"49.6897",height:"34",rx:"2.5",fill:"url(#paint0_linear)",stroke:"#F1F1F1"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M19.7636 16.9816H26.6269C26.5657 15.4963 26.2201 13.9649 25.1717 12.9813C23.9229 11.8096 21.7355 11.375 19.781 11.375C17.7466 11.375 15.4968 11.8517 14.2414 13.1088C13.1588 14.1918 12.9248 15.9341 12.9248 17.4997C12.9248 19.1395 13.3827 21.0469 14.5571 22.1456C15.8059 23.3147 17.8294 23.625 19.781 23.625C21.6767 23.625 23.7302 23.2746 24.9718 22.1647C26.2099 21.0561 26.6377 19.1888 26.6377 17.4997V17.4918H19.7636V16.9816ZM27.0876 17.4921V23.3511H36.6352V23.3432C38.0322 23.267 39.1436 22.0059 39.1436 20.4575C39.1436 18.9084 38.0322 17.5664 36.6352 17.4895V17.4921H27.0876ZM36.5263 11.6203C37.8879 11.6203 38.9687 12.8032 38.9687 14.2957C38.9687 15.7087 37.9762 16.8626 36.7135 16.9816H27.0873V11.6118H36.2251C36.2813 11.6049 36.3468 11.6097 36.4108 11.6144C36.4508 11.6174 36.4901 11.6203 36.5263 11.6203Z",fill:"#FEFEFE"}),Object(l.createElement)("defs",null,Object(l.createElement)("linearGradient",{id:"paint0_linear",x1:"14.4385",y1:"-4.43215",x2:"2.09335",y2:"33.4202",gradientUnits:"userSpaceOnUse"},Object(l.createElement)("stop",{stopColor:"#222E72"}),Object(l.createElement)("stop",{offset:"0.591647",stopColor:"#40CBFF"}),Object(l.createElement)("stop",{offset:"1",stopColor:"#3CB792"})))),m=()=>Object(l.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"1.18945",y:"0.5",width:"49",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8.30356 21.1794C8.30356 20.493 7.95026 20.5381 7.6123 20.5309V20.3325C7.90535 20.347 8.20582 20.347 8.49964 20.347C8.8153 20.347 9.24386 20.3325 9.80068 20.3325C11.7479 20.3325 12.8085 21.6523 12.8085 23.0038C12.8085 23.7601 12.3724 25.6602 9.71022 25.6602C9.32704 25.6602 8.97313 25.6451 8.61983 25.6451C8.28172 25.6451 7.95026 25.6521 7.6123 25.6602V25.4616C8.06302 25.4154 8.28172 25.4004 8.30356 24.8815V21.1794ZM9.04049 24.759C9.04049 25.3469 9.4545 25.4153 9.82282 25.4153C11.4476 25.4153 11.9807 24.1715 11.9807 23.0344C11.9807 21.6071 11.0785 20.5769 9.62735 20.5769C9.31835 20.5769 9.17617 20.5992 9.04049 20.6074V24.759Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M13.0713 25.4613H13.2138C13.4243 25.4613 13.5748 25.4613 13.5748 25.2088V23.1406C13.5748 22.8053 13.4622 22.7589 13.1836 22.6067V22.4849C13.537 22.377 13.9585 22.233 13.9882 22.2101C14.0412 22.1794 14.0856 22.1712 14.1239 22.1712C14.1608 22.1712 14.1762 22.2171 14.1762 22.2788V25.2088C14.1762 25.4613 14.342 25.4613 14.5528 25.4613H14.6801V25.6599C14.4244 25.6599 14.1608 25.6448 13.8909 25.6448C13.6202 25.6448 13.3493 25.6518 13.0713 25.6599V25.4613ZM13.8758 20.9962C13.6799 20.9962 13.5074 20.813 13.5074 20.6146C13.5074 20.4235 13.6881 20.2476 13.8758 20.2476C14.071 20.2476 14.2445 20.4084 14.2445 20.6146C14.2445 20.8211 14.0786 20.9962 13.8758 20.9962Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M15.3957 23.1866C15.3957 22.9048 15.3126 22.8285 14.9598 22.6833V22.5383C15.2826 22.4316 15.5907 22.3319 15.952 22.1714C15.9748 22.1714 15.9966 22.1866 15.9966 22.2476V22.7439C16.426 22.4316 16.7944 22.1714 17.299 22.1714C17.9373 22.1714 18.1628 22.6447 18.1628 23.2401V25.209C18.1628 25.4614 18.3287 25.4614 18.5391 25.4614H18.6747V25.66C18.4107 25.66 18.1478 25.6449 17.8774 25.6449C17.6065 25.6449 17.3356 25.652 17.0649 25.66V25.4614H17.2005C17.4112 25.4614 17.561 25.4614 17.561 25.209V23.233C17.561 22.7974 17.299 22.5839 16.8699 22.5839C16.629 22.5839 16.2455 22.782 15.9966 22.9507V25.209C15.9966 25.4614 16.1628 25.4614 16.3735 25.4614H16.5085V25.66C16.2455 25.66 15.9822 25.6449 15.711 25.6449C15.4409 25.6449 15.1698 25.652 14.8994 25.66V25.4614H15.0351C15.2454 25.4614 15.3957 25.4614 15.3957 25.209V23.1866Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M19.2463 23.5536C19.2309 23.6223 19.2309 23.7366 19.2463 23.9963C19.2906 24.7212 19.7503 25.3162 20.3512 25.3162C20.7654 25.3162 21.0889 25.0871 21.3666 24.8053L21.4716 24.9122C21.1256 25.3777 20.6972 25.7748 20.0811 25.7748C18.8851 25.7748 18.6445 24.5987 18.6445 24.1106C18.6445 22.6144 19.6369 22.1714 20.1627 22.1714C20.7725 22.1714 21.4272 22.5606 21.4342 23.3699C21.4342 23.4163 21.4342 23.4616 21.4272 23.5075L21.3592 23.5536H19.2463ZM20.5777 23.309C20.7653 23.309 20.7873 23.2097 20.7873 23.1177C20.7873 22.7291 20.5544 22.4161 20.1331 22.4161C19.6748 22.4161 19.3588 22.759 19.2687 23.309H20.5777Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M21.6074 25.4614H21.8106C22.0203 25.4614 22.1709 25.4614 22.1709 25.209V23.0646C22.1709 22.8285 21.8929 22.782 21.78 22.721V22.6069C22.3289 22.3701 22.6298 22.1714 22.6986 22.1714C22.7426 22.1714 22.7652 22.1943 22.7652 22.2711V22.9581H22.781C22.9684 22.6605 23.2848 22.1714 23.7433 22.1714C23.9312 22.1714 24.1715 22.3011 24.1715 22.576C24.1715 22.782 24.0294 22.9661 23.8189 22.9661C23.585 22.9661 23.585 22.782 23.3215 22.782C23.1939 22.782 22.7728 22.9581 22.7728 23.4163V25.209C22.7728 25.4614 22.923 25.4614 23.1337 25.4614H23.5543V25.66C23.1408 25.652 22.8261 25.6449 22.5023 25.6449C22.194 25.6449 21.878 25.652 21.6074 25.66V25.4614Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M24.5026 24.5985C24.6006 25.1022 24.9008 25.5303 25.4508 25.5303C25.8937 25.5303 26.059 25.2552 26.059 24.9882C26.059 24.0871 24.4202 24.3775 24.4202 23.1488C24.4202 22.7208 24.7587 22.1714 25.586 22.1714C25.8262 22.1714 26.1495 22.2406 26.4425 22.3934L26.4953 23.1712H26.3225C26.2473 22.6906 25.9845 22.416 25.5027 22.416C25.2019 22.416 24.9164 22.5913 24.9164 22.9195C24.9164 23.8131 26.6606 23.5378 26.6606 24.7362C26.6606 25.2396 26.2625 25.7746 25.3673 25.7746C25.0668 25.7746 24.7127 25.6675 24.4504 25.5149L24.3672 24.637L24.5026 24.5985Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M33.4502 21.7134H33.2626C33.1195 20.8213 32.4953 20.4621 31.6537 20.4621C30.7879 20.4621 29.5325 21.0494 29.5325 22.8812C29.5325 24.4236 30.6158 25.5305 31.7736 25.5305C32.5173 25.5305 33.1353 25.0112 33.2854 24.2093L33.4584 24.2549L33.2854 25.3696C32.9696 25.5683 32.1196 25.7748 31.6227 25.7748C29.8638 25.7748 28.751 24.622 28.751 22.9048C28.751 21.3396 30.1271 20.2173 31.601 20.2173C32.2099 20.2173 32.7963 20.4163 33.3754 20.6226L33.4502 21.7134Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M33.7217 25.461H33.864C34.0753 25.461 34.2254 25.461 34.2254 25.2086V20.9583C34.2254 20.4617 34.1128 20.4466 33.8267 20.3625V20.2402C34.1273 20.1411 34.4434 20.004 34.6017 19.9117C34.6834 19.8666 34.7441 19.8276 34.7662 19.8276C34.8122 19.8276 34.8271 19.874 34.8271 19.9353V25.2086C34.8271 25.461 34.9927 25.461 35.203 25.461H35.3303V25.6596C35.0754 25.6596 34.8122 25.6445 34.5413 25.6445C34.2707 25.6445 34.0003 25.6516 33.7217 25.6596V25.461Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M38.5497 25.2394C38.5497 25.3773 38.632 25.3846 38.7594 25.3846C38.8502 25.3846 38.9625 25.3773 39.061 25.3773V25.538C38.7374 25.5682 38.1205 25.7285 37.9774 25.7744L37.9399 25.7512V25.1329C37.4892 25.5067 37.1429 25.7744 36.6082 25.7744C36.2023 25.7744 35.7815 25.5067 35.7815 24.8664V22.9118C35.7815 22.7131 35.7516 22.5223 35.3311 22.4847V22.3393C35.6019 22.3317 36.2023 22.2861 36.3003 22.2861C36.3838 22.2861 36.3838 22.3393 36.3838 22.5075V24.4765C36.3838 24.7057 36.3838 25.3614 37.0379 25.3614C37.2931 25.3614 37.6316 25.1635 37.9472 24.8967V22.843C37.9472 22.6905 37.5865 22.6065 37.3162 22.5306V22.3933C37.9923 22.3468 38.4139 22.2861 38.489 22.2861C38.5497 22.2861 38.5497 22.3393 38.5497 22.4235V25.2394Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M40.0461 22.7206C40.3468 22.4617 40.753 22.171 41.1666 22.171C42.0391 22.171 42.5655 22.9427 42.5655 23.7745C42.5655 24.774 41.8433 25.7744 40.7674 25.7744C40.2116 25.7744 39.9182 25.5906 39.7223 25.5066L39.4974 25.6822L39.3397 25.5986C39.4069 25.1484 39.4449 24.7057 39.4449 24.2398V20.9583C39.4449 20.4617 39.3317 20.4466 39.0459 20.3625V20.2402C39.347 20.1411 39.6625 20.004 39.8203 19.9117C39.9033 19.8666 39.9633 19.8276 39.9862 19.8276C40.031 19.8276 40.0461 19.8742 40.0461 19.9353V22.7206ZM40.0461 24.7968C40.0461 25.0867 40.317 25.5756 40.8207 25.5756C41.6252 25.5756 41.9634 24.774 41.9634 24.0944C41.9634 23.2703 41.3475 22.5835 40.7611 22.5835C40.4816 22.5835 40.249 22.767 40.0461 22.9427V24.7968Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M11.6628 29.4042L11.671 29.3961V27.8391C11.671 27.4982 11.4374 27.4484 11.3148 27.4484H11.2246V27.3237C11.4173 27.3237 11.6055 27.3402 11.7976 27.3402C11.9653 27.3402 12.1338 27.3237 12.3009 27.3237V27.4484H12.2401C12.0677 27.4484 11.8753 27.4816 11.8753 27.9758V29.8655C11.8753 30.0111 11.8793 30.1563 11.8995 30.2852H11.744L9.63706 27.9008V29.6124C9.63706 29.974 9.7063 30.098 10.0213 30.098H10.091V30.2228C9.91508 30.2228 9.73929 30.2066 9.56334 30.2066C9.37964 30.2066 9.19115 30.2228 9.00684 30.2228V30.098H9.0643C9.3465 30.098 9.43246 29.9027 9.43246 29.5715V27.8216C9.43246 27.5893 9.24366 27.4484 9.06027 27.4484H9.00684V27.3237C9.16203 27.3237 9.32187 27.3402 9.47707 27.3402C9.6002 27.3402 9.71884 27.3237 9.84151 27.3237L11.6628 29.4042Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M11.899 30.3027L11.7305 30.2969L9.65398 27.9477V29.6124C9.65893 29.9735 9.71299 30.0761 10.0211 30.0801H10.1087V30.2406H10.0906C9.91356 30.2406 9.73746 30.2239 9.56291 30.2239C9.37968 30.2239 9.19165 30.2406 9.00625 30.2406H8.98828V30.0801H9.06387C9.33429 30.0785 9.41158 29.9021 9.41453 29.5714V27.8223C9.41375 27.6011 9.23501 27.4665 9.05999 27.4665H8.98828V27.3057H9.00625C9.16268 27.3057 9.32283 27.3219 9.47632 27.3219C9.59837 27.3219 9.71655 27.3057 9.85455 27.3121L11.6528 29.3667V27.8391C11.6508 27.5092 11.4336 27.4692 11.3145 27.4665H11.2062V27.3057H11.2244C11.4177 27.3057 11.606 27.3219 11.7971 27.3219C11.9636 27.3219 12.1314 27.3057 12.3005 27.3057H12.3185V27.4665H12.2396C12.0707 27.4709 11.8973 27.486 11.8924 27.9758V29.8655C11.8924 30.0109 11.8969 30.1559 11.9165 30.2819L11.9203 30.3027H11.899ZM11.7436 30.2666H11.879C11.8607 30.1416 11.8574 30.0037 11.8574 29.8655V27.9759C11.8574 27.4774 12.0633 27.4304 12.2395 27.4301H12.2826V27.3417C12.1214 27.343 11.9598 27.3581 11.797 27.3581C11.6096 27.3581 11.4275 27.343 11.2425 27.3417L11.2421 27.4301H11.3144C11.4402 27.4304 11.6882 27.4871 11.6882 27.8391L11.6827 29.4092L11.6747 29.4172L11.6607 29.4305L9.84099 27.3417C9.71941 27.3417 9.60107 27.3581 9.47624 27.3581C9.32631 27.3581 9.17266 27.343 9.02428 27.3417L9.02351 27.4301H9.0599C9.25181 27.4304 9.44929 27.5791 9.44929 27.8223V29.5714C9.44929 29.9037 9.35806 30.116 9.06378 30.1171L9.02428 30.116V30.2051C9.20147 30.2037 9.38408 30.1884 9.56282 30.1884C9.7335 30.1884 9.90387 30.2037 10.0727 30.2051V30.1171H10.021C9.69896 30.116 9.61919 29.9735 9.61888 29.6124V27.8544L11.7436 30.2666ZM11.6623 29.4042L11.6752 29.3922L11.6623 29.4042ZM11.6528 29.3961V29.3945L11.6495 29.3918L11.6528 29.3961Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M12.9145 27.5313C12.6072 27.5313 12.5953 27.6064 12.534 27.9092H12.4111C12.4272 27.7928 12.4475 27.6767 12.4602 27.5562C12.4766 27.4394 12.4849 27.3236 12.4849 27.2035H12.5829C12.6159 27.3281 12.7181 27.3236 12.8292 27.3236H14.9396C15.0507 27.3236 15.1528 27.3195 15.1611 27.1948L15.2588 27.2118C15.2431 27.3236 15.2263 27.4358 15.2144 27.5482C15.2063 27.6603 15.2063 27.7721 15.2063 27.8842L15.0834 27.9304C15.075 27.777 15.0547 27.5313 14.7806 27.5313H14.1094V29.7408C14.1094 30.0612 14.2529 30.0978 14.449 30.0978H14.527V30.2227C14.3673 30.2227 14.0808 30.2066 13.8602 30.2066C13.6144 30.2066 13.3277 30.2227 13.1681 30.2227V30.0978H13.2461C13.4716 30.0978 13.5856 30.0774 13.5856 29.7495V27.5313H12.9145Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M14.5273 30.2405C14.366 30.2405 14.0798 30.224 13.8602 30.224C13.6147 30.224 13.3285 30.2405 13.168 30.2405H13.1505V30.08H13.2461C13.4716 30.0746 13.5622 30.0717 13.5673 29.7495V27.5491H12.9144V27.5128H13.6028V29.7495C13.6028 30.083 13.4703 30.1166 13.2461 30.1169H13.1855V30.2047C13.3469 30.2035 13.6226 30.1883 13.8602 30.1883C14.0731 30.1883 14.3465 30.2035 14.5087 30.2047V30.1169H14.4489C14.251 30.1166 14.0917 30.0673 14.0917 29.7409V27.5128H14.7805C15.055 27.5139 15.0914 27.7506 15.0999 27.9052L15.1885 27.872C15.1885 27.7635 15.1889 27.6549 15.1962 27.5458C15.2083 27.4384 15.2232 27.3329 15.2385 27.2265L15.1764 27.216C15.1573 27.3369 15.0419 27.3433 14.9394 27.3417H12.8077C12.7114 27.3419 12.6065 27.3372 12.5697 27.2213H12.5021C12.5015 27.3361 12.4938 27.4475 12.4783 27.5582C12.4661 27.6723 12.4473 27.7819 12.4324 27.892H12.5198C12.5735 27.6022 12.6085 27.5087 12.9144 27.5128V27.5491C12.6113 27.5552 12.618 27.6066 12.5512 27.9135L12.5483 27.9276H12.3906L12.3928 27.9061C12.4095 27.7902 12.4303 27.6736 12.4422 27.553C12.4592 27.4378 12.467 27.3225 12.467 27.2035V27.1851H12.5969L12.6 27.1982C12.6276 27.3029 12.7033 27.3037 12.8077 27.3056H14.9394C15.0534 27.3037 15.1364 27.3021 15.1434 27.1935L15.1448 27.1738L15.1634 27.1776L15.2791 27.1962L15.2761 27.2145C15.2599 27.326 15.2439 27.4378 15.2315 27.5491C15.2239 27.6604 15.2239 27.772 15.2239 27.8843V27.8968L15.212 27.9017L15.0672 27.9555L15.0663 27.9314C15.055 27.7752 15.0392 27.5491 14.7805 27.5491H14.1274V29.7409C14.1314 30.054 14.2537 30.0761 14.4489 30.08H14.5446V30.2405H14.5273Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M15.3896 30.098H15.4473C15.5941 30.098 15.7501 30.0775 15.7501 29.8614V27.6856C15.7501 27.4691 15.5941 27.4486 15.4473 27.4486H15.3896V27.3237C15.6384 27.3237 16.0649 27.3402 16.408 27.3402C16.7523 27.3402 17.1773 27.3237 17.4555 27.3237C17.4484 27.5023 17.4523 27.7771 17.4643 27.9595L17.341 27.9925C17.3214 27.7224 17.2723 27.507 16.8425 27.507H16.2744V28.5944H16.7604C17.0059 28.5944 17.0593 28.4537 17.0836 28.2293H17.2063C17.1981 28.3915 17.1938 28.5534 17.1938 28.7152C17.1938 28.8733 17.1981 29.031 17.2063 29.1885L17.0836 29.2134C17.0593 28.9645 17.0471 28.8026 16.7644 28.8026H16.2744V29.7697C16.2744 30.0401 16.5107 30.0401 16.7728 30.0401C17.2639 30.0401 17.4806 30.0066 17.6034 29.5338L17.7176 29.5624C17.6644 29.7829 17.6157 30.0024 17.579 30.2228C17.3167 30.2228 16.8462 30.2068 16.4786 30.2068C16.1096 30.2068 15.6231 30.2228 15.3896 30.2228V30.098Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M17.5784 30.2406C17.3159 30.2406 16.845 30.2233 16.478 30.2233C16.1093 30.2233 15.623 30.2406 15.3893 30.2406H15.3721V30.0801H15.4469C15.595 30.0776 15.7297 30.0644 15.7316 29.8614V27.6856C15.7297 27.4827 15.595 27.4692 15.4469 27.4665H15.3721V27.3057H15.3893C15.6392 27.3057 16.0649 27.3219 16.4078 27.3219C16.7518 27.3219 17.1766 27.3057 17.4553 27.3057H17.4734L17.4728 27.3248C17.4701 27.3869 17.4684 27.4608 17.4684 27.539C17.4684 27.6825 17.4728 27.8399 17.4808 27.9581L17.4816 27.9729L17.4678 27.9771L17.3239 28.0153L17.3233 27.9939C17.2992 27.7245 17.2635 27.5284 16.8422 27.5243H16.2907L16.2904 28.5762H16.76C16.9959 28.5737 17.038 28.4514 17.0659 28.2274L17.0668 28.2107H17.2242L17.2239 28.2297C17.216 28.392 17.2112 28.5534 17.2112 28.7152C17.2112 28.8722 17.216 29.0299 17.2239 29.1878L17.2242 29.2029L17.2093 29.2063L17.0668 29.2351L17.0659 29.2156C17.0376 28.962 17.0357 28.823 16.7642 28.8201H16.2907V29.7698C16.2913 30.0222 16.5057 30.0208 16.7724 30.0222C17.2651 30.0192 17.4624 29.9947 17.5857 29.5289L17.5899 29.5121L17.6071 29.5149L17.7383 29.5495L17.7345 29.5666C17.6814 29.7864 17.6326 30.006 17.5956 30.2258L17.5925 30.2406H17.5784ZM17.5634 30.2048C17.5992 29.9948 17.6458 29.785 17.696 29.576L17.6155 29.5554C17.4929 30.0199 17.2555 30.0614 16.7726 30.0583C16.5147 30.0583 16.257 30.0583 16.2556 29.7698V28.7846H16.7644C17.0501 28.7816 17.0789 28.9564 17.0994 29.1927L17.1877 29.1743C17.1798 29.0212 17.1757 28.8674 17.1757 28.7152C17.1757 28.5593 17.1798 28.4037 17.1877 28.2469H17.0994C17.0763 28.4639 17.0095 28.6153 16.7602 28.6126H16.2556V27.4884H16.8424C17.2679 27.4849 17.3384 27.7085 17.357 27.9698L17.4451 27.9454C17.438 27.8283 17.4332 27.6777 17.4332 27.539C17.4332 27.4679 17.435 27.4005 17.4373 27.3417C17.1593 27.343 16.7444 27.3581 16.408 27.3581C16.0717 27.3581 15.6579 27.343 15.4074 27.3417V27.4301H15.4471C15.5927 27.4304 15.7667 27.456 15.7677 27.6857V29.8614C15.7667 30.0907 15.5927 30.116 15.4471 30.1171H15.4074V30.2048C15.6456 30.204 16.1186 30.1884 16.4781 30.1884C16.8393 30.1884 17.2996 30.204 17.5634 30.2048Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M18.27 27.7636C18.27 27.4608 18.1062 27.4484 17.9792 27.4484H17.9053V27.3237C18.0363 27.3237 18.29 27.3402 18.5398 27.3402C18.7849 27.3402 18.9816 27.3237 19.1984 27.3237C19.7132 27.3237 20.1725 27.4647 20.1725 28.0549C20.1725 28.4284 19.9268 28.6565 19.6037 28.7861L20.3031 29.8487C20.418 30.0244 20.4991 30.0736 20.7002 30.098V30.2228C20.5648 30.2228 20.4339 30.2068 20.2993 30.2068C20.1725 30.2068 20.041 30.2228 19.9148 30.2228C19.5994 29.8035 19.3291 29.3552 19.0634 28.8768H18.7937V29.7661C18.7937 30.0859 18.9407 30.098 19.1282 30.098H19.2024V30.2228C18.9687 30.2228 18.7324 30.2068 18.4986 30.2068C18.3022 30.2068 18.1099 30.2228 17.9053 30.2228V30.098H17.9792C18.1311 30.098 18.27 30.0277 18.27 29.8743V27.7636ZM18.7938 28.7271H18.9938C19.4035 28.7271 19.624 28.5697 19.624 28.0794C19.624 27.7101 19.3908 27.4733 19.0262 27.4733C18.9033 27.4733 18.851 27.4861 18.7938 27.4901V28.7271Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M20.6994 30.2406C20.5622 30.2406 20.4321 30.2239 20.2975 30.2239C20.1728 30.2239 20.0419 30.2406 19.8994 30.2337C19.5855 29.8155 19.3165 29.3697 19.0522 28.8955H18.8105V29.7653C18.8149 30.078 18.9384 30.0757 19.1275 30.0801H19.2193V30.2406H19.2012C18.9674 30.2406 18.7296 30.2239 18.4977 30.2239C18.3023 30.2239 18.1101 30.2406 17.9045 30.2406H17.8867V30.0801H17.9784C18.1256 30.079 18.2499 30.0139 18.2505 29.8743V27.7636C18.2474 27.4692 18.1054 27.4706 17.9784 27.4665H17.8867V27.3057H17.9045C18.037 27.3057 18.2896 27.3219 18.5391 27.3219C18.7833 27.3219 18.9796 27.3057 19.1977 27.3057C19.7136 27.3068 20.1883 27.4509 20.1895 28.0543C20.1895 28.4291 19.9455 28.6633 19.6299 28.794L20.317 29.8388C20.4315 30.0111 20.5027 30.0541 20.7018 30.0801L20.7166 30.0824V30.2406H20.6994ZM18.7927 28.8589H19.0723L19.0774 28.8682C19.3438 29.3453 19.6127 29.7938 19.9139 30.2051C20.0385 30.2051 20.17 30.1884 20.2974 30.1884C20.428 30.1884 20.5544 30.2029 20.6814 30.2048V30.1136C20.4896 30.0899 20.3995 30.0311 20.2878 29.8589L19.5755 28.7778L19.5956 28.7695C19.9161 28.6414 20.1535 28.4195 20.1537 28.0543C20.1535 27.4782 19.712 27.3445 19.1976 27.3417C18.9821 27.3417 18.7854 27.3585 18.539 27.3585C18.2986 27.3585 18.0558 27.343 17.9219 27.3417V27.4301H17.9783C18.1053 27.4304 18.2861 27.4518 18.2861 27.7637V29.8743C18.2861 30.041 18.1323 30.1168 17.9783 30.1171H17.9219V30.2048C18.1186 30.204 18.3054 30.1884 18.4977 30.1884C18.7261 30.1884 18.9572 30.204 19.1835 30.2048V30.1171H19.1274C18.9411 30.1168 18.7746 30.0933 18.7746 29.7654V28.8589H18.7927ZM18.7926 28.7449H18.7745V27.4733L18.7901 27.472C18.8462 27.4674 18.9013 27.456 19.0251 27.456C19.3978 27.456 19.6405 27.7017 19.6408 28.0804C19.6397 28.5762 19.4055 28.7448 18.9927 28.7449H18.7926ZM18.993 28.7094C19.398 28.7053 19.6021 28.5628 19.6061 28.0803C19.6037 27.7174 19.3817 27.4923 19.0253 27.4908C18.9149 27.4908 18.8627 27.5011 18.8105 27.5063V28.7094H18.993Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M23.4779 29.4042L23.485 29.3961V27.8391C23.485 27.4982 23.2526 27.4484 23.1297 27.4484H23.0401V27.3237C23.2324 27.3237 23.42 27.3402 23.6128 27.3402C23.781 27.3402 23.9475 27.3237 24.1165 27.3237V27.4484H24.0549C23.8829 27.4484 23.6907 27.4816 23.6907 27.9758V29.8655C23.6907 30.0111 23.6948 30.1563 23.7149 30.2852H23.56L21.453 27.9008V29.6124C21.453 29.974 21.522 30.098 21.8371 30.098H21.9068V30.2228C21.731 30.2228 21.5547 30.2066 21.3789 30.2066C21.1941 30.2066 21.0063 30.2228 20.8223 30.2228V30.098H20.8791C21.1616 30.098 21.248 29.9027 21.248 29.5715V27.8216C21.248 27.5893 21.0595 27.4484 20.8755 27.4484H20.8223V27.3237C20.9772 27.3237 21.1379 27.3402 21.2931 27.3402C21.4147 27.3402 21.534 27.3237 21.6569 27.3237L23.4779 29.4042Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M23.7146 30.3027L23.5466 30.2969L21.4697 27.948V29.6122C21.4745 29.974 21.5284 30.0761 21.8369 30.0801H21.9237V30.2406H21.9059C21.729 30.2406 21.5534 30.2233 21.378 30.2233C21.1954 30.2233 21.0063 30.2406 20.822 30.2406H20.8037V30.0801H20.879C21.1493 30.079 21.2267 29.9021 21.2293 29.5704V27.8216C21.2289 27.6003 21.0504 27.4665 20.8754 27.4665H20.8037V27.3057H20.822C20.9783 27.3057 21.1384 27.3219 21.2928 27.3219C21.413 27.3219 21.5315 27.3057 21.6687 27.311L23.4676 29.3667V27.8391C23.4665 27.5092 23.2486 27.4684 23.1296 27.4665H23.0217V27.3057H23.0395C23.2328 27.3057 23.4214 27.3219 23.6127 27.3219C23.7794 27.3219 23.9469 27.3057 24.1156 27.3057H24.1337V27.4665H24.0547C23.8859 27.4706 23.712 27.486 23.708 27.9758V29.8655C23.708 30.0109 23.7115 30.1549 23.7324 30.2822L23.7344 30.3027H23.7146ZM23.5596 30.2666H23.6937C23.6757 30.1424 23.6722 30.0037 23.6722 29.8655V27.9759C23.6725 27.4774 23.8794 27.4312 24.0546 27.4301H24.0976V27.3417C23.9365 27.343 23.7748 27.3581 23.6125 27.3581C23.4245 27.3581 23.2422 27.343 23.0571 27.3417V27.4301H23.1294C23.2555 27.4312 23.5033 27.4871 23.5033 27.8391L23.4974 29.4092L23.49 29.4172L23.4772 29.4309L21.6566 27.3417C21.5349 27.3417 21.4163 27.3581 21.2927 27.3581C21.1409 27.3581 20.988 27.343 20.8393 27.3417V27.4301H20.8752C21.0678 27.4312 21.2646 27.5785 21.2646 27.8217V29.5705C21.2646 29.9037 21.1722 30.116 20.8788 30.1171L20.8393 30.1168V30.2049C21.016 30.2037 21.1983 30.1884 21.3778 30.1884C21.5495 30.1884 21.7195 30.2037 21.8887 30.2049V30.1171H21.8368C21.5148 30.1168 21.4345 29.974 21.4336 29.6123V27.855L23.5596 30.2666ZM23.4775 29.4042L23.4908 29.3922L23.4775 29.4042ZM23.4674 29.3961V29.395L23.4645 29.3918L23.4674 29.3961Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M24.7741 29.6286C24.7329 29.7693 24.6835 29.8784 24.6835 29.9521C24.6835 30.0771 24.8559 30.0977 24.9905 30.0977H25.0362V30.2224C24.8718 30.2134 24.7049 30.2064 24.5402 30.2064C24.3929 30.2064 24.2464 30.2134 24.0986 30.2224V30.0977H24.1236C24.2828 30.0977 24.4183 30.002 24.479 29.8275L25.1337 27.9218C25.1872 27.7677 25.2609 27.5602 25.2858 27.4062C25.4159 27.3607 25.5803 27.2782 25.6577 27.2277C25.6704 27.2235 25.678 27.2192 25.6904 27.2192C25.7028 27.2192 25.7104 27.2192 25.7194 27.2323C25.7314 27.2651 25.7434 27.3026 25.7561 27.3357L26.5093 29.5082C26.5584 29.6531 26.607 29.8067 26.6594 29.932C26.7092 30.0485 26.7952 30.0977 26.9307 30.0977H26.9552V30.2224C26.7709 30.2134 26.5862 30.2064 26.3908 30.2064C26.1902 30.2064 25.985 30.2134 25.7763 30.2224V30.0977H25.8214C25.915 30.0977 26.0756 30.0812 26.0756 29.9773C26.0756 29.9237 26.0386 29.8114 25.9929 29.678L25.8335 29.1964H24.9048L24.7741 29.6286ZM25.3715 27.7887H25.363L24.9827 28.9642H25.7472L25.3715 27.7887Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M26.954 30.2404C26.7702 30.2324 26.5857 30.2231 26.3909 30.2231C26.1903 30.2231 25.9861 30.2324 25.7774 30.2404L25.7585 30.2414V30.0791H25.8215C25.9163 30.0791 26.0569 30.0576 26.0572 29.9776C26.0582 29.9302 26.0215 29.8164 25.9764 29.6844L25.8206 29.2145H24.9181L24.7907 29.6342C24.7495 29.7754 24.6998 29.8851 24.7006 29.9527C24.702 30.0541 24.8547 30.0791 24.9905 30.0791H25.0534V30.2414L25.0348 30.2404C24.8713 30.2324 24.7043 30.2231 24.5406 30.2231C24.3945 30.2231 24.2466 30.2324 24.1001 30.2404L24.0811 30.2414V30.0791H24.1235C24.2756 30.0788 24.4026 29.9906 24.4618 29.8219L25.1171 27.9156C25.1699 27.7619 25.2442 27.5553 25.2796 27.3893C25.408 27.3451 25.5726 27.2613 25.6511 27.2112C25.6626 27.2071 25.6739 27.2017 25.6903 27.2017C25.7004 27.2011 25.7224 27.2038 25.7357 27.2255C25.7473 27.2596 25.7603 27.2969 25.7727 27.3303L26.5261 29.5025C26.5742 29.6479 26.6232 29.8013 26.6768 29.9244C26.7242 30.0351 26.7993 30.0788 26.9308 30.0791H26.9721V30.2414L26.954 30.2404ZM25.7943 30.2036C25.9968 30.1963 26.1955 30.1883 26.391 30.1883C26.5807 30.1883 26.7591 30.1963 26.9369 30.2036V30.1166H26.9309C26.7906 30.1176 26.6941 30.0616 26.6439 29.9391C26.5906 29.8137 26.5409 29.6595 26.4926 29.5141L25.7392 27.3417C25.7268 27.3086 25.7152 27.2715 25.7045 27.2419C25.7005 27.2378 25.7014 27.2378 25.6981 27.2378H25.6904C25.6822 27.2378 25.6772 27.2403 25.6667 27.243C25.588 27.2949 25.424 27.3768 25.3023 27.4092C25.2773 27.5666 25.2036 27.7741 25.1505 27.928L24.4958 29.8338C24.4335 30.0143 24.2896 30.1169 24.1236 30.1166H24.1168V30.2036C24.2571 30.1963 24.3987 30.1883 24.5407 30.1883C24.699 30.1883 24.8604 30.1963 25.017 30.2036V30.1166H24.9906C24.857 30.1147 24.6708 30.1 24.6661 29.9528C24.6665 29.8704 24.7166 29.7642 24.7568 29.6238L24.7741 29.629L24.7568 29.6233L24.8915 29.1795H25.8456L26.0095 29.6727C26.0555 29.8063 26.0926 29.917 26.0926 29.9777C26.0866 30.1048 25.9144 30.1143 25.8216 30.1166H25.7943V30.2036ZM24.958 28.9822L25.3497 27.7704H25.3717V27.7888L25.3674 27.7901L25.3717 27.7888V27.7704H25.384L25.7729 28.9822H24.958ZM25.0067 28.946H25.723L25.3669 27.8331L25.0067 28.946ZM25.3539 27.7943L25.3634 27.7913L25.3539 27.7943Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M27.1347 27.5313C26.8279 27.5313 26.8155 27.6064 26.7538 27.9092H26.6309C26.647 27.7928 26.6679 27.6767 26.6806 27.5562C26.6965 27.4394 26.7044 27.3236 26.7044 27.2035H26.8034C26.8355 27.3281 26.938 27.3236 27.0484 27.3236H29.1601C29.2701 27.3236 29.3723 27.3195 29.3804 27.1948L29.4783 27.2118C29.4631 27.3236 29.4468 27.4358 29.434 27.5482C29.425 27.6603 29.425 27.7721 29.425 27.8842L29.3028 27.9304C29.2955 27.777 29.2749 27.5313 29.0001 27.5313H28.3292V29.7408C28.3292 30.0612 28.4726 30.0978 28.6687 30.0978H28.7469V30.2227C28.5871 30.2227 28.3011 30.2066 28.0797 30.2066C27.8346 30.2066 27.5475 30.2227 27.3879 30.2227V30.0978H27.4658C27.6914 30.0978 27.8055 30.0774 27.8055 29.7495V27.5313H27.1347Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M28.748 30.2405C28.5872 30.2405 28.3003 30.224 28.0804 30.224C27.836 30.224 27.5495 30.2405 27.3892 30.2405H27.371V30.08H27.4669C27.6926 30.0746 27.783 30.0717 27.7885 29.7495L27.7881 27.5491H27.1358V27.5128H27.8242V29.7495C27.8242 30.083 27.6917 30.1158 27.4669 30.1166H27.4067V30.205C27.5684 30.2035 27.8433 30.1883 28.0804 30.1883C28.2943 30.1883 28.5681 30.2035 28.7297 30.205V30.1166H28.6696C28.4724 30.1158 28.313 30.0677 28.3127 29.7409V27.5128H29.0012C29.2761 27.5139 29.3119 27.7506 29.3198 27.9052L29.4087 27.872C29.4087 27.7635 29.4093 27.6549 29.4177 27.5463C29.4287 27.4392 29.4442 27.3329 29.459 27.227L29.3972 27.216C29.3785 27.3369 29.263 27.3436 29.1612 27.3417H27.0286C26.9321 27.3428 26.8277 27.3372 26.7905 27.221H26.7236C26.7227 27.3359 26.7146 27.4475 26.6985 27.5582C26.6874 27.6728 26.6677 27.7827 26.6521 27.892H26.74C26.7938 27.6022 26.8293 27.5082 27.1358 27.5128V27.5491C26.8323 27.5544 26.839 27.6066 26.7723 27.9135L26.7693 27.9276H26.6113L26.6138 27.907C26.631 27.7902 26.6511 27.6736 26.6631 27.554C26.6798 27.4378 26.6874 27.3225 26.6874 27.2035V27.1851H26.8173L26.8207 27.1982C26.8494 27.3029 26.9239 27.3037 27.0286 27.3056H29.1612C29.2744 27.3037 29.357 27.3021 29.3643 27.1938L29.3649 27.1738L29.3839 27.1776L29.5001 27.1962L29.4973 27.2145C29.4804 27.326 29.4649 27.4378 29.4525 27.5491C29.4442 27.6604 29.4442 27.7722 29.4442 27.8843V27.8968L29.4322 27.9017L29.2874 27.9555L29.2868 27.9314C29.2761 27.7752 29.2599 27.5491 29.0012 27.5491H28.3479V29.7409C28.3521 30.054 28.4746 30.0761 28.6696 30.08H28.765V30.2405H28.748Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M29.6299 30.098H29.6872C29.8346 30.098 29.9895 30.0775 29.9895 29.8614V27.6856C29.9895 27.4691 29.8346 27.4486 29.6872 27.4486H29.6299V27.3237C29.7896 27.3237 30.0346 27.3402 30.2349 27.3402C30.4399 27.3402 30.6856 27.3237 30.8783 27.3237V27.4486H30.8208C30.6729 27.4486 30.5174 27.4691 30.5174 27.6856V29.8614C30.5174 30.0775 30.6729 30.098 30.8208 30.098H30.8783V30.2228C30.6817 30.2228 30.4359 30.2068 30.2316 30.2068C30.0307 30.2068 29.7896 30.2228 29.6299 30.2228V30.098Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M30.8792 30.2406C30.6814 30.2406 30.4367 30.2233 30.2325 30.2233C30.0322 30.2233 29.7906 30.2406 29.6308 30.2406H29.6133V30.0801H29.6882C29.8357 30.0776 29.9717 30.0644 29.9726 29.8614V27.6856C29.9717 27.4827 29.8357 27.4692 29.6882 27.4665H29.6133V27.3057H29.6308C29.7906 27.3057 30.0367 27.3219 30.2358 27.3219C30.4397 27.3219 30.6852 27.3057 30.8792 27.3057H30.8962V27.4665H30.8218C30.6721 27.4692 30.5375 27.4827 30.5361 27.6856V29.8614C30.5375 30.0644 30.6721 30.0776 30.8218 30.0801H30.8962V30.2406H30.8792ZM30.8605 30.2048V30.1168H30.8218C30.6749 30.1168 30.5013 30.0907 30.5013 29.8614V27.6857C30.5013 27.456 30.6749 27.4304 30.8218 27.4301H30.8605V27.3417C30.6718 27.343 30.4349 27.3581 30.2357 27.3581C30.0415 27.3581 29.8073 27.343 29.6485 27.3417V27.4301H29.6882C29.8334 27.4304 30.0078 27.456 30.0083 27.6857V29.8614C30.0078 30.0907 29.8334 30.1168 29.6882 30.1168H29.6485V30.2048C29.8062 30.2029 30.0376 30.1884 30.2324 30.1884C30.4308 30.1884 30.6675 30.2037 30.8605 30.2048Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M32.5103 27.2612C33.3826 27.2612 34.0779 27.81 34.0779 28.6947C34.0779 29.65 33.4026 30.285 32.5314 30.285C31.6636 30.285 31.001 29.6868 31.001 28.7937C31.001 27.9303 31.6595 27.2612 32.5103 27.2612ZM32.5722 30.1022C33.3663 30.1022 33.5048 29.3915 33.5048 28.7859C33.5048 28.179 33.1823 27.4442 32.5027 27.4442C31.7868 27.4442 31.5738 28.0922 31.5738 28.6482C31.5738 29.3915 31.9096 30.1022 32.5722 30.1022Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M30.9824 28.7937C30.9841 27.9203 31.6503 27.2446 32.5099 27.2432V27.2795C31.6687 27.2795 31.018 27.9394 31.0171 28.7937C31.0188 29.6767 31.6718 30.266 32.5313 30.2663C33.3937 30.266 34.0592 29.6401 34.0603 28.694C34.0595 27.8204 33.3752 27.2803 32.5099 27.2795V27.2432C33.3884 27.2441 34.0936 27.7983 34.0951 28.694C34.0942 29.6593 33.4109 30.3009 32.5313 30.3024C31.656 30.3009 30.9841 29.6967 30.9824 28.7937ZM31.5555 28.6481C31.5567 28.0892 31.7715 27.4255 32.5021 27.4255C33.1969 27.4271 33.5209 28.1755 33.5223 28.7858C33.5209 29.3914 33.3806 30.1191 32.572 30.1191V30.0839C33.35 30.0833 33.4852 29.3914 33.4864 28.7858C33.4864 28.1842 33.1672 27.4632 32.5021 27.4621C31.8002 27.4629 31.5928 28.0949 31.5909 28.6481C31.5912 29.3879 31.9239 30.0828 32.572 30.0839V30.1191C31.8928 30.1185 31.5567 29.3958 31.5555 28.6481Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M36.8353 29.4042L36.8439 29.3961V27.8391C36.8439 27.4982 36.6098 27.4484 36.4872 27.4484H36.3981V27.3237C36.5899 27.3237 36.7785 27.3402 36.9703 27.3402C37.1385 27.3402 37.3062 27.3237 37.4741 27.3237V27.4484H37.4126C37.2411 27.4484 37.048 27.4816 37.048 27.9758V29.8655C37.048 30.0111 37.0523 30.1563 37.0731 30.2852H36.9174L34.8098 27.9008V29.6124C34.8098 29.974 34.8796 30.098 35.194 30.098H35.2642V30.2228C35.0881 30.2228 34.9124 30.2066 34.7365 30.2066C34.5522 30.2066 34.3638 30.2228 34.1797 30.2228V30.098H34.2371C34.52 30.098 34.6053 29.9027 34.6053 29.5715V27.8216C34.6053 27.5893 34.4177 27.4484 34.2327 27.4484H34.1797V27.3237C34.3352 27.3237 34.4946 27.3402 34.6504 27.3402C34.7729 27.3402 34.8909 27.3237 35.0142 27.3237L36.8353 29.4042Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M37.0729 30.3027L36.9036 30.2969L34.827 27.948V29.6124C34.8318 29.974 34.8859 30.0757 35.1932 30.0793H35.2813V30.2406H35.264C35.0868 30.2406 34.9111 30.2239 34.7361 30.2239C34.5527 30.2239 34.3644 30.2406 34.1788 30.2406H34.1621V30.0793H34.2366C34.5063 30.0785 34.584 29.9021 34.5873 29.5714V27.8216C34.5871 27.6003 34.4082 27.4665 34.2324 27.4665H34.1621V27.3057H34.1788C34.3357 27.3057 34.4953 27.3219 34.6498 27.3219C34.7713 27.3219 34.8888 27.3057 35.0276 27.3121L36.8255 29.3667V27.8391C36.8241 27.5092 36.6062 27.4692 36.4868 27.4665H36.379V27.3057H36.3977C36.5899 27.3057 36.7787 27.3219 36.9694 27.3219C37.1368 27.3219 37.3038 27.3057 37.4739 27.3057H37.4911V27.4665H37.4121C37.2434 27.4709 37.0693 27.4866 37.0659 27.9758V29.8655C37.0659 30.0109 37.0691 30.1559 37.089 30.2822L37.0925 30.3027H37.0729ZM36.9171 30.2666H37.0512C37.0339 30.1416 37.0298 30.0037 37.0298 29.8655V27.9759C37.0298 27.4766 37.2365 27.4312 37.4121 27.4301H37.4564V27.3417C37.2939 27.343 37.133 27.3581 36.9694 27.3581C36.7831 27.3581 36.6002 27.343 36.4149 27.3417V27.4301H36.4868C36.6132 27.4312 36.8602 27.4879 36.8602 27.8391L36.8559 29.4092L36.848 29.4172L36.835 29.4309L35.014 27.3417C34.8929 27.3417 34.7738 27.3581 34.6499 27.3581C34.499 27.3581 34.3454 27.343 34.1965 27.3417V27.4301H34.2325C34.4248 27.4312 34.6225 27.5785 34.6225 27.8217V29.5714C34.6225 29.9037 34.5311 30.116 34.2366 30.1171L34.1965 30.1168V30.2049C34.3742 30.2037 34.5563 30.1884 34.7361 30.1884C34.9068 30.1884 35.0764 30.2037 35.246 30.2049V30.1171H35.1932C34.8721 30.1168 34.7916 29.974 34.7916 29.6124V27.8544L36.9171 30.2666ZM36.8351 29.4042L36.8481 29.3922L36.8351 29.4042ZM36.8256 29.3961V29.395L36.8224 29.3918L36.8256 29.3961Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M38.1311 29.6286C38.091 29.7693 38.0416 29.8784 38.0416 29.9521C38.0416 30.0771 38.214 30.0977 38.3481 30.0977H38.3938V30.2224C38.2299 30.2134 38.062 30.2064 37.8981 30.2064C37.7508 30.2064 37.6037 30.2134 37.457 30.2224V30.0977H37.4803C37.6407 30.0977 37.7762 30.002 37.836 29.8275L38.4921 27.9218C38.5449 27.7677 38.619 27.5602 38.6425 27.4062C38.774 27.3607 38.9373 27.2782 39.0161 27.2277C39.0276 27.2235 39.0358 27.2192 39.0485 27.2192C39.0604 27.2192 39.0681 27.2192 39.0765 27.2323C39.0889 27.2651 39.1011 27.3026 39.1135 27.3357L39.8664 29.5082C39.9152 29.6531 39.9646 29.8067 40.0181 29.932C40.0672 30.0485 40.153 30.0977 40.2879 30.0977H40.3131V30.2224C40.1286 30.2134 39.9443 30.2064 39.7478 30.2064C39.5477 30.2064 39.3428 30.2134 39.1338 30.2224V30.0977H39.1792C39.2728 30.0977 39.4332 30.0812 39.4332 29.9773C39.4332 29.9237 39.3965 29.8114 39.3508 29.678L39.1913 29.1964H38.2626L38.1311 29.6286ZM38.7292 27.7887H38.721L38.3395 28.9642H39.1059L38.7292 27.7887Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M40.3115 30.2407C40.127 30.2333 39.9433 30.224 39.7482 30.224C39.5485 30.224 39.3435 30.2333 39.135 30.2407L39.1169 30.2416V30.0802H39.1791C39.2742 30.0802 39.4152 30.0578 39.4153 29.9778C39.4164 29.9305 39.3794 29.8172 39.3334 29.6846L39.1782 29.2147H38.2749L38.1485 29.6345C38.1073 29.7768 38.059 29.8854 38.0595 29.9527C38.0595 30.0544 38.213 30.0802 38.3482 30.0802H38.4107V30.2416L38.3923 30.2407C38.2289 30.2333 38.0613 30.224 37.8984 30.224C37.7517 30.224 37.6049 30.2333 37.4573 30.2407L37.4395 30.2416V30.0802H37.4805C37.6331 30.0794 37.7607 29.9907 37.8206 29.8222L38.475 27.9159C38.5275 27.7621 38.6014 27.5556 38.6373 27.3902C38.7659 27.3449 38.9302 27.2616 39.0098 27.2114C39.0204 27.2073 39.0318 27.2023 39.0486 27.2023C39.0591 27.2014 39.0806 27.204 39.0932 27.2267C39.1056 27.2594 39.1176 27.2971 39.131 27.3306L39.8834 29.5027C39.9326 29.6482 39.9814 29.8016 40.0346 29.9255C40.0827 30.0354 40.1568 30.0791 40.2876 30.0802H40.3299V30.2416L40.3115 30.2407ZM39.1516 30.204C39.354 30.1964 39.5531 30.1884 39.7482 30.1884C39.9384 30.1884 40.1164 30.1964 40.295 30.2037L40.2945 30.1167H40.2877C40.1491 30.117 40.0526 30.0617 40.0015 29.9392C39.9476 29.8138 39.8989 29.6596 39.8498 29.5146L39.0969 27.3418C39.0849 27.3087 39.0718 27.2716 39.0622 27.2428C39.0588 27.2379 39.0591 27.2379 39.0567 27.2379H39.0486C39.0398 27.2379 39.0353 27.2407 39.0255 27.2436C38.9455 27.2944 38.7817 27.3777 38.6609 27.4093C38.6355 27.5666 38.5612 27.7738 38.5085 27.9281L37.8532 29.8339C37.7909 30.0144 37.648 30.117 37.4806 30.1167H37.4745V30.2037C37.6155 30.1964 37.7561 30.1884 37.8984 30.1884C38.0569 30.1884 38.2187 30.1964 38.3759 30.2037V30.1167H38.3482C38.2152 30.1148 38.0287 30.1005 38.0234 29.9526C38.0245 29.8705 38.0749 29.7646 38.115 29.6239L38.1314 29.6291L38.115 29.6234L38.2496 29.1796H39.2033L39.3677 29.6728C39.4127 29.8064 39.4501 29.9171 39.4501 29.9777C39.4442 30.1049 39.2721 30.1144 39.1792 30.1167H39.1516V30.204ZM38.3155 28.9823L38.7084 27.7707H38.7296V27.7891L38.725 27.7904L38.7296 27.7891V27.7707H38.742L39.1289 28.9823H38.3155ZM38.3649 28.9463H39.0808L38.7249 27.834L38.3649 28.9463ZM38.7127 27.7946L38.7207 27.7916L38.7127 27.7946Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M41.3443 29.8155C41.3443 29.9824 41.458 30.0316 41.5893 30.0489C41.7568 30.0614 41.9411 30.0614 42.1299 30.0401C42.3015 30.0192 42.4488 29.9202 42.5222 29.8155C42.5873 29.7243 42.6241 29.608 42.6492 29.5168H42.7676C42.7225 29.7535 42.6653 29.9864 42.6162 30.2228C42.2569 30.2228 41.8957 30.2068 41.5362 30.2068C41.1758 30.2068 40.816 30.2228 40.4561 30.2228V30.098H40.5127C40.6603 30.098 40.8205 30.0775 40.8205 29.82V27.6856C40.8205 27.4691 40.6603 27.4484 40.5127 27.4484H40.4561V27.3237C40.6726 27.3237 40.8857 27.3402 41.1022 27.3402C41.3113 27.3402 41.5155 27.3237 41.7247 27.3237V27.4484H41.6219C41.4664 27.4484 41.3443 27.4528 41.3443 27.6729V29.8155Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M42.6158 30.2406C42.2549 30.2406 41.8945 30.2239 41.5356 30.2239C41.1757 30.2239 40.8156 30.2406 40.4547 30.2406H40.4375V30.0801H40.5122C40.6601 30.0765 40.8006 30.0659 40.8023 29.8201V27.6849C40.8006 27.4827 40.6607 27.4692 40.5122 27.4665H40.4375V27.3057H40.4547C40.6726 27.3057 40.8856 27.3219 41.1013 27.3219C41.3097 27.3219 41.5135 27.3057 41.7241 27.3057H41.7405V27.4665H41.6213C41.4625 27.4706 41.364 27.4633 41.3605 27.6729V29.8155C41.3615 29.9708 41.4618 30.0123 41.5897 30.0309C41.6617 30.0359 41.7379 30.0386 41.8166 30.0386C41.9167 30.0386 42.0206 30.0339 42.1264 30.0225C42.293 30.0024 42.4364 29.9046 42.5072 29.8056C42.5704 29.7168 42.606 29.603 42.6308 29.5126L42.6342 29.4989H42.7885L42.7849 29.5207C42.7386 29.7581 42.6823 29.9898 42.6327 30.2266L42.6294 30.2406H42.6158ZM42.6009 30.2048C42.6483 29.9792 42.7017 29.7586 42.746 29.5346H42.6616C42.6365 29.625 42.6 29.7365 42.5358 29.8269C42.4593 29.9344 42.3078 30.0359 42.1303 30.0583C42.0233 30.0693 41.9169 30.0749 41.8167 30.0749C41.7374 30.0749 41.6609 30.0713 41.5864 30.066C41.4533 30.0512 41.3246 29.9932 41.3248 29.8155V27.6729C41.3248 27.442 41.4687 27.4301 41.6214 27.4301H41.7056V27.3417C41.5041 27.343 41.3051 27.3585 41.1014 27.3585C40.89 27.3585 40.6826 27.343 40.4732 27.3417V27.4301H40.5123C40.6582 27.4301 40.8367 27.456 40.8367 27.6849V29.8201C40.8367 30.0889 40.6589 30.1168 40.5123 30.1168H40.4732V30.2048C40.8267 30.204 41.1804 30.1884 41.5357 30.1884C41.8915 30.1884 42.2467 30.204 42.6009 30.2048Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M17.1602 12.6614C17.1602 8.77664 20.2628 5.62744 24.09 5.62744C27.9174 5.62744 31.0201 8.77664 31.0201 12.6614C31.0201 16.5462 27.9174 19.6956 24.09 19.6956C20.2628 19.6956 17.1602 16.5462 17.1602 12.6614Z",fill:"#FFFFFE"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M28.2807 12.5227C28.2779 10.7214 27.1686 9.18519 25.6055 8.5768V16.4683C27.1686 15.8592 28.2779 14.3243 28.2807 12.5227ZM22.6238 16.4669V8.57771C21.0621 9.18799 19.9545 10.722 19.9503 12.5229C19.9545 14.3232 21.0621 15.8571 22.6238 16.4669ZM24.1156 5.85259C20.4863 5.85401 17.546 8.83908 17.5454 12.5228C17.546 16.206 20.4863 19.1906 24.1156 19.1913C27.7449 19.1906 30.6858 16.206 30.6867 12.5228C30.6858 8.83908 27.7449 5.85401 24.1156 5.85259ZM24.0995 19.8207C20.1281 19.8399 16.8594 16.5742 16.8594 12.5989C16.8594 8.25425 20.1281 5.24921 24.0995 5.25H25.9606C29.8851 5.24921 33.4668 8.25284 33.4668 12.5989C33.4668 16.5728 29.8851 19.8207 25.9606 19.8207H24.0995Z",fill:"#0069AA"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M7.60352 30.098H7.66098C7.80812 30.098 7.96362 30.0775 7.96362 29.8614V27.6856C7.96362 27.4691 7.80812 27.4486 7.66098 27.4486H7.60352V27.3237C7.76305 27.3237 8.00854 27.3402 8.20958 27.3402C8.41403 27.3402 8.65921 27.3237 8.85158 27.3237V27.4486H8.79381C8.64729 27.4486 8.49147 27.4691 8.49147 27.6856V29.8614C8.49147 30.0775 8.64729 30.098 8.79381 30.098H8.85158V30.2228C8.65534 30.2228 8.40923 30.2068 8.2054 30.2068C8.00451 30.2068 7.76305 30.2228 7.60352 30.2228V30.098Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8.85181 30.2406C8.65387 30.2406 8.409 30.2239 8.20532 30.2239C8.00506 30.2239 7.76437 30.2406 7.60375 30.2406H7.58594V30.0801H7.6609C7.80944 30.0765 7.9445 30.0644 7.94574 29.8614V27.6856C7.9445 27.4824 7.80944 27.4692 7.6609 27.4665H7.58594V27.3057H7.60375C7.76437 27.3057 8.0097 27.3219 8.20981 27.3219C8.41333 27.3219 8.65836 27.3057 8.85181 27.3057H8.87009V27.4665H8.79482C8.64613 27.4692 8.51029 27.4824 8.50952 27.6856V29.8614C8.51029 30.0644 8.64613 30.0765 8.79482 30.0801H8.87009V30.2406H8.85181ZM8.83398 30.2051L8.83413 30.1168H8.79464C8.64827 30.1168 8.47433 30.0907 8.4734 29.8614V27.6857C8.47433 27.456 8.64827 27.4304 8.79464 27.4304H8.83398V27.3417C8.64517 27.3419 8.40804 27.3585 8.20963 27.3585C8.01525 27.3585 7.77998 27.343 7.62154 27.3417V27.4304H7.66072C7.80662 27.4304 7.98087 27.456 7.98087 27.6857V29.8614C7.98087 30.0907 7.80662 30.1168 7.66072 30.1168H7.62154V30.2051C7.77998 30.2037 8.01107 30.1884 8.20514 30.1884C8.40448 30.1884 8.64099 30.204 8.83398 30.2051Z",fill:"#1A1919"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M42.7169 27.2061C42.9638 27.2061 43.1479 27.3985 43.1479 27.6444C43.1479 27.8901 42.9638 28.0804 42.7169 28.0804C42.4706 28.0804 42.2861 27.8901 42.2861 27.6444C42.2861 27.3985 42.4706 27.2061 42.7169 27.2061ZM42.7168 27.9992C42.9101 27.9992 43.0577 27.8324 43.0577 27.6443C43.0577 27.456 42.9118 27.288 42.7168 27.288C42.5229 27.288 42.3754 27.456 42.3754 27.6443C42.3754 27.8324 42.5229 27.9992 42.7168 27.9992ZM42.5025 27.8751V27.8538C42.5551 27.8461 42.565 27.8475 42.565 27.8149V27.4907C42.565 27.4451 42.5606 27.4294 42.5042 27.4319V27.4096H42.7247C42.8005 27.4096 42.8703 27.4464 42.8703 27.5259C42.8703 27.5908 42.8282 27.6391 42.7684 27.6578L42.8393 27.7581C42.8723 27.8034 42.91 27.8461 42.9341 27.8612V27.8751H42.8503C42.8101 27.8751 42.7745 27.7892 42.6955 27.674H42.6478V27.8189C42.6478 27.8475 42.6577 27.8461 42.7106 27.8538V27.8751H42.5025ZM42.6481 27.6444H42.6989C42.7545 27.6444 42.7802 27.6018 42.7802 27.5328C42.7802 27.4633 42.7406 27.4386 42.6961 27.4386H42.6481V27.6444Z",fill:"#1A1919"})),s=()=>Object(l.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"0.878906",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M17.1461 17.7823C17.1461 19.9191 18.8322 21.576 21.0021 21.576C21.6154 21.576 22.141 21.456 22.7888 21.1524V19.4832C22.2192 20.0506 21.7145 20.2795 21.0685 20.2795C19.6332 20.2795 18.6146 19.2439 18.6146 17.7717C18.6146 16.376 19.6654 15.2751 21.0021 15.2751C21.6818 15.2751 22.1963 15.5163 22.7888 16.0929V14.4246C22.1633 14.1089 21.649 13.978 21.0357 13.978C18.8768 13.978 17.1461 15.6685 17.1461 17.7823ZM13.4892 16.0168C13.4892 16.4097 13.7401 16.6173 14.5953 16.9321C16.2163 17.5222 16.6967 18.0449 16.6967 19.2001C16.6967 20.6072 15.6577 21.5872 14.177 21.5872C13.0926 21.5872 12.304 21.1622 11.6475 20.2031L12.5682 19.3209C12.8962 19.9523 13.4437 20.2907 14.1234 20.2907C14.7593 20.2907 15.2298 19.8542 15.2298 19.2653C15.2298 18.96 15.0873 18.6979 14.8025 18.5128C14.6594 18.4252 14.3754 18.2949 13.8174 18.0988C12.479 17.6197 12.0201 17.1068 12.0201 16.1053C12.0201 14.9155 13.006 14.0224 14.2987 14.0224C15.0997 14.0224 15.8327 14.2948 16.4455 14.8282L15.6998 15.7997C15.3286 15.3857 14.9775 15.211 14.5507 15.211C13.9366 15.211 13.4892 15.559 13.4892 16.0168ZM9.68583 21.4123H11.1109V14.1424H9.68583V21.4123ZM6.77288 19.6035C6.32524 20.006 5.74353 20.1815 4.82283 20.1815H4.44039V15.374H4.82283C5.74353 15.374 6.30238 15.538 6.77288 15.9621C7.26569 16.3986 7.56205 17.0755 7.56205 17.7717C7.56205 18.4697 7.26569 19.1671 6.77288 19.6035ZM5.10834 14.1424H3.0166V21.4121H5.09733C6.20374 21.4121 7.0025 21.1525 7.70389 20.5728C8.53737 19.8867 9.03017 18.8523 9.03017 17.7824C9.03017 15.6369 7.41938 14.1424 5.10834 14.1424ZM32.1394 14.1424L34.0875 19.0255L36.061 14.1424H37.6057L34.4496 21.5988H33.6828L30.5826 14.1424H32.1394ZM38.2501 21.4122H42.2913V20.1815H39.6741V18.2191H42.1951V16.9878H39.6741V15.3742H42.2913V14.1424H38.2501V21.4122ZM44.6585 17.4893H45.0748C45.9851 17.4893 46.4674 17.0958 46.4674 16.365C46.4674 15.6575 45.9851 15.2876 45.0974 15.2876H44.6585V17.4893ZM45.3485 14.1422C46.9918 14.1422 47.9339 14.9275 47.9339 16.2886C47.9339 17.4016 47.3429 18.1325 46.2695 18.3496L48.5695 21.4121H46.817L44.8447 18.4917H44.6587V21.4121H43.2353V14.1422H45.3485Z",fill:"#1D1D1B"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M30.415 19.8859C31.5716 18.0862 31.0433 15.6953 29.235 14.5445C27.4267 13.3937 25.0236 13.9191 23.867 15.7188C22.7107 17.518 23.2391 19.9096 25.0474 21.0604C26.8557 22.2112 29.2587 21.6851 30.415 19.8859Z",fill:"url(#paint0_linear)"}),Object(l.createElement)("defs",null,Object(l.createElement)("linearGradient",{id:"paint0_linear",x1:"32.5088",y1:"16.6279",x2:"25.9795",y2:"12.4317",gradientUnits:"userSpaceOnUse"},Object(l.createElement)("stop",{stopColor:"#F6A000"}),Object(l.createElement)("stop",{offset:"0.623918",stopColor:"#E47E02"}),Object(l.createElement)("stop",{offset:"1",stopColor:"#D36002"})))),u=()=>Object(l.createElement)("svg",{width:"51",height:"35",viewBox:"0 0 51 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"0.878906",y:"0.5",width:"49",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M33.473 17.5973H33.494H33.5346H33.5752H33.6158H33.6564H33.697H33.7376H33.7782H33.8188H33.8594H33.9H33.9406H33.9812H34.0218H34.0624H34.103H34.1436H34.1842H34.2248H34.2654H34.306H34.3466H34.3872H34.4278H34.4684H34.509H34.5496H34.5902H34.6308H34.6714H34.712H34.7527H34.7932H34.8338H34.8745H34.915H34.9556H34.9963H35.0368H35.0774H35.1181H35.1586H35.1992H35.2399H35.2805H35.321H35.3617H35.4023H35.4428H35.4835H35.5241H35.5646H35.6053H35.6459H35.6864H35.7271H35.7677H35.8083H35.8489H35.8895H35.9233L35.9301 17.5975C35.9426 17.5978 35.9563 17.5984 35.9707 17.5991C35.9837 17.5999 35.9973 17.6008 36.0113 17.6018C36.0246 17.6028 36.0383 17.604 36.0519 17.6052C36.0656 17.6065 36.0792 17.6079 36.0925 17.6093C36.1064 17.6109 36.1201 17.6126 36.1331 17.6143C36.1476 17.6163 36.1613 17.6183 36.1737 17.6205C36.1826 17.622 36.1909 17.6235 36.1982 17.6251L36.2143 17.6288C36.2279 17.632 36.2414 17.6354 36.2549 17.6392C36.2685 17.643 36.282 17.647 36.2955 17.6513C36.3091 17.6557 36.3227 17.6603 36.3361 17.6653C36.3497 17.6702 36.3632 17.6755 36.3767 17.681C36.3903 17.6866 36.4039 17.6926 36.4173 17.6987C36.431 17.705 36.4445 17.7116 36.4579 17.7185C36.4716 17.7254 36.4851 17.7328 36.4985 17.7403C36.5122 17.7481 36.5257 17.7562 36.5391 17.7646C36.5528 17.7732 36.5664 17.7821 36.5797 17.7913C36.5934 17.8008 36.607 17.8105 36.6203 17.8206C36.634 17.8311 36.6476 17.8419 36.6609 17.8531C36.6747 17.8646 36.6882 17.8766 36.7015 17.8889C36.7154 17.9017 36.7289 17.9148 36.7421 17.9284C36.7561 17.9426 36.7695 17.9574 36.7827 17.9725C36.7967 17.9885 36.8102 18.005 36.8233 18.0219C36.8374 18.0401 36.8509 18.0587 36.8639 18.0779C36.8781 18.0989 36.8917 18.1205 36.9045 18.1426C36.919 18.1675 36.9325 18.1932 36.9451 18.2196C36.96 18.2508 36.9736 18.283 36.9857 18.3161C37.002 18.3606 37.0156 18.4067 37.0263 18.4544C37.0449 18.5369 37.0549 18.6238 37.0549 18.7145C37.0549 18.8057 37.0449 18.893 37.0263 18.9758C37.0156 19.0235 37.002 19.0698 36.9857 19.1143C36.9736 19.1476 36.96 19.1798 36.9451 19.2111C36.9325 19.2375 36.919 19.2632 36.9045 19.2882C36.8917 19.3103 36.8782 19.3319 36.8639 19.3529C36.8509 19.3721 36.8374 19.3908 36.8233 19.409C36.8102 19.4259 36.7967 19.4423 36.7827 19.4584C36.7695 19.4734 36.756 19.4881 36.7421 19.5024C36.7289 19.516 36.7153 19.5291 36.7015 19.5419C36.6882 19.5541 36.6747 19.566 36.6609 19.5775C36.6476 19.5886 36.6341 19.5995 36.6203 19.6099C36.607 19.62 36.5934 19.6297 36.5797 19.6391C36.5664 19.6483 36.5528 19.6572 36.5391 19.6657C36.5257 19.674 36.5122 19.6821 36.4985 19.6898C36.4851 19.6974 36.4716 19.7046 36.4579 19.7116C36.4445 19.7184 36.431 19.7249 36.4173 19.7312C36.4039 19.7373 36.3903 19.7432 36.3767 19.7488C36.3632 19.7542 36.3497 19.7595 36.3361 19.7644C36.3227 19.7692 36.3091 19.7738 36.2955 19.7781C36.282 19.7824 36.2685 19.7863 36.2549 19.7901C36.2414 19.7937 36.2279 19.7971 36.2143 19.8003L36.1982 19.8039C36.1909 19.8056 36.1826 19.8073 36.1737 19.8089C36.1613 19.8111 36.1476 19.8132 36.1331 19.8152C36.1201 19.8169 36.1064 19.8186 36.0925 19.8202C36.0792 19.8217 36.0656 19.823 36.0519 19.8243C36.0383 19.8255 36.0246 19.8266 36.0113 19.8276C35.9973 19.8286 35.9837 19.8295 35.9707 19.8301C35.9563 19.8309 35.9426 19.8314 35.9301 19.8317L35.9083 19.832H35.8895H35.8489H35.8083H35.7677H35.7271H35.6864H35.6459H35.6053H35.5646H35.5241H35.4835H35.4428H35.4023H35.3617H35.321H35.2805H35.2399H35.1992H35.1586H35.1181H35.0774H35.0368H34.9963H34.9556H34.915H34.8745H34.8338H34.7932H34.7527H34.712H34.6714H34.6308H34.5902H34.5496H34.509H34.4684H34.4278H34.3872H34.3466H34.306H34.2654H34.2248H34.1842H34.1436H34.103H34.0624H34.0218H33.9812H33.9406H33.9H33.8594H33.8188H33.7782H33.7376H33.697H33.6564H33.6158H33.5752H33.5346H33.494H33.473V17.5973ZM36.7423 15.0681C36.7586 15.1431 36.7673 15.2226 36.7673 15.3062C36.7673 15.3898 36.7586 15.4691 36.7423 15.5441C36.7318 15.5923 36.7181 15.6385 36.7016 15.6829C36.6896 15.7153 36.676 15.7467 36.661 15.7769C36.6485 15.8023 36.6349 15.8269 36.6204 15.8508C36.6076 15.8718 36.594 15.8923 36.5798 15.9122C36.5668 15.9303 36.5533 15.9478 36.5392 15.9649C36.5261 15.9807 36.5126 15.9961 36.4986 16.011C36.4855 16.0251 36.472 16.0387 36.4581 16.0518C36.4448 16.0644 36.4313 16.0765 36.4174 16.0883C36.4042 16.0996 36.3906 16.1104 36.3768 16.121C36.3636 16.1311 36.35 16.1409 36.3363 16.1504C36.3229 16.1596 36.3094 16.1684 36.2956 16.1769C36.2823 16.1852 36.2687 16.1931 36.255 16.2007C36.2417 16.2082 36.2281 16.2154 36.2145 16.2222C36.2011 16.2289 36.1875 16.2353 36.1738 16.2415C36.1604 16.2475 36.1469 16.2532 36.1332 16.2586C36.1198 16.2639 36.1063 16.269 36.0926 16.2737C36.0792 16.2784 36.0657 16.2829 36.0521 16.287C36.0386 16.2911 36.0251 16.295 36.0114 16.2985C35.998 16.3021 35.9845 16.3053 35.9708 16.3083C35.9574 16.3113 35.9439 16.3139 35.9303 16.3164L35.9204 16.3182C35.9125 16.3195 35.902 16.3211 35.8896 16.3227C35.8777 16.3242 35.8639 16.3258 35.849 16.3273C35.8362 16.3286 35.8225 16.3299 35.8085 16.3311C35.7952 16.3322 35.7815 16.3332 35.7678 16.334C35.7542 16.3349 35.7405 16.3357 35.7272 16.3362C35.7132 16.3368 35.6995 16.3372 35.6867 16.3372L35.6802 16.3373H35.6461H35.6054H35.5648H35.5243H35.4836H35.443H35.4025H35.3619H35.3212H35.2807H35.2401H35.1994H35.1589H35.1183H35.0776H35.037H34.9965H34.9559H34.9152H34.8747H34.8341H34.7934H34.7529H34.7123H34.6716H34.6311H34.5905H34.5499H34.5092H34.4687H34.4281H34.3874H34.3469H34.3063H34.2657H34.2251H34.1845H34.1439H34.1033H34.0627H34.0221H33.9814H33.9409H33.9003H33.8597H33.8191H33.7785H33.7379H33.6973H33.6567H33.6161H33.5754H33.5349H33.4943H33.473V14.2751H33.4943H33.5349H33.5754H33.6161H33.6567H33.6973H33.7379H33.7785H33.8191H33.8597H33.9003H33.9409H33.9814H34.0221H34.0627H34.1033H34.1439H34.1845H34.2251H34.2657H34.3063H34.3469H34.3874H34.4281H34.4687H34.5092H34.5499H34.5905H34.6311H34.6716H34.7123H34.7529H34.7934H34.8341H34.8747H34.9152H34.9559H34.9965H35.037H35.0776H35.1183H35.1589H35.1994H35.2401H35.2807H35.3212H35.3619H35.4025H35.443H35.4836H35.5243H35.5648H35.6054H35.6461H35.6867H35.7037L35.7272 14.2759C35.7405 14.2764 35.7542 14.2772 35.7678 14.2781C35.7815 14.279 35.7952 14.28 35.8085 14.2811C35.8225 14.2822 35.8362 14.2835 35.849 14.2848C35.8639 14.2862 35.8777 14.2877 35.8896 14.2891C35.902 14.2905 35.9125 14.2919 35.9204 14.293L35.9303 14.2948C35.9439 14.2972 35.9574 14.3 35.9708 14.303C35.9845 14.306 35.998 14.3093 36.0114 14.3129C36.0251 14.3165 36.0386 14.3203 36.0521 14.3245C36.0657 14.3287 36.0792 14.3331 36.0926 14.3378C36.1063 14.3427 36.1198 14.3477 36.1332 14.3531C36.1469 14.3585 36.1604 14.3643 36.1738 14.3703C36.1875 14.3765 36.2011 14.3829 36.2145 14.3896C36.2281 14.3965 36.2417 14.4036 36.255 14.4111C36.2687 14.4187 36.2823 14.4267 36.2956 14.435C36.3094 14.4435 36.3229 14.4523 36.3363 14.4615C36.35 14.4709 36.3636 14.4808 36.3768 14.491C36.3906 14.5015 36.4042 14.5124 36.4174 14.5237C36.4313 14.5355 36.4448 14.5476 36.4581 14.5602C36.472 14.5733 36.4855 14.5869 36.4986 14.601C36.5126 14.6159 36.5261 14.6313 36.5392 14.6471C36.5533 14.6642 36.5668 14.6818 36.5798 14.6999C36.594 14.7197 36.6076 14.7402 36.6204 14.7613C36.6349 14.7851 36.6485 14.8097 36.661 14.8351C36.676 14.8654 36.6896 14.8968 36.7016 14.9292C36.7181 14.9736 36.7318 15.0199 36.7423 15.0681ZM41.7774 4.375H41.7941L41.794 25.5542C41.794 25.6807 41.7882 25.8059 41.7774 25.9296C41.7679 26.0385 41.7543 26.1462 41.7368 26.2526C41.725 26.324 41.7115 26.3948 41.6962 26.465C41.6838 26.5221 41.6703 26.5789 41.6556 26.6351C41.6429 26.6836 41.6293 26.7318 41.615 26.7796C41.602 26.8228 41.5886 26.8658 41.5744 26.9085C41.5614 26.9472 41.5478 26.9855 41.5338 27.0237C41.5207 27.0593 41.5071 27.0946 41.4932 27.1297C41.48 27.1627 41.4665 27.1954 41.4526 27.228C41.4394 27.2588 41.4258 27.2895 41.4119 27.3199C41.3987 27.3489 41.3852 27.3778 41.3714 27.4064C41.3581 27.4339 41.3446 27.4612 41.3307 27.4883C41.3175 27.5143 41.3039 27.5402 41.2901 27.5659C41.2769 27.5906 41.2632 27.615 41.2495 27.6394C41.2362 27.663 41.2227 27.6865 41.2089 27.7098C41.1956 27.7325 41.182 27.755 41.1683 27.7775C41.1549 27.7993 41.1414 27.8211 41.1277 27.8427C41.1144 27.8635 41.1007 27.8842 41.0871 27.9049C41.0737 27.9249 41.0601 27.9448 41.0465 27.9647C41.0331 27.9841 41.0195 28.0036 41.0058 28.0228C40.9924 28.0416 40.979 28.0603 40.9653 28.0789C40.9519 28.097 40.9383 28.1148 40.9246 28.1327C40.9112 28.1503 40.8977 28.1678 40.884 28.1852C40.8706 28.2022 40.8571 28.2191 40.8434 28.2359C40.83 28.2523 40.8164 28.2686 40.8028 28.2849C40.7893 28.3009 40.7759 28.3171 40.7622 28.3329C40.7488 28.3484 40.7352 28.3637 40.7216 28.379C40.7082 28.3941 40.6946 28.4091 40.681 28.424C40.6675 28.4387 40.6541 28.4534 40.6404 28.4679C40.627 28.4821 40.6134 28.4961 40.5998 28.5102C40.5863 28.5241 40.5729 28.5382 40.5592 28.5519C40.5458 28.5654 40.5321 28.5786 40.5186 28.5919C40.5051 28.605 40.4916 28.6182 40.4779 28.6313C40.4645 28.644 40.451 28.6567 40.4374 28.6693C40.4239 28.6818 40.4103 28.6942 40.3967 28.7065C40.3833 28.7187 40.3698 28.7308 40.3561 28.7428C40.3427 28.7546 40.3291 28.7664 40.3155 28.7781C40.302 28.7896 40.2886 28.8012 40.2749 28.8126C40.2615 28.8239 40.2479 28.8349 40.2343 28.846C40.2208 28.857 40.2073 28.8681 40.1937 28.8789C40.1803 28.8896 40.1667 28.9001 40.1531 28.9106C40.1396 28.9211 40.1261 28.9316 40.1125 28.942C40.0991 28.9521 40.0854 28.9621 40.0719 28.9721C40.0583 28.9821 40.0449 28.9921 40.0313 29.002C40.0178 29.0117 40.0042 29.0211 39.9907 29.0306C39.9771 29.0401 39.9637 29.0496 39.95 29.059C39.9366 29.0682 39.923 29.0772 39.9095 29.0862C39.896 29.0953 39.8825 29.1043 39.8688 29.1132C39.8554 29.122 39.8418 29.1306 39.8282 29.1392C39.8147 29.1477 39.8012 29.1563 39.7876 29.1647C39.7742 29.1731 39.7606 29.1813 39.747 29.1895L39.7064 29.2137C39.6929 29.2216 39.6794 29.2295 39.6658 29.2373C39.6524 29.2451 39.6388 29.2526 39.6252 29.2602C39.6117 29.2678 39.5982 29.2754 39.5846 29.2828C39.5711 29.2901 39.5575 29.2972 39.544 29.3044C39.5304 29.3115 39.517 29.3189 39.5034 29.326C39.4899 29.3329 39.4763 29.3395 39.4628 29.3463C39.4492 29.3531 39.4358 29.36 39.4221 29.3666C39.4087 29.3732 39.3951 29.3796 39.3816 29.3861L39.3409 29.4052L39.3003 29.4237C39.2868 29.4298 39.2733 29.4357 39.2597 29.4416C39.2462 29.4475 39.2327 29.4536 39.2191 29.4593C39.2057 29.4651 39.1921 29.4704 39.1785 29.476C39.165 29.4816 39.1515 29.4873 39.1379 29.4927C39.1244 29.4981 39.1109 29.5032 39.0973 29.5085C39.0838 29.5137 39.0703 29.5189 39.0567 29.524L39.0161 29.5391C39.0026 29.544 38.989 29.5487 38.9755 29.5535C38.9619 29.5583 38.9485 29.5632 38.9349 29.5679C38.9214 29.5724 38.9078 29.5767 38.8942 29.5811L38.8537 29.5944C38.8402 29.5987 38.8266 29.6028 38.813 29.607L38.7724 29.6192C38.7589 29.6232 38.7454 29.6273 38.7318 29.6311C38.7184 29.635 38.7048 29.6385 38.6912 29.6422C38.6777 29.6459 38.6642 29.6497 38.6506 29.6533L38.61 29.6636L38.5694 29.6735L38.5288 29.6833C38.5153 29.6864 38.5017 29.6892 38.4882 29.6922C38.4746 29.6952 38.4611 29.6983 38.4476 29.7011L38.407 29.7093L38.3663 29.7171C38.3528 29.7196 38.3393 29.7224 38.3257 29.7248C38.3123 29.7272 38.2987 29.7293 38.2851 29.7316L38.2445 29.7383L38.2039 29.7445L38.1633 29.7502C38.1498 29.7521 38.1363 29.7542 38.1227 29.7559C38.1092 29.7577 38.0956 29.759 38.0821 29.7606C38.0686 29.7622 38.0551 29.7638 38.0415 29.7653C38.028 29.7667 38.0145 29.7682 38.0009 29.7695C37.9874 29.7709 37.9738 29.772 37.9603 29.7732C37.9467 29.7744 37.9332 29.7757 37.9197 29.7768C37.9062 29.7778 37.8926 29.7787 37.8791 29.7796L37.8384 29.7822C37.8249 29.783 37.8114 29.784 37.7978 29.7847C37.7844 29.7854 37.7708 29.7857 37.7572 29.7863L37.7166 29.7878L37.676 29.7889C37.6625 29.7892 37.649 29.7892 37.6354 29.7894L37.5948 29.7899L37.5864 29.79H37.5542H37.5136H37.473H37.4324H37.3917H37.3512H37.3105H37.2699H37.2293H37.1887H37.1481H37.1075H37.0669H37.0263H36.9857H36.9451H36.9045H36.8638H36.8233H36.7826H36.742H36.7014H36.6608H36.6202H36.5796H36.539H36.4984H36.4578H36.4172H36.3766H36.3359H36.2954H36.2547H36.2141H36.1735H36.1329H36.0923H36.0517H36.0111H35.9705H35.9299H35.8893H35.8487H35.808H35.7675H35.7268H35.6862H35.6456H35.605H35.5644H35.5238H35.4832H35.4426H35.402H35.3614H35.3208H35.2801H35.2396H35.1989H35.1583H35.1177H35.0771H35.0365H34.9959H34.9553H34.9147H34.8741H34.8335H34.7929H34.7522H34.7117H34.671H34.6304H34.5898H34.5492H34.5086H34.468H34.4274H34.3868H34.3462H34.3056H34.265H34.2243H34.1838H34.1431H34.1025H34.0619H34.0213H33.9807H33.9401H33.8995H33.8589H33.8183H33.7777H33.7371H33.6964H33.6559H33.6152H33.5746H33.534H33.4934H33.4528H33.4122H33.3716H33.331H33.2904H33.2498H33.2092H33.1685H33.128H33.0873H33.0467H33.0061H32.9655H32.9249H32.8843H32.8437H32.8031H32.7625H32.7219H32.6813H32.6406H32.6H32.5594H32.5188H32.4782H32.4376H32.397H32.3564H32.3158H32.2752H32.2346H32.194H32.1534H32.1127H32.0721H32.0315H31.9909H31.9503H31.9097H31.8691H31.8285H31.7879H31.7473H31.7067H31.666H31.6255H31.5848H31.5442H31.5036H31.416V21.1545H31.5036H31.5442H31.5848H31.6255H31.666H31.7067H31.7473H31.7879H31.8285H31.8691H31.9097H31.9503H31.9909H32.0315H32.0721H32.1127H32.1534H32.194H32.2346H32.2752H32.3158H32.3564H32.397H32.4376H32.4782H32.5188H32.5594H32.6H32.6406H32.6813H32.7219H32.7625H32.8031H32.8437H32.8843H32.9249H32.9655H33.0061H33.0467H33.0873H33.128H33.1685H33.2092H33.2498H33.2904H33.331H33.3716H33.4122H33.4528H33.4934H33.534H33.5746H33.6152H33.6559H33.6964H33.7371H33.7777H33.8183H33.8589H33.8995H33.9401H33.9807H34.0213H34.0619H34.1025H34.1431H34.1838H34.2243H34.265H34.3056H34.3462H34.3868H34.4274H34.468H34.5086H34.5492H34.5898H34.6304H34.671H34.7117H34.7522H34.7929H34.8335H34.8741H34.9147H34.9553H34.9959H35.0365H35.0771H35.1177H35.1583H35.1989H35.2396H35.2801H35.3208H35.3614H35.402H35.4426H35.4832H35.5238H35.5644H35.605H35.6456H35.6862H35.7268H35.7675H35.808H35.8487H35.8893H35.9299H35.9705H36.0111H36.0517H36.0923H36.1329H36.1735H36.2141H36.2547H36.2954H36.3359H36.3766H36.4172H36.4578H36.4984H36.539H36.5796H36.6202H36.6608H36.7014H36.742H36.7826H36.8233H36.8638H36.9045H36.9451H36.9857H37.0263H37.0669H37.1075H37.1481H37.1887H37.2293H37.2699H37.3105H37.3512H37.3917H37.4324H37.473H37.5136H37.5542H37.5948H37.6354H37.676H37.7166H37.7572H37.7978H37.8384H37.8791H37.9197H37.9412L37.9603 21.1543L38.0009 21.154L38.0415 21.1532C38.0551 21.1528 38.0686 21.1526 38.0821 21.1521C38.0957 21.1517 38.1092 21.151 38.1227 21.1504L38.1633 21.1485L38.2039 21.146L38.2445 21.1432L38.2851 21.14L38.3257 21.1363C38.3393 21.135 38.3529 21.1337 38.3663 21.1323C38.3799 21.1309 38.3934 21.1293 38.407 21.1277L38.4476 21.1227C38.4611 21.1209 38.4747 21.1192 38.4882 21.1173C38.5018 21.1154 38.5153 21.1133 38.5288 21.1113C38.5424 21.1092 38.5559 21.1072 38.5694 21.105L38.61 21.0981L38.6506 21.0908C38.6642 21.0882 38.6778 21.0858 38.6912 21.0831C38.7048 21.0804 38.7183 21.0775 38.7318 21.0746L38.7724 21.0658C38.786 21.0627 38.7996 21.0597 38.813 21.0565C38.8267 21.0533 38.8402 21.0498 38.8537 21.0465C38.8672 21.0431 38.8808 21.0396 38.8942 21.0361C38.9078 21.0325 38.9214 21.0288 38.9349 21.0251C38.9485 21.0213 38.962 21.0174 38.9755 21.0134C38.9891 21.0095 39.0026 21.0054 39.0161 21.0013L39.0567 20.9885C39.0703 20.9841 39.0838 20.9796 39.0973 20.975C39.1109 20.9704 39.1245 20.9658 39.1379 20.961C39.1515 20.9562 39.1651 20.9514 39.1785 20.9464C39.1922 20.9413 39.2056 20.9361 39.2191 20.9308C39.2327 20.9256 39.2463 20.9202 39.2597 20.9147C39.2733 20.9092 39.2869 20.9037 39.3003 20.898C39.314 20.8923 39.3275 20.8864 39.3409 20.8804C39.3546 20.8744 39.3681 20.8682 39.3816 20.862C39.3951 20.8557 39.4087 20.8493 39.4221 20.8429C39.4357 20.8363 39.4493 20.8297 39.4628 20.823C39.4764 20.8161 39.4899 20.8091 39.5034 20.802C39.517 20.7949 39.5305 20.7875 39.544 20.7801C39.5576 20.7727 39.5712 20.7651 39.5846 20.7574C39.5982 20.7496 39.6118 20.7417 39.6252 20.7337C39.6388 20.7256 39.6524 20.7174 39.6658 20.709C39.6795 20.7004 39.693 20.6917 39.7064 20.683C39.7201 20.674 39.7336 20.6649 39.747 20.6558C39.7607 20.6464 39.7742 20.6369 39.7876 20.6274C39.8013 20.6176 39.8148 20.6077 39.8282 20.5976C39.8419 20.5874 39.8555 20.577 39.8688 20.5665C39.8825 20.5558 39.8961 20.5449 39.9095 20.5339C39.9232 20.5226 39.9367 20.5112 39.95 20.4996C39.9638 20.4878 39.9773 20.4757 39.9907 20.4636C40.0044 20.451 40.0179 20.4383 40.0313 20.4255C40.045 20.4123 40.0586 20.3989 40.0719 20.3854C40.0857 20.3714 40.0992 20.3572 40.1125 20.3428C40.1263 20.328 40.1398 20.3129 40.1531 20.2977C40.1669 20.2819 40.1805 20.2658 40.1937 20.2496C40.2075 20.2327 40.2211 20.2156 40.2343 20.1983C40.2481 20.1801 40.2618 20.1618 40.2749 20.1432C40.2889 20.1235 40.3023 20.1034 40.3155 20.0831C40.3295 20.0616 40.343 20.0399 40.3561 20.0179C40.3702 19.9942 40.3837 19.9701 40.3967 19.9458C40.4109 19.9193 40.4245 19.8926 40.4374 19.8654C40.4517 19.835 40.4652 19.8041 40.4779 19.7728C40.4926 19.7368 40.5061 19.7004 40.5186 19.6634C40.5338 19.6177 40.5473 19.5711 40.5592 19.5237C40.5765 19.4544 40.5901 19.3833 40.5998 19.3104C40.6116 19.222 40.6178 19.1312 40.6178 19.0378C40.6178 18.9469 40.6116 18.8586 40.5998 18.7728C40.5901 18.7024 40.5765 18.6337 40.5592 18.5668C40.5473 18.5213 40.5338 18.4766 40.5186 18.4327C40.5061 18.3968 40.4926 18.3613 40.4779 18.3266C40.4653 18.2964 40.4517 18.2667 40.4374 18.2375C40.4245 18.2112 40.4109 18.1853 40.3967 18.1598C40.3837 18.1363 40.3702 18.113 40.3561 18.0901C40.3431 18.0689 40.3295 18.0479 40.3155 18.0273C40.3024 18.0078 40.2889 17.9884 40.2749 17.9694C40.2618 17.9514 40.2482 17.9337 40.2343 17.9163C40.2211 17.8995 40.2075 17.883 40.1937 17.8667C40.1804 17.851 40.1669 17.8355 40.1531 17.8202C40.1398 17.8055 40.1263 17.791 40.1125 17.7767C40.0992 17.7628 40.0856 17.7492 40.0719 17.7357C40.0586 17.7226 40.045 17.7097 40.0313 17.697C40.0179 17.6846 40.0044 17.6724 39.9907 17.6604C39.9773 17.6486 39.9638 17.637 39.95 17.6255C39.9367 17.6143 39.9232 17.6033 39.9095 17.5924C39.8961 17.5817 39.8825 17.5712 39.8688 17.5608C39.8554 17.5507 39.8419 17.5407 39.8282 17.5307C39.8148 17.5211 39.8013 17.5115 39.7876 17.502C39.7742 17.4928 39.7607 17.4836 39.747 17.4745C39.7336 17.4657 39.7201 17.4569 39.7064 17.4482C39.693 17.4398 39.6795 17.4314 39.6658 17.4231C39.6524 17.415 39.6389 17.407 39.6252 17.3991C39.6118 17.3913 39.5982 17.3837 39.5846 17.3761C39.5712 17.3687 39.5576 17.3614 39.544 17.3541C39.5305 17.347 39.517 17.3399 39.5034 17.333C39.4899 17.3261 39.4764 17.3193 39.4628 17.3126C39.4493 17.3061 39.4358 17.2995 39.4221 17.2931C39.4087 17.2868 39.3951 17.2807 39.3816 17.2746C39.3681 17.2685 39.3546 17.2626 39.3409 17.2567C39.3275 17.251 39.3139 17.2453 39.3003 17.2396C39.2868 17.2341 39.2734 17.2285 39.2597 17.2231C39.2463 17.2178 39.2327 17.2126 39.2191 17.2074C39.2057 17.2023 39.1921 17.1974 39.1785 17.1925C39.165 17.1876 39.1515 17.1828 39.1379 17.1781C39.1244 17.1734 39.1109 17.1688 39.0973 17.1642C39.0838 17.1598 39.0703 17.1554 39.0567 17.1511C39.0432 17.1468 39.0296 17.1427 39.0161 17.1386L38.9755 17.1266C38.962 17.1227 38.9485 17.1188 38.9349 17.1151C38.9214 17.1114 38.9078 17.1078 38.8942 17.1043C38.8808 17.1008 38.8672 17.0974 38.8537 17.094C38.8402 17.0906 38.8266 17.0873 38.813 17.0841C38.7996 17.0809 38.786 17.0779 38.7724 17.0748C38.759 17.0718 38.7454 17.069 38.7318 17.0661C38.7183 17.0633 38.7048 17.0604 38.6912 17.0577L38.6506 17.0499L38.61 17.0426C38.5965 17.0403 38.583 17.0379 38.5694 17.0357L38.5288 17.0292C38.5153 17.0272 38.5017 17.0252 38.4882 17.0233L38.4476 17.0178L38.407 17.0126L38.3663 17.008L38.3257 17.0037L38.2851 16.9999L38.2445 16.9965L38.207 16.9936L38.207 16.9446L38.2445 16.9387L38.2851 16.932C38.2987 16.9296 38.3123 16.9273 38.3257 16.9248L38.3663 16.9169C38.38 16.9142 38.3935 16.9114 38.407 16.9085L38.4476 16.8996C38.4611 16.8965 38.4747 16.8934 38.4882 16.8902C38.5018 16.8869 38.5153 16.8834 38.5288 16.8799C38.5424 16.8764 38.5559 16.8728 38.5694 16.8691C38.583 16.8654 38.5965 16.8616 38.61 16.8577C38.6236 16.8538 38.6372 16.8498 38.6506 16.8457C38.6642 16.8416 38.6778 16.8373 38.6912 16.833C38.7049 16.8286 38.7184 16.824 38.7318 16.8195C38.7455 16.8148 38.759 16.81 38.7724 16.8052C38.7861 16.8003 38.7996 16.7954 38.813 16.7903C38.8267 16.7852 38.8402 16.7799 38.8537 16.7746C38.8673 16.7692 38.8808 16.7637 38.8942 16.7581C38.9079 16.7524 38.9214 16.7467 38.9349 16.7408C38.9485 16.7348 38.962 16.7287 38.9755 16.7225C38.9891 16.7162 39.0026 16.7098 39.0161 16.7033C39.0297 16.6967 39.0433 16.6899 39.0567 16.6831C39.0703 16.6761 39.0839 16.6691 39.0973 16.662C39.111 16.6547 39.1245 16.6472 39.1379 16.6398C39.1516 16.6321 39.1651 16.6243 39.1785 16.6165C39.1922 16.6084 39.2057 16.6003 39.2191 16.592C39.2328 16.5836 39.2463 16.575 39.2597 16.5664C39.2734 16.5575 39.2869 16.5485 39.3003 16.5394C39.314 16.53 39.3276 16.5206 39.3409 16.511C39.3547 16.5012 39.3682 16.4912 39.3816 16.4812C39.3953 16.4708 39.4088 16.4604 39.4221 16.4498C39.4359 16.4389 39.4494 16.4279 39.4628 16.4167C39.4765 16.4052 39.49 16.3935 39.5034 16.3817C39.5172 16.3695 39.5307 16.3571 39.544 16.3446C39.5577 16.3317 39.5713 16.3186 39.5846 16.3054C39.5984 16.2917 39.6119 16.2779 39.6252 16.2639C39.639 16.2493 39.6526 16.2345 39.6658 16.2195C39.6796 16.2039 39.6932 16.1881 39.7064 16.1721C39.7203 16.1554 39.7338 16.1385 39.747 16.1214C39.7609 16.1033 39.7745 16.085 39.7876 16.0665C39.8016 16.0469 39.8151 16.027 39.8282 16.007C39.8422 15.9856 39.8558 15.9639 39.8688 15.942C39.8829 15.9183 39.8965 15.8942 39.9095 15.8699C39.9237 15.8432 39.9372 15.8162 39.95 15.7888C39.9645 15.7581 39.978 15.7268 39.9907 15.6953C40.0054 15.6586 40.0189 15.6214 40.0313 15.5836C40.0467 15.5362 40.0603 15.488 40.0719 15.4393C40.0906 15.3601 40.1042 15.2793 40.1125 15.1971C40.1191 15.1322 40.1225 15.0665 40.1225 15.0001C40.1225 14.93 40.1191 14.8616 40.1125 14.795C40.1041 14.7098 40.0905 14.6275 40.0719 14.5481C40.0603 14.4985 40.0467 14.45 40.0313 14.4027C40.0189 14.3648 40.0054 14.3277 39.9907 14.2913C39.978 14.26 39.9644 14.2292 39.95 14.199C39.9372 14.172 39.9236 14.1455 39.9095 14.1194C39.8965 14.0955 39.8829 14.072 39.8688 14.0488C39.8557 14.0273 39.8422 14.0061 39.8282 13.9852C39.8151 13.9656 39.8015 13.9464 39.7876 13.9274C39.7745 13.9095 39.7609 13.8917 39.747 13.8743C39.7338 13.8576 39.7203 13.8411 39.7064 13.8249C39.6932 13.8095 39.6796 13.7943 39.6658 13.7793C39.6526 13.7649 39.639 13.7506 39.6252 13.7366C39.6119 13.723 39.5984 13.7097 39.5846 13.6965C39.5713 13.6837 39.5577 13.6711 39.544 13.6587C39.5307 13.6466 39.5171 13.6347 39.5034 13.623C39.49 13.6116 39.4765 13.6004 39.4628 13.5893C39.4494 13.5785 39.4359 13.5678 39.4221 13.5574C39.4088 13.5471 39.3952 13.5371 39.3816 13.5272C39.3682 13.5174 39.3546 13.5079 39.3409 13.4984C39.3275 13.4892 39.314 13.4801 39.3003 13.4711C39.2869 13.4623 39.2734 13.4537 39.2597 13.4451C39.2463 13.4367 39.2328 13.4285 39.2191 13.4204C39.2057 13.4124 39.1922 13.4045 39.1785 13.3968C39.1651 13.3891 39.1516 13.3816 39.1379 13.3742C39.1245 13.3669 39.111 13.3598 39.0973 13.3527C39.0839 13.3458 39.0703 13.339 39.0567 13.3322C39.0432 13.3256 39.0297 13.319 39.0161 13.3126C39.0026 13.3062 38.9891 13.2999 38.9755 13.2938C38.962 13.2878 38.9485 13.282 38.9349 13.2761C38.9214 13.2704 38.9078 13.2647 38.8942 13.2591C38.8808 13.2536 38.8672 13.2482 38.8537 13.2429C38.8402 13.2376 38.8267 13.2324 38.813 13.2273C38.7996 13.2223 38.7861 13.2174 38.7724 13.2126C38.759 13.2078 38.7454 13.2031 38.7318 13.1985C38.7184 13.194 38.7048 13.1895 38.6912 13.1852C38.6778 13.1808 38.6642 13.1766 38.6506 13.1724C38.6371 13.1683 38.6236 13.1641 38.61 13.1602C38.5965 13.1563 38.583 13.1525 38.5694 13.1488C38.5559 13.1451 38.5424 13.1415 38.5288 13.1379L38.4882 13.1275C38.4747 13.1242 38.4612 13.1209 38.4476 13.1177C38.4341 13.1145 38.4206 13.1115 38.407 13.1085L38.3663 13.0998C38.3528 13.097 38.3394 13.0941 38.3257 13.0915C38.3123 13.0888 38.2987 13.0864 38.2851 13.0839L38.2445 13.0767C38.231 13.0744 38.2175 13.0721 38.2039 13.0699L38.1633 13.0637L38.1227 13.0579L38.0821 13.0525L38.0415 13.0477L38.0009 13.0433L37.9603 13.0392L37.9197 13.0356L37.8791 13.0324L37.8384 13.0295L37.7978 13.0272L37.7817 13.0263L37.7572 13.0247L37.7166 13.0223L37.676 13.0201L37.6354 13.0179L37.5948 13.016L37.5542 13.0142L37.5136 13.0127L37.473 13.0114L37.4324 13.0105L37.3917 13.01L37.3694 13.0099H37.3512H37.3105H37.2699H37.2293H37.1887H37.1481H37.1075H37.0669H37.0263H36.9857H36.9451H36.9045H36.8638H36.8233H36.7826H36.742H36.7014H36.6608H36.6202H36.5796H36.539H36.4984H36.4578H36.4172H36.3766H36.3359H36.2954H36.2547H36.2141H36.1735H36.1329H36.0923H36.0517H36.0111H35.9705H35.9299H35.8893H35.8487H35.808H35.7675H35.7268H35.6862H35.6456H35.605H35.5644H35.5238H35.4832H35.4426H35.402H35.3614H35.3208H35.2801H35.2396H35.1989H35.1583H35.1177H35.0771H35.0365H34.9959H34.9553H34.9147H34.8741H34.8335H34.7929H34.7522H34.7117H34.671H34.6304H34.5898H34.5492H34.5086H34.468H34.4274H34.3868H34.3462H34.3056H34.265H34.2243H34.1838H34.1431H34.1025H34.0619H34.0213H33.9807H33.9401H33.8995H33.8589H33.8183H33.7777H33.7371H33.6964H33.6559H33.6152H33.5746H33.534H33.4934H33.4528H33.4122H33.3716H33.331H33.2904H33.2498H33.2092H33.1685H33.128H33.0873H33.0467H33.0061H32.9655H32.9249H32.8843H32.8437H32.8031H32.7625H32.7219H32.6813H32.6406H32.6H32.5594H32.5188H32.4782H32.4376H32.397H32.3564H32.3158H32.2752H32.2346H32.194H32.1534H32.1127H32.0721H32.0315H31.9909H31.9503H31.9097H31.8691H31.8285H31.7879H31.7473H31.7067H31.666H31.6255H31.5848H31.5442H31.5036H31.416V8.61119C31.416 8.31526 31.4463 8.02649 31.5036 7.74768C31.5159 7.68797 31.5295 7.62877 31.5442 7.57005C31.5569 7.5198 31.5705 7.46992 31.5848 7.42041C31.5977 7.37619 31.6112 7.33227 31.6255 7.28866C31.6385 7.24881 31.6519 7.20917 31.666 7.16986C31.6791 7.1334 31.6926 7.09714 31.7067 7.06115C31.7198 7.02754 31.7333 6.99409 31.7473 6.96092C31.7605 6.92961 31.774 6.89843 31.7879 6.8675C31.8011 6.83812 31.8147 6.80894 31.8285 6.77994C31.8417 6.75225 31.8553 6.72477 31.8691 6.69742C31.8824 6.67113 31.896 6.64504 31.9097 6.61905C31.923 6.594 31.9365 6.56913 31.9503 6.5444C31.9636 6.5204 31.9772 6.49662 31.9909 6.47293C32.0043 6.44996 32.0178 6.42708 32.0315 6.40441C32.0449 6.38242 32.0584 6.3606 32.0721 6.33888C32.0854 6.31784 32.0991 6.297 32.1127 6.27622C32.1261 6.25589 32.1396 6.23563 32.1534 6.21554C32.1668 6.19585 32.1802 6.17616 32.194 6.15671C32.2073 6.13787 32.2209 6.11927 32.2346 6.10066C32.2479 6.08236 32.2615 6.0642 32.2752 6.04614C32.2887 6.02835 32.3021 6.01049 32.3158 5.99294C32.3291 5.97586 32.3428 5.95905 32.3564 5.94218C32.3698 5.92554 32.3833 5.90904 32.397 5.89264C32.4105 5.87644 32.4239 5.86024 32.4376 5.84425C32.451 5.82866 32.4646 5.81335 32.4782 5.79796C32.4917 5.78271 32.5051 5.76743 32.5188 5.75239C32.5322 5.73765 32.5458 5.72304 32.5594 5.70847C32.5729 5.6941 32.5864 5.67984 32.6 5.66564C32.6135 5.65164 32.627 5.63765 32.6406 5.62382C32.6541 5.61024 32.6677 5.59685 32.6813 5.58347C32.6948 5.57015 32.7082 5.5567 32.7219 5.54358C32.7352 5.53074 32.7489 5.5182 32.7625 5.50553C32.776 5.49292 32.7894 5.48028 32.8031 5.46785C32.8165 5.45561 32.8301 5.44362 32.8437 5.43159C32.8572 5.41963 32.8707 5.40767 32.8843 5.39587C32.8978 5.38425 32.9113 5.3728 32.9249 5.36134C32.9384 5.34999 32.9519 5.33868 32.9655 5.32749C32.979 5.31641 32.9925 5.30547 33.0061 5.29459L33.0467 5.26243L33.0873 5.23112C33.1008 5.22085 33.1144 5.21066 33.128 5.20056C33.1414 5.19056 33.1549 5.1806 33.1685 5.17074L33.2092 5.1417C33.2226 5.13218 33.2362 5.12272 33.2498 5.11337L33.2904 5.08568L33.331 5.05875C33.3445 5.0499 33.358 5.04109 33.3716 5.03242L33.4122 5.00687C33.4257 4.99843 33.4392 4.98999 33.4528 4.98172C33.4663 4.97352 33.4799 4.96556 33.4934 4.95753C33.507 4.9495 33.5204 4.9414 33.534 4.93354C33.5474 4.92578 33.5611 4.91829 33.5746 4.91067C33.5882 4.90304 33.6016 4.89528 33.6152 4.88783C33.6287 4.88047 33.6423 4.87336 33.6559 4.86614C33.6694 4.85892 33.6828 4.85167 33.6964 4.84459C33.7099 4.83757 33.7235 4.83076 33.7371 4.82392C33.7506 4.81711 33.7641 4.81026 33.7777 4.80359L33.8183 4.78383C33.8318 4.77736 33.8453 4.77106 33.8589 4.76472C33.8724 4.75842 33.8859 4.75201 33.8995 4.74585C33.913 4.73975 33.9266 4.73392 33.9401 4.72795C33.9536 4.72199 33.9671 4.71592 33.9807 4.7101C33.9942 4.70434 34.0078 4.69885 34.0213 4.69322C34.0348 4.68763 34.0483 4.68197 34.0619 4.67651L34.1025 4.66042L34.1431 4.64487C34.1567 4.63972 34.1702 4.6345 34.1838 4.62952C34.1972 4.62457 34.2108 4.61993 34.2243 4.61511C34.2379 4.6103 34.2514 4.60539 34.265 4.60071C34.2784 4.59607 34.292 4.5917 34.3056 4.58719L34.3462 4.57394C34.3597 4.56961 34.3732 4.56523 34.3868 4.56103C34.4003 4.55686 34.4139 4.55293 34.4274 4.5489C34.4409 4.54484 34.4544 4.54063 34.468 4.53674C34.4815 4.53287 34.4951 4.52928 34.5086 4.52555C34.5221 4.52183 34.5356 4.51807 34.5492 4.51447L34.5898 4.5039C34.6033 4.50048 34.6169 4.49723 34.6304 4.49394C34.644 4.49065 34.6574 4.48713 34.671 4.48398C34.6845 4.48086 34.6981 4.47801 34.7117 4.475L34.7522 4.46608L34.7929 4.45765C34.8063 4.45494 34.8199 4.45239 34.8335 4.44982C34.847 4.44721 34.8605 4.44443 34.8741 4.44196C34.8876 4.43948 34.9012 4.43738 34.9147 4.43508L34.9553 4.4283C34.9688 4.42613 34.9824 4.4239 34.9959 4.42186C35.0094 4.41983 35.023 4.41803 35.0365 4.41614C35.0501 4.41424 35.0635 4.41217 35.0771 4.41041C35.0906 4.40865 35.1042 4.40709 35.1177 4.40546L35.1583 4.40079C35.1719 4.3993 35.1854 4.39764 35.1989 4.39628C35.2124 4.39492 35.226 4.39387 35.2396 4.39265L35.2801 4.38903L35.3208 4.38595C35.3343 4.38496 35.3478 4.38418 35.3614 4.38334C35.3749 4.38252 35.3884 4.38147 35.402 4.38076C35.4155 4.38005 35.4291 4.37964 35.4426 4.37907L35.4832 4.37754C35.4968 4.3771 35.5103 4.37652 35.5238 4.37622C35.5373 4.37591 35.5509 4.37588 35.5644 4.37568C35.5779 4.37547 35.5915 4.37524 35.605 4.37517L35.6152 4.37503H35.6456H35.6862H35.7268H35.7675H35.808H35.8487H35.8893H35.9299H35.9705H36.0111H36.0517H36.0923H36.1329H36.1735H36.2141H36.2547H36.2954H36.3359H36.3766H36.4172H36.4578H36.4984H36.539H36.5796H36.6202H36.6608H36.7014H36.742H36.7826H36.8233H36.8638H36.9045H36.9451H36.9857H37.0263H37.0669H37.1075H37.1481H37.1887H37.2293H37.2699H37.3105H37.3512H37.3917H37.4324H37.473H37.5136H37.5542H37.5948H37.6354H37.676H37.7166H37.7572H37.7978H37.8384H37.8791H37.9197H37.9603H38.0009H38.0415H38.0821H38.1227H38.1633H38.2039H38.2445H38.2851H38.3257H38.3663H38.407H38.4476H38.4882H38.5288H38.5694H38.61H38.6506H38.6912H38.7318H38.7724H38.813H38.8537H38.8942H38.9349H38.9755H39.0161H39.0567H39.0973H39.1379H39.1785H39.2191H39.2597H39.3003H39.3409H39.3816H39.4221H39.4628H39.5034H39.544H39.5846H39.6252H39.6658H39.7064H39.747H39.7876H39.8282H39.8688H39.9095H39.95H39.9907H40.0313H40.0719H40.1125H40.1531H40.1937H40.2343H40.2749H40.3155H40.3561H40.3967H40.4374H40.4779H40.5186H40.5592H40.5998H40.6404H40.681H40.7216H40.7622H40.8028H40.8434H40.884H40.9246H40.9653H41.0058H41.0465H41.0871H41.1277H41.1683H41.2089H41.2495H41.2901H41.3307H41.3714H41.4119H41.4526H41.4932H41.5338H41.5744H41.615H41.6556H41.6962H41.7368H41.7774V4.375Z",fill:"#54B230"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8.1377 19.0723V8.61115C8.1377 8.31608 8.16772 8.02805 8.22476 7.74998C8.23702 7.69027 8.25061 7.63111 8.26532 7.57235C8.27791 7.52203 8.29147 7.47212 8.30584 7.42254C8.31867 7.37836 8.3322 7.33451 8.34637 7.29093C8.35936 7.25105 8.37279 7.21133 8.38692 7.17196C8.40001 7.13553 8.41341 7.09931 8.42744 7.06336C8.44054 7.02974 8.45407 6.99623 8.468 6.96302C8.48116 6.93171 8.49466 6.9006 8.50852 6.8697C8.52172 6.84032 8.53525 6.81118 8.54904 6.78217C8.56224 6.75445 8.57583 6.72694 8.5896 6.69956C8.60283 6.67326 8.61636 6.64717 8.63012 6.62121C8.64338 6.59617 8.65691 6.5713 8.67064 6.54656C8.68394 6.52261 8.69744 6.49879 8.7112 6.47513C8.72453 6.45216 8.73796 6.42925 8.75172 6.40655C8.76505 6.38459 8.77858 6.3628 8.79228 6.34112C8.80554 6.32011 8.81917 6.29923 8.8328 6.27846C8.84616 6.25813 8.85963 6.23787 8.87332 6.21777C8.88679 6.19805 8.90011 6.17836 8.91388 6.15888C8.92717 6.14007 8.94081 6.1215 8.9544 6.1029C8.9678 6.0846 8.98129 6.06644 8.99496 6.04838C9.00839 6.03062 9.02178 6.01273 9.03548 5.99518C9.04881 5.97813 9.06244 5.96136 9.076 5.94452C9.0894 5.92788 9.10289 5.91131 9.11656 5.89487C9.12999 5.87871 9.14342 5.86258 9.15708 5.84662C9.17044 5.831 9.18404 5.81565 9.19764 5.80023C9.21107 5.78495 9.2245 5.7697 9.23816 5.75466C9.25152 5.73992 9.26512 5.72538 9.27868 5.71084C9.29211 5.69644 9.30561 5.68217 9.31924 5.66798C9.33267 5.65398 9.34613 5.63999 9.35976 5.62616C9.37316 5.61257 9.38675 5.59919 9.40032 5.5858C9.41381 5.57249 9.42717 5.55907 9.44084 5.54595C9.4542 5.53311 9.46783 5.52057 9.48136 5.5079C9.49486 5.49526 9.50825 5.48259 9.52192 5.47015C9.53531 5.45795 9.54891 5.44599 9.56244 5.43396C9.57594 5.422 9.58936 5.41 9.603 5.39821C9.61642 5.38659 9.62995 5.37517 9.64352 5.36372C9.65695 5.35237 9.67044 5.34101 9.68404 5.32983C9.69747 5.31878 9.71103 5.30784 9.7246 5.29693C9.73806 5.28612 9.75152 5.27541 9.76512 5.26477C9.77855 5.25423 9.79211 5.24383 9.80567 5.23346C9.81914 5.22319 9.83263 5.21296 9.8462 5.20286L9.88672 5.17311C9.90018 5.16332 9.91368 5.15359 9.92728 5.14397C9.9407 5.13445 9.95423 5.12503 9.9678 5.11567C9.98126 5.10635 9.99476 5.0971 10.0083 5.08795C10.0218 5.07891 10.0353 5.07 10.0489 5.06108C10.0623 5.05224 10.0758 5.04333 10.0894 5.03462C10.1028 5.02601 10.1164 5.01761 10.13 5.00914C10.1435 5.0007 10.1569 4.99216 10.1705 4.98389C10.1839 4.97573 10.1975 4.9678 10.211 4.95977C10.2245 4.95173 10.2379 4.94357 10.2516 4.93567C10.265 4.92791 10.2786 4.92049 10.2921 4.91287C10.3056 4.90524 10.319 4.89752 10.3326 4.89003C10.3461 4.88264 10.3596 4.87553 10.3732 4.86831C10.3867 4.86109 10.4001 4.85384 10.4137 4.84676C10.4271 4.83974 10.4407 4.83286 10.4542 4.82598L10.4948 4.80572C10.5083 4.79905 10.5217 4.79237 10.5353 4.78586C10.5488 4.77939 10.5623 4.77312 10.5758 4.76679C10.5894 4.76045 10.6028 4.75398 10.6164 4.74781C10.6298 4.74168 10.6434 4.73592 10.6569 4.72995C10.6704 4.72399 10.6839 4.71792 10.6974 4.71209C10.7109 4.70633 10.7245 4.70078 10.738 4.69515L10.7785 4.67848L10.819 4.66228L10.8596 4.64676C10.8731 4.64161 10.8865 4.63629 10.9001 4.63128C10.9135 4.62633 10.9272 4.62172 10.9407 4.61691C10.9542 4.6121 10.9676 4.60722 10.9812 4.60254L11.0217 4.58889L11.0623 4.57564C11.0758 4.5713 11.0892 4.56686 11.1028 4.56266C11.1162 4.55849 11.1298 4.55453 11.1433 4.55049C11.1568 4.54643 11.1703 4.54229 11.1839 4.53836C11.1973 4.53447 11.2109 4.53084 11.2244 4.52708L11.265 4.516C11.2784 4.51241 11.2919 4.50875 11.3055 4.50529C11.3189 4.50183 11.3325 4.49868 11.346 4.49536C11.3595 4.49204 11.373 4.48858 11.3866 4.4854C11.4 4.48225 11.4136 4.4793 11.4271 4.47628L11.4676 4.46741C11.4811 4.46449 11.4946 4.46154 11.5082 4.4588C11.5216 4.45609 11.5352 4.45358 11.5487 4.45097C11.5622 4.44836 11.5757 4.44565 11.5892 4.44318C11.6027 4.4407 11.6162 4.43847 11.6298 4.43613L11.6703 4.42935C11.6838 4.42718 11.6973 4.42485 11.7108 4.42278C11.7243 4.42071 11.7379 4.41898 11.7514 4.41705C11.7648 4.41515 11.7783 4.41312 11.7919 4.41133L11.8324 4.40621L11.873 4.40157C11.8865 4.40004 11.8999 4.39828 11.9135 4.39689C11.927 4.3955 11.9405 4.39448 11.954 4.39323L11.9946 4.38964C12.0081 4.38849 12.0216 4.38737 12.0351 4.38639C12.0486 4.3854 12.0621 4.38462 12.0757 4.38378C12.0892 4.38293 12.1027 4.38195 12.1162 4.3812C12.1297 4.38046 12.1432 4.37995 12.1567 4.37934L12.1973 4.37778C12.2108 4.3773 12.2242 4.37659 12.2378 4.37625C12.2513 4.37591 12.2648 4.37595 12.2783 4.37575C12.2918 4.37554 12.3053 4.3753 12.3189 4.37524L12.3594 4.375H12.3999H12.4405H12.481H12.5215H12.5621H12.6026H12.6431H12.6837H12.7242H12.7647H12.8053H12.8458H12.8863H12.9269H12.9674H13.008H13.0485H13.089H13.1296H13.1701H13.2107H13.2512H13.2917H13.3323H13.3728H13.4133H13.4539H13.4944H13.5349H13.5755H13.616H13.6565H13.6971H13.7376H13.7781H13.8187H13.8592H13.8997H13.9403H13.9808H14.0213H14.0619H14.1024H14.143H14.1835H14.224H14.2646H14.3051H14.3457H14.3862H14.4267H14.4673H14.5078H14.5483H14.5889H14.6294H14.6699H14.7105H14.751H14.7915H14.8321H14.8726H14.9131H14.9537H14.9942H15.0347H15.0753H15.1158H15.1563H15.1969H15.2374H15.278H15.3185H15.359H15.3996H15.4401H15.4807H15.5212H15.5617H15.6023H15.6428H15.6833H15.7239H15.7644H15.8049H15.8455H15.886H15.9265H15.9671H16.0076H16.0481H16.0887H16.1292H16.1697H16.2103H16.2508H16.2914H16.3319H16.3724H16.413H16.4535H16.494H16.5346H16.5751H16.6157H16.6562H16.6967H16.7373H16.7778H16.8183H16.8589H16.8994H16.9399H16.9805H17.021H17.0615H17.1021H17.1426H17.1831H17.2237H17.2642H17.3047H17.3453H17.3858H17.4264H17.4669H17.5074H17.548H17.5885H17.629H17.6696H17.7101H17.7506H17.7912H17.8317H17.8722H17.9128H17.9533H17.9938H18.0344H18.0749H18.1154H18.156H18.1965H18.237H18.2776H18.3181H18.3586H18.3992H18.4397H18.4803H18.5147V25.5542C18.5147 25.7384 18.5029 25.9199 18.4803 26.098C18.4694 26.1837 18.4556 26.2685 18.4397 26.3525C18.4276 26.4165 18.4141 26.4799 18.3992 26.5427C18.3866 26.5957 18.3732 26.6483 18.3586 26.7005C18.3459 26.7463 18.3323 26.7917 18.3181 26.8368C18.3052 26.8777 18.2917 26.9182 18.2776 26.9586C18.2646 26.9959 18.2511 27.033 18.237 27.0698C18.224 27.1042 18.2105 27.1384 18.1965 27.1723C18.1834 27.2043 18.1699 27.2361 18.156 27.2676C18.1428 27.2976 18.1293 27.3273 18.1154 27.357C18.1023 27.3852 18.0887 27.4132 18.0749 27.4411C18.0617 27.4679 18.0482 27.4945 18.0344 27.5209C18.0211 27.5463 18.0076 27.5717 17.9938 27.5968C17.9805 27.6211 17.9671 27.6454 17.9533 27.6694C17.94 27.6926 17.9265 27.7156 17.9128 27.7385C17.8995 27.7607 17.8859 27.7826 17.8722 27.8045C17.8589 27.8259 17.8454 27.8471 17.8317 27.8682C17.8183 27.8889 17.8049 27.9095 17.7912 27.9298C17.7778 27.9496 17.7643 27.9692 17.7506 27.9887C17.7373 28.0078 17.7238 28.0267 17.7101 28.0456C17.6967 28.0641 17.6832 28.0826 17.6696 28.1009C17.6562 28.1188 17.6427 28.1365 17.629 28.1542C17.6157 28.1714 17.6021 28.1885 17.5885 28.2056C17.5751 28.2224 17.5616 28.2392 17.548 28.2558C17.5346 28.2721 17.521 28.2881 17.5074 28.3041C17.494 28.3199 17.4805 28.3356 17.4669 28.3512C17.4534 28.3665 17.44 28.382 17.4264 28.3972C17.413 28.412 17.3994 28.4266 17.3858 28.4413C17.3723 28.4558 17.359 28.4704 17.3453 28.4847C17.3319 28.4988 17.3183 28.5126 17.3047 28.5265C17.2913 28.5402 17.2778 28.5539 17.2642 28.5674C17.2508 28.5808 17.2373 28.5941 17.2237 28.6073C17.2103 28.6203 17.1967 28.6331 17.1831 28.6459C17.1697 28.6586 17.1562 28.6713 17.1426 28.6839C17.1292 28.6962 17.1156 28.7082 17.1021 28.7204C17.0886 28.7325 17.0752 28.7447 17.0615 28.7566C17.0482 28.7682 17.0345 28.7796 17.021 28.7911C17.0075 28.8026 16.9941 28.8142 16.9805 28.8255C16.9671 28.8366 16.9535 28.8475 16.9399 28.8584C16.9264 28.8693 16.913 28.8803 16.8994 28.8911C16.886 28.9016 16.8724 28.9119 16.8589 28.9223C16.8454 28.9327 16.832 28.9431 16.8183 28.9533C16.8049 28.9634 16.7913 28.9732 16.7778 28.9831C16.7643 28.993 16.7509 29.003 16.7373 29.0127C16.7239 29.0222 16.7103 29.0316 16.6967 29.041C16.6832 29.0504 16.6698 29.0599 16.6562 29.0692C16.6428 29.0782 16.6292 29.0871 16.6157 29.096C16.6021 29.105 16.5887 29.1141 16.5751 29.1229C16.5617 29.1315 16.5481 29.1398 16.5346 29.1484C16.5211 29.1569 16.5076 29.1655 16.494 29.1738C16.4806 29.1821 16.467 29.1901 16.4535 29.1981C16.44 29.2062 16.4266 29.2143 16.413 29.2223C16.3995 29.2301 16.386 29.2377 16.3724 29.2454C16.359 29.2531 16.3455 29.2608 16.3319 29.2682C16.3184 29.2757 16.3049 29.2831 16.2914 29.2904L16.2508 29.3119C16.2373 29.319 16.2239 29.3261 16.2103 29.333C16.1968 29.3399 16.1833 29.3466 16.1697 29.3533C16.1562 29.36 16.1428 29.367 16.1292 29.3735C16.1158 29.38 16.1022 29.3862 16.0887 29.3926C16.0752 29.399 16.0617 29.4054 16.0481 29.4117C16.0347 29.4178 16.0211 29.4237 16.0076 29.4298L15.9671 29.4476L15.9265 29.4649C15.9131 29.4706 15.8995 29.4761 15.886 29.4816C15.8725 29.4871 15.859 29.4928 15.8455 29.4982C15.832 29.5035 15.8185 29.5085 15.8049 29.5136C15.7914 29.5188 15.778 29.5241 15.7644 29.5292C15.7509 29.5342 15.7374 29.539 15.7239 29.5438L15.6833 29.5582L15.6428 29.5722C15.6293 29.5767 15.6158 29.5811 15.6023 29.5854C15.5887 29.5899 15.5753 29.5944 15.5617 29.5987C15.5483 29.6029 15.5347 29.6068 15.5212 29.6109L15.4807 29.6231L15.4401 29.6347C15.4266 29.6385 15.4131 29.6421 15.3996 29.6457C15.386 29.6494 15.3726 29.6533 15.359 29.6567C15.3456 29.6602 15.332 29.6633 15.3185 29.6667C15.305 29.6701 15.2915 29.6734 15.278 29.6767L15.2374 29.6861L15.1969 29.695C15.1834 29.6979 15.1699 29.7011 15.1563 29.7039C15.1429 29.7067 15.1293 29.709 15.1158 29.7117C15.1023 29.7143 15.0888 29.717 15.0753 29.7195L15.0347 29.7269C15.0213 29.7292 15.0077 29.7314 14.9942 29.7336C14.9807 29.7358 14.9672 29.7383 14.9537 29.7404C14.9402 29.7425 14.9267 29.7442 14.9131 29.7462L14.8726 29.7519C14.8591 29.7538 14.8456 29.7556 14.8321 29.7573C14.8186 29.759 14.805 29.7604 14.7915 29.762C14.778 29.7635 14.7645 29.7652 14.751 29.7667C14.7375 29.7681 14.724 29.7693 14.7105 29.7706L14.6699 29.7742C14.6564 29.7754 14.6429 29.7768 14.6294 29.7778C14.6159 29.7788 14.6024 29.7795 14.5889 29.7804L14.5483 29.783L14.5078 29.7851C14.4943 29.7858 14.4808 29.7862 14.4673 29.7867C14.4537 29.7872 14.4402 29.7878 14.4267 29.7883C14.4132 29.7886 14.3997 29.7888 14.3862 29.789L14.3457 29.7895C14.3331 29.7896 14.3207 29.79 14.3081 29.79H14.2646H14.224H14.1835H14.143H14.1024H14.0619H14.0213H13.9808H13.9403H13.8997H13.8592H13.8187H13.7781H13.7376H13.6971H13.6565H13.616H13.5755H13.5349H13.4944H13.4539H13.4133H13.3728H13.3323H13.2917H13.2512H13.2107H13.1701H13.1296H13.089H13.0485H13.008H12.9674H12.9269H12.8863H12.8458H12.8053H12.7647H12.7242H12.6837H12.6431H12.6026H12.5621H12.5215H12.481H12.4405H12.3999H12.3594H12.3189H12.2783H12.2378H12.1973H12.1567H12.1162H12.0757H12.0351H11.9946H11.954H11.9135H11.873H11.8324H11.7919H11.7514H11.7108H11.6703H11.6298H11.5892H11.5487H11.5082H11.4676H11.4271H11.3866H11.346H11.3055H11.265H11.2244H11.1839H11.1433H11.1028H11.0623H11.0217H10.9812H10.9407H10.9001H10.8596H10.819H10.7785H10.738H10.6974H10.6569H10.6164H10.5758H10.5353H10.4948H10.4542H10.4137H10.3732H10.3326H10.2921H10.2516H10.211H10.1705H10.13H10.0894H10.0489H10.0083H9.9678H9.92728H9.88672H9.8462H9.80567H9.76512H9.7246H9.68404H9.64352H9.603H9.56244H9.52192H9.48136H9.44084H9.40032H9.35976H9.31924H9.27868H9.23816H9.19764H9.15708H9.11656H9.076H9.03548H8.99496H8.9544H8.91388H8.87332H8.8328H8.79228H8.75172H8.7112H8.67064H8.63012H8.5896H8.54904H8.50852H8.468H8.42744H8.38692H8.34637H8.30584H8.26532H8.22476H8.1377V20.8632C8.1377 20.8632 8.16765 20.8715 8.22476 20.886L8.26532 20.8962L8.30584 20.9062L8.34637 20.916L8.38692 20.9256L8.42744 20.9351L8.468 20.9445L8.50852 20.9537L8.54904 20.9628L8.5896 20.9717L8.63012 20.9805L8.67064 20.9893L8.7112 20.9979L8.75172 21.0064L8.79228 21.0147L8.8328 21.023L8.87332 21.0311L8.91388 21.0392L8.9544 21.0471L8.99496 21.055L9.03548 21.0628L9.076 21.0704L9.11656 21.078L9.15708 21.0854L9.19764 21.0928L9.23816 21.1001L9.27868 21.1072L9.31924 21.1143L9.35976 21.1212L9.40032 21.1281L9.44084 21.1349L9.48136 21.1416L9.52192 21.1482L9.56244 21.1547L9.603 21.1612L9.64352 21.1675L9.68404 21.1738L9.7246 21.1799L9.76512 21.186L9.80567 21.1919L9.8462 21.1978L9.88672 21.2036L9.92728 21.2093L9.9678 21.2149L10.0083 21.2205L10.0489 21.2259L10.0894 21.2313L10.13 21.2366L10.1705 21.2417L10.211 21.2468L10.2516 21.2518L10.2921 21.2568L10.3326 21.2615L10.3732 21.2663L10.4137 21.2709L10.4542 21.2755L10.4948 21.28L10.5353 21.2844L10.5758 21.2887L10.6164 21.2929L10.6569 21.297L10.6974 21.301L10.738 21.305L10.7785 21.3089L10.819 21.3126L10.8596 21.3164L10.9001 21.3199L10.9407 21.3234L10.9812 21.3269L11.0217 21.3302L11.0623 21.3335L11.1028 21.3366L11.1433 21.3397L11.1839 21.3426L11.2244 21.3455L11.265 21.3483L11.3055 21.351L11.346 21.3536L11.3866 21.3562L11.4271 21.3586L11.4676 21.361L11.5082 21.3632L11.5487 21.3654L11.5892 21.3675L11.6298 21.3694L11.6703 21.3713L11.7108 21.3731L11.7514 21.3748L11.7919 21.3765L11.8324 21.3779L11.873 21.3793L11.9135 21.3807L11.954 21.3819L11.9946 21.3831L12.0351 21.3841L12.0757 21.385L12.1162 21.386L12.1567 21.3867L12.1973 21.3873L12.2378 21.388L12.2783 21.3884L12.3189 21.3888L12.3594 21.3891L12.3999 21.3892L12.4405 21.3893L12.481 21.3893L12.5215 21.389L12.5621 21.3888L12.6026 21.3885L12.6431 21.3883L12.6837 21.3875L12.7242 21.3868L12.7647 21.3861L12.8053 21.3853L12.8458 21.3843L12.8863 21.383L12.9269 21.3818L12.9674 21.3805L13.008 21.3791L13.0485 21.3773L13.089 21.3756L13.1296 21.3737L13.1701 21.3719L13.2107 21.3697L13.2512 21.3673L13.2917 21.3649L13.3323 21.3626L13.3728 21.3599L13.4133 21.3569L13.4539 21.354L13.4944 21.3511L13.5349 21.3478L13.5755 21.3443L13.616 21.3408L13.6565 21.3372L13.6971 21.3334L13.7376 21.3292L13.7781 21.3251L13.8187 21.3209L13.8592 21.3164L13.8997 21.3116L13.9403 21.3068L13.9808 21.302L14.0213 21.2967L14.0619 21.2912L14.1024 21.2858L14.143 21.2802C14.1566 21.2782 14.1699 21.2761 14.1835 21.2741L14.224 21.2679L14.2646 21.2618L14.3051 21.2553C14.3187 21.2531 14.3321 21.2507 14.3457 21.2484L14.3862 21.2416C14.3996 21.2393 14.4134 21.2371 14.4267 21.2347C14.4404 21.2323 14.4537 21.2296 14.4673 21.2271L14.5078 21.2195L14.5483 21.2119L14.5889 21.2039C14.6025 21.2012 14.6159 21.1983 14.6294 21.1955L14.6699 21.1871L14.7105 21.1785C14.7242 21.1756 14.7374 21.1724 14.751 21.1694L14.7915 21.1601C14.805 21.157 14.8187 21.1541 14.8321 21.151C14.8458 21.1477 14.8591 21.1443 14.8726 21.141L14.9131 21.1309C14.9266 21.1276 14.9403 21.1243 14.9537 21.1209C14.9673 21.1174 14.9807 21.1138 14.9942 21.1102L15.0347 21.0993C15.0482 21.0956 15.0619 21.0921 15.0753 21.0883C15.0889 21.0846 15.1023 21.0806 15.1158 21.0768L15.1563 21.065C15.1698 21.061 15.1835 21.0571 15.1969 21.0531L15.2374 21.0406L15.278 21.0278C15.2914 21.0235 15.3052 21.0193 15.3185 21.015L15.359 21.0014L15.3996 20.9876C15.413 20.9829 15.4268 20.9784 15.4401 20.9737C15.4538 20.9689 15.4671 20.9638 15.4807 20.9589L15.5212 20.9441C15.5347 20.939 15.5483 20.9341 15.5617 20.9289C15.5754 20.9237 15.5887 20.9183 15.6023 20.913C15.6158 20.9077 15.6294 20.9024 15.6428 20.897L15.6833 20.8804L15.7239 20.8632C15.7373 20.8575 15.7511 20.852 15.7644 20.8461C15.7781 20.8401 15.7914 20.8339 15.8049 20.8278L15.8455 20.8094L15.886 20.7904C15.8996 20.784 15.9131 20.7774 15.9265 20.7708C15.94 20.7642 15.9538 20.7577 15.9671 20.751C15.9808 20.7441 15.9941 20.737 16.0076 20.73C16.0211 20.723 16.0348 20.7161 16.0481 20.7091C16.0618 20.7018 16.0752 20.6944 16.0887 20.687C16.1023 20.6796 16.1158 20.6721 16.1292 20.6646C16.1428 20.6569 16.1563 20.6492 16.1697 20.6415C16.1834 20.6336 16.1969 20.6255 16.2103 20.6175C16.2239 20.6094 16.2375 20.6013 16.2508 20.5931C16.2645 20.5847 16.2779 20.5761 16.2914 20.5676C16.3049 20.559 16.3186 20.5504 16.3319 20.5417C16.3456 20.5328 16.359 20.5236 16.3724 20.5145C16.386 20.5053 16.3996 20.4963 16.413 20.487C16.4267 20.4774 16.44 20.4677 16.4535 20.458C16.4671 20.4482 16.4807 20.4384 16.494 20.4286C16.5077 20.4184 16.5211 20.408 16.5346 20.3977C16.5482 20.3872 16.5618 20.3767 16.5751 20.3661C16.5888 20.3552 16.6023 20.3442 16.6157 20.3331C16.6293 20.3218 16.6428 20.3104 16.6562 20.299C16.6698 20.2873 16.6834 20.2757 16.6967 20.2639C16.7105 20.2517 16.7238 20.2393 16.7373 20.2269C16.7508 20.2144 16.7645 20.2019 16.7778 20.1892C16.7915 20.176 16.805 20.1627 16.8183 20.1493C16.832 20.1356 16.8455 20.1218 16.8589 20.108C16.8725 20.0938 16.8861 20.0797 16.8994 20.0653C16.9132 20.0503 16.9265 20.0351 16.9399 20.0199C16.9537 20.0043 16.9672 19.9885 16.9805 19.9727C16.9942 19.9564 17.0078 19.9401 17.021 19.9236C17.0347 19.9064 17.0483 19.8892 17.0615 19.8718C17.0754 19.8536 17.0888 19.8352 17.1021 19.8168C17.1159 19.7976 17.1293 19.7783 17.1426 19.7589C17.1564 19.7386 17.1699 19.7182 17.1831 19.6976C17.197 19.6761 17.2105 19.6543 17.2237 19.6324C17.2375 19.6093 17.2511 19.5861 17.2642 19.5627C17.2781 19.5378 17.2916 19.5128 17.3047 19.4876C17.3187 19.4606 17.3322 19.4335 17.3453 19.4061C17.3594 19.3767 17.3728 19.3471 17.3858 19.3172C17.3999 19.2847 17.4135 19.2521 17.4264 19.2191C17.4406 19.1827 17.4541 19.1461 17.4669 19.1091C17.4814 19.0672 17.4948 19.0248 17.5074 18.982C17.5222 18.9321 17.5356 18.8816 17.548 18.8307C17.5632 18.7675 17.5768 18.7036 17.5885 18.639C17.6059 18.5428 17.6193 18.4451 17.629 18.346C17.6426 18.208 17.6493 18.0674 17.6493 17.9246V13.01H17.5885H17.548H17.5074H17.4669H17.4264H17.3858H17.3453H17.3047H17.2642H17.2237H17.1831H17.1426H17.1021H17.0615H17.021H16.9805H16.9399H16.8994H16.8589H16.8183H16.7778H16.7373H16.6967H16.6562H16.6157H16.5751H16.5346H16.494H16.4535H16.413H16.3724H16.3319H16.2914H16.2508H16.2103H16.1697H16.1292H16.0887H16.0481H16.0076H15.9671H15.9265H15.886H15.8455H15.8049H15.7644H15.7239H15.6833H15.6428H15.6023H15.5617H15.5212H15.4807H15.4401H15.3996H15.359H15.3185H15.278H15.2374H15.1969H15.1563H15.1158H15.0753H15.0347H14.9942H14.9537H14.9131H14.8726H14.8321H14.7915H14.751H14.7105H14.6699H14.6294H14.5889H14.5483H14.5078H14.4673H14.4267H14.3862H14.3457H14.3051H14.2646H14.224H14.1835H14.143H14.1024H14.0619L14.0312 17.9246C14.0312 18.0016 14.0278 18.0773 14.0213 18.1515C14.0131 18.2461 13.9995 18.3383 13.9808 18.4279C13.9693 18.4833 13.9557 18.5377 13.9403 18.591C13.928 18.6335 13.9145 18.6753 13.8997 18.7164C13.8872 18.7514 13.8736 18.7858 13.8592 18.8198C13.8464 18.8501 13.8329 18.8799 13.8187 18.9093C13.8057 18.9361 13.7923 18.9626 13.7781 18.9885C13.7652 19.0125 13.7516 19.0359 13.7376 19.0592C13.7245 19.081 13.711 19.1026 13.6971 19.1238C13.6839 19.1438 13.6704 19.1634 13.6565 19.1828C13.6433 19.2013 13.6299 19.2196 13.616 19.2376C13.6028 19.2547 13.5893 19.2714 13.5755 19.288C13.5622 19.3039 13.5487 19.3196 13.5349 19.3351C13.5217 19.35 13.5082 19.3647 13.4944 19.3792C13.4811 19.3931 13.4676 19.407 13.4539 19.4206C13.4406 19.4337 13.4271 19.4467 13.4133 19.4594C13.4 19.4718 13.3865 19.4839 13.3728 19.4958C13.3595 19.5075 13.346 19.519 13.3323 19.5302C13.3189 19.5412 13.3054 19.552 13.2917 19.5627C13.2784 19.5731 13.2648 19.5833 13.2512 19.5934C13.2378 19.6032 13.2243 19.6129 13.2107 19.6224C13.1973 19.6318 13.1838 19.6409 13.1701 19.65C13.1567 19.6588 13.1432 19.6674 13.1296 19.676C13.1162 19.6843 13.1027 19.6925 13.089 19.7006C13.0757 19.7085 13.0621 19.7162 13.0485 19.7239C13.0351 19.7314 13.0216 19.7386 13.008 19.7459C12.9946 19.753 12.9811 19.76 12.9674 19.7668C12.954 19.7735 12.9405 19.7801 12.9269 19.7866C12.9135 19.793 12.9 19.7993 12.8863 19.8055C12.8729 19.8115 12.8595 19.8175 12.8458 19.8232C12.8325 19.8289 12.8189 19.8344 12.8053 19.8399C12.7919 19.8453 12.7783 19.8505 12.7647 19.8557C12.7513 19.8608 12.7379 19.8657 12.7242 19.8706C12.7108 19.8754 12.6973 19.8802 12.6837 19.8848C12.6703 19.8893 12.6567 19.8937 12.6431 19.898C12.6298 19.9022 12.6162 19.9063 12.6026 19.9103C12.5892 19.9143 12.5757 19.9183 12.5621 19.922C12.5486 19.9258 12.5351 19.9295 12.5215 19.933C12.5081 19.9365 12.4946 19.9399 12.481 19.9432C12.4676 19.9464 12.4541 19.9495 12.4405 19.9526C12.427 19.9556 12.4135 19.9586 12.3999 19.9614C12.3865 19.9642 12.373 19.967 12.3594 19.9696C12.346 19.9722 12.3324 19.9746 12.3189 19.977C12.3054 19.9794 12.2919 19.9817 12.2783 19.9839L12.2378 19.9902C12.2244 19.9921 12.2108 19.9939 12.1973 19.9957C12.1838 19.9975 12.1703 19.9993 12.1567 20.0009L12.1162 20.0054C12.1028 20.0068 12.0892 20.008 12.0757 20.0093C12.0622 20.0105 12.0487 20.0118 12.0351 20.0128C12.0217 20.0139 12.0082 20.0148 11.9946 20.0157C11.9811 20.0165 11.9676 20.0173 11.954 20.018C11.9405 20.0187 11.9271 20.0194 11.9135 20.02C11.9001 20.0205 11.8865 20.0208 11.873 20.0212L11.8324 20.0222C11.819 20.0224 11.8055 20.0225 11.7919 20.0226L11.7514 20.0226L11.7108 20.0223L11.6703 20.0218L11.6298 20.0209L11.5892 20.0198L11.5487 20.0185L11.5082 20.0168L11.4676 20.0149L11.4271 20.0129L11.3866 20.0104L11.346 20.0078L11.3055 20.0049L11.265 20.0017L11.2244 19.9983L11.1839 19.9946L11.1433 19.9907L11.1028 19.9865L11.0623 19.9821L11.0217 19.9775L10.9812 19.9725L10.9407 19.9674L10.9001 19.962L10.8596 19.9563L10.819 19.9504L10.7785 19.9443L10.738 19.9379L10.6974 19.9313L10.6569 19.9244L10.6164 19.9173L10.5758 19.91L10.5353 19.9024L10.4948 19.8946L10.4542 19.8866L10.4137 19.8783L10.3732 19.8698L10.3326 19.8611L10.2921 19.8521L10.2516 19.8429L10.211 19.8335L10.1705 19.8238L10.13 19.8139L10.0894 19.8038L10.0489 19.7936L10.0083 19.7829L9.9678 19.7722L9.92728 19.7612L9.88672 19.75L9.8462 19.7386L9.80567 19.7269L9.76512 19.7151L9.7246 19.703L9.68404 19.6907L9.64352 19.6782L9.603 19.6654L9.56244 19.6525L9.52192 19.6394L9.48136 19.6261L9.44084 19.6125L9.40032 19.5987L9.35976 19.5847L9.31924 19.5706L9.27868 19.5562L9.23816 19.5416L9.19764 19.5269L9.15708 19.5118L9.11656 19.4967L9.076 19.4813L9.03548 19.4657L8.99496 19.4499L8.9544 19.434L8.91388 19.4178L8.87332 19.4014L8.8328 19.3849L8.79228 19.3682L8.75172 19.3513L8.7112 19.3341L8.67064 19.3168L8.63012 19.2993L8.5896 19.2817L8.54904 19.2637L8.50852 19.2457L8.468 19.2275L8.42744 19.2091L8.38692 19.1905L8.34637 19.1717L8.30584 19.1528L8.26532 19.1336L8.22476 19.1144C8.19572 19.1004 8.16671 19.0864 8.1377 19.0723Z",fill:"#006CB9"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M19.8644 14.0838C19.8348 14.1079 19.8057 14.1321 19.7773 14.1567V8.61115C19.7773 8.31625 19.8074 8.02839 19.8644 7.75042C19.8766 7.69075 19.8902 7.63162 19.9049 7.57289C19.9175 7.52251 19.9311 7.47253 19.9455 7.42288C19.9583 7.37866 19.9718 7.33475 19.986 7.29114C19.999 7.25129 20.0124 7.21164 20.0265 7.17233C20.0396 7.1359 20.0531 7.09965 20.0671 7.06369C20.0802 7.03008 20.0937 6.99663 20.1076 6.96343C20.1208 6.93212 20.1343 6.90101 20.1481 6.87007C20.1613 6.84069 20.1749 6.81152 20.1887 6.78251C20.2019 6.75479 20.2154 6.72734 20.2292 6.69997C20.2424 6.67367 20.256 6.64758 20.2698 6.62159C20.283 6.59655 20.2966 6.57167 20.3103 6.54694C20.3236 6.52298 20.3371 6.49912 20.3508 6.47547C20.3642 6.4525 20.3776 6.42963 20.3914 6.40692C20.4047 6.385 20.4182 6.36321 20.4319 6.34156C20.4452 6.32048 20.4588 6.29964 20.4725 6.27883C20.4858 6.25847 20.4993 6.23824 20.513 6.21814C20.5264 6.19842 20.5398 6.1787 20.5535 6.15925C20.5668 6.14041 20.5805 6.12188 20.5941 6.10331C20.6074 6.08501 20.6209 6.06681 20.6346 6.04875C20.648 6.03096 20.6614 6.01314 20.6751 5.99558C20.6884 5.97854 20.7021 5.96173 20.7157 5.94489C20.7291 5.92825 20.7426 5.91168 20.7562 5.89528C20.7696 5.87908 20.7831 5.86295 20.7968 5.84699C20.8101 5.83141 20.8237 5.81602 20.8373 5.80064C20.8507 5.78539 20.8642 5.77007 20.8778 5.75499C20.8912 5.74025 20.9048 5.72568 20.9183 5.71115C20.9317 5.69678 20.9453 5.68251 20.9589 5.66832C20.9723 5.65432 20.9858 5.64033 20.9994 5.6265C21.0128 5.61295 21.0264 5.59956 21.0399 5.58618C21.0534 5.57286 21.0668 5.55944 21.0805 5.54629C21.0938 5.53345 21.1075 5.52091 21.121 5.50824C21.1345 5.4956 21.1479 5.48293 21.1616 5.47049C21.175 5.45829 21.1886 5.4463 21.2021 5.43427C21.2156 5.42227 21.229 5.41031 21.2426 5.39852C21.2561 5.38689 21.2696 5.37548 21.2832 5.36402C21.2966 5.35267 21.3101 5.34132 21.3237 5.3301C21.3372 5.31906 21.3507 5.30811 21.3642 5.29723C21.3777 5.28642 21.3912 5.27568 21.4048 5.26504C21.4182 5.25454 21.4318 5.24413 21.4453 5.23377C21.4588 5.2235 21.4723 5.21326 21.4858 5.20317C21.4993 5.19314 21.5128 5.18324 21.5264 5.17338C21.5399 5.16362 21.5534 5.15386 21.5669 5.14424C21.5804 5.13472 21.5939 5.12533 21.6075 5.11594C21.6209 5.10663 21.6344 5.09734 21.648 5.08819C21.6614 5.07914 21.675 5.07023 21.6886 5.06132C21.702 5.05244 21.7155 5.04356 21.7291 5.03486C21.7425 5.02625 21.7561 5.01785 21.7696 5.00937C21.7831 5.00094 21.7966 4.9924 21.8102 4.98413C21.8236 4.97593 21.8372 4.968 21.8507 4.95997C21.8642 4.95194 21.8776 4.94374 21.8912 4.93588C21.9046 4.92812 21.9183 4.92066 21.9318 4.91304C21.9453 4.90541 21.9587 4.89772 21.9723 4.89027C21.9857 4.88288 21.9993 4.87576 22.0128 4.86851C22.0263 4.86129 22.0398 4.85404 22.0534 4.84696L22.0939 4.82619L22.1345 4.80589C22.1479 4.79925 22.1614 4.79254 22.175 4.78603C22.1885 4.77956 22.202 4.77329 22.2156 4.76696C22.2291 4.76065 22.2425 4.75415 22.2561 4.74798C22.2695 4.74185 22.2831 4.73609 22.2966 4.73012C22.3101 4.72416 22.3236 4.71809 22.3372 4.71226C22.3506 4.7065 22.3641 4.70091 22.3777 4.69529L22.4182 4.67862C22.4317 4.67316 22.4452 4.6677 22.4588 4.66238C22.4722 4.6571 22.4858 4.65202 22.4993 4.64686C22.5128 4.64171 22.5262 4.63639 22.5398 4.63138C22.5533 4.62643 22.5669 4.62182 22.5804 4.61701C22.5939 4.6122 22.6073 4.60732 22.6209 4.60264C22.6344 4.59797 22.6479 4.59349 22.6615 4.58899L22.702 4.57574C22.7155 4.5714 22.729 4.56696 22.7425 4.56273C22.756 4.55856 22.7696 4.55463 22.7831 4.5506C22.7966 4.54653 22.8101 4.54236 22.8236 4.53843C22.8371 4.53453 22.8506 4.53091 22.8641 4.52715L22.9047 4.51607C22.9182 4.51247 22.9317 4.50881 22.9452 4.50536C22.9587 4.50194 22.9722 4.49872 22.9857 4.4954C22.9992 4.49208 23.0127 4.48862 23.0263 4.48543C23.0398 4.48228 23.0533 4.47933 23.0668 4.47632L23.1074 4.46744C23.1209 4.46456 23.1343 4.46158 23.1479 4.45883C23.1614 4.45609 23.1749 4.45361 23.1885 4.45101C23.202 4.4484 23.2154 4.44565 23.229 4.44318L23.2695 4.43613L23.3101 4.42939C23.3236 4.42718 23.337 4.42485 23.3506 4.42278C23.3641 4.42071 23.3776 4.41898 23.3911 4.41705C23.4046 4.41515 23.4181 4.41316 23.4317 4.41136L23.4722 4.40624L23.5127 4.40157C23.5263 4.40004 23.5397 4.39828 23.5533 4.39689C23.5668 4.3955 23.5803 4.39448 23.5938 4.39323L23.6344 4.38964C23.6479 4.38849 23.6614 4.38737 23.6749 4.38639C23.6884 4.3854 23.7019 4.38462 23.7154 4.38378C23.7289 4.38293 23.7424 4.38195 23.756 4.3812C23.7695 4.38046 23.783 4.37995 23.7965 4.37934L23.837 4.37778C23.8506 4.3773 23.864 4.37659 23.8776 4.37625C23.8911 4.37591 23.9046 4.37595 23.9181 4.37575C23.9317 4.37554 23.9452 4.3753 23.9587 4.37524L23.9766 4.375H23.9992H24.0398H24.0803H24.1208H24.1614H24.2019H24.2424H24.283H24.3235H24.364H24.4046H24.4451H24.4856H24.5262H24.5667H24.6073H24.6478H24.6884H24.7289H24.7694H24.81H24.8505H24.891H24.9316H24.9721H25.0127H25.0532H25.0937H25.1343H25.1748H25.2153H25.2559H25.2964H25.3369H25.3775H25.418H25.4585H25.4991H25.5396H25.5802H25.6207H25.6613H25.7018H25.7423H25.7829H25.8234H25.8639H25.9045H25.945H25.9855H26.0261H26.0666H26.1072H26.1477H26.1882H26.2288H26.2693H26.3099H26.3504H26.3909H26.4315H26.472H26.5125H26.5531H26.5936H26.6342H26.6747H26.7152H26.7558H26.7963H26.8368H26.8774H26.9179H26.9584H26.999H27.0395H27.0801H27.1206H27.1612H27.2017H27.2422H27.2828H27.3233H27.3638H27.4044H27.4449H27.4854H27.526H27.5665H27.6071H27.6476H27.6882H27.7287H27.7692H27.8098H27.8503H27.8908H27.9314H27.9719H28.0124H28.053H28.0935H28.1341H28.1746H28.2151H28.2557H28.2962H28.3367H28.3773H28.4178H28.4583H28.4989H28.5394H28.58H28.6205H28.661H28.7016H28.7421H28.7827H28.8232H28.8637H28.9043H28.9448H28.9853H29.0259H29.0664H29.107H29.1475H29.1881H29.2286H29.2691H29.3097H29.3502H29.3907H29.4313H29.4718H29.5123H29.5529H29.5934H29.6339H29.6745H29.715H29.7556H29.7961H29.8366H29.8772H29.9177H29.9582H29.9988H30.0393H30.0799H30.1204H30.1548V25.5542C30.1548 25.7384 30.1431 25.9197 30.1204 26.0977C30.1095 26.1834 30.0958 26.2682 30.0799 26.3522C30.0678 26.4161 30.0543 26.4796 30.0393 26.5425C30.0267 26.5954 30.0133 26.6481 29.9988 26.7003C29.986 26.746 29.9725 26.7915 29.9582 26.8366C29.9454 26.8775 29.9318 26.918 29.9177 26.9583C29.9047 26.9956 29.8912 27.0327 29.8772 27.0696C29.8641 27.1039 29.8506 27.1381 29.8366 27.172C29.8235 27.204 29.81 27.2358 29.7961 27.2674C29.7829 27.2973 29.7694 27.3271 29.7556 27.3567C29.7423 27.3849 29.7288 27.413 29.715 27.4409C29.7018 27.4676 29.6883 27.4942 29.6745 27.5207C29.6612 27.5461 29.6477 27.5715 29.6339 27.5966C29.6206 27.621 29.6072 27.6452 29.5934 27.6692C29.5801 27.6924 29.5666 27.7154 29.5529 27.7382C29.5396 27.7604 29.526 27.7824 29.5123 27.8043C29.499 27.8256 29.4855 27.8469 29.4718 27.868C29.4584 27.8886 29.445 27.9093 29.4313 27.9296C29.4179 27.9494 29.4044 27.969 29.3907 27.9885C29.3774 28.0076 29.3638 28.0265 29.3502 28.0453C29.3368 28.0639 29.3233 28.0824 29.3097 28.1007C29.2963 28.1185 29.2828 28.1362 29.2691 28.1539C29.2558 28.1712 29.2422 28.1883 29.2286 28.2053C29.2151 28.2222 29.2017 28.239 29.1881 28.2556C29.1747 28.2718 29.1611 28.2879 29.1475 28.3039C29.1341 28.3197 29.1206 28.3353 29.107 28.3509C29.0935 28.3663 29.0801 28.3817 29.0664 28.3969C29.0531 28.4118 29.0394 28.4264 29.0259 28.441C29.0124 28.4556 28.999 28.4702 28.9853 28.4845C28.9719 28.4986 28.9584 28.5124 28.9448 28.5263C28.9314 28.54 28.9179 28.5536 28.9043 28.5672C28.8909 28.5806 28.8774 28.5939 28.8637 28.6071C28.8503 28.62 28.8368 28.6329 28.8232 28.6457C28.8097 28.6584 28.7963 28.6712 28.7827 28.6837C28.7693 28.696 28.7557 28.7081 28.7421 28.7202C28.7286 28.7323 28.7152 28.7445 28.7016 28.7564C28.6882 28.7681 28.6746 28.7795 28.661 28.791C28.6475 28.8025 28.6341 28.814 28.6205 28.8253C28.6071 28.8364 28.5935 28.8472 28.58 28.8582C28.5665 28.8691 28.5531 28.8801 28.5394 28.8909C28.526 28.9014 28.5124 28.9117 28.4989 28.9221C28.4854 28.9325 28.472 28.9429 28.4583 28.9532C28.445 28.9632 28.4314 28.9731 28.4178 28.983C28.4043 28.9928 28.3909 29.0028 28.3773 29.0125C28.3639 29.0221 28.3503 29.0314 28.3367 29.0408C28.3232 29.0502 28.3098 29.0597 28.2962 29.069C28.2828 29.0781 28.2692 29.0869 28.2557 29.0959C28.2422 29.1048 28.2288 29.1139 28.2151 29.1227C28.2017 29.1314 28.1881 29.1397 28.1746 29.1482C28.1611 29.1567 28.1477 29.1653 28.1341 29.1737C28.1206 29.1819 28.107 29.1899 28.0935 29.198C28.08 29.206 28.0666 29.2142 28.053 29.2221C28.0395 29.2299 28.026 29.2376 28.0124 29.2453L27.9719 29.2681L27.9314 29.2902L27.8908 29.3118C27.8773 29.3188 27.8639 29.326 27.8503 29.3329C27.8368 29.3398 27.8233 29.3465 27.8098 29.3532C27.7963 29.3599 27.7828 29.3668 27.7692 29.3734C27.7558 29.3799 27.7422 29.3861 27.7287 29.3925C27.7152 29.3988 27.7017 29.4053 27.6882 29.4115C27.6747 29.4177 27.6611 29.4236 27.6476 29.4297L27.6071 29.4475L27.5665 29.4648L27.526 29.4815C27.5125 29.487 27.499 29.4927 27.4854 29.4981C27.472 29.5034 27.4584 29.5084 27.4449 29.5135C27.4314 29.5187 27.418 29.524 27.4044 29.5291C27.3909 29.5341 27.3774 29.5389 27.3638 29.5437L27.3233 29.5581L27.2828 29.5721C27.2693 29.5766 27.2557 29.5809 27.2422 29.5853C27.2287 29.5898 27.2152 29.5944 27.2017 29.5986C27.1883 29.6028 27.1747 29.6067 27.1612 29.6108L27.1206 29.623L27.0801 29.6346C27.0666 29.6384 27.0531 29.642 27.0395 29.6456C27.026 29.6493 27.0126 29.6532 26.999 29.6567C26.9855 29.6602 26.972 29.6633 26.9584 29.6666C26.945 29.67 26.9315 29.6734 26.9179 29.6766L26.8774 29.686L26.8368 29.6949C26.8233 29.6978 26.8099 29.701 26.7963 29.7038C26.7829 29.7066 26.7693 29.709 26.7558 29.7116L26.7152 29.7194L26.6747 29.7268C26.6612 29.7292 26.6477 29.7313 26.6342 29.7336C26.6206 29.7358 26.6072 29.7382 26.5936 29.7403C26.5802 29.7424 26.5666 29.7442 26.5531 29.7462L26.5125 29.7519C26.499 29.7537 26.4855 29.7556 26.472 29.7573C26.4585 29.759 26.445 29.7604 26.4315 29.762C26.418 29.7635 26.4045 29.7652 26.3909 29.7666C26.3775 29.7681 26.3639 29.7693 26.3504 29.7706L26.3099 29.7742C26.2963 29.7754 26.2829 29.7768 26.2693 29.7778C26.2559 29.7788 26.2423 29.7795 26.2288 29.7804L26.1882 29.783L26.1477 29.7851C26.1342 29.7858 26.1207 29.7862 26.1072 29.7867C26.0936 29.7872 26.0802 29.7878 26.0666 29.7882C26.0532 29.7886 26.0396 29.7887 26.0261 29.789L25.9855 29.7895C25.9729 29.7896 25.9604 29.79 25.9477 29.79H25.945H25.9045H25.8639H25.8234H25.7829H25.7423H25.7018H25.6613H25.6207H25.5802H25.5396H25.4991H25.4585H25.418H25.3775H25.3369H25.2964H25.2559H25.2153H25.1748H25.1343H25.0937H25.0532H25.0127H24.9721H24.9316H24.891H24.8505H24.81H24.7694H24.7289H24.6884H24.6478H24.6073H24.5667H24.5262H24.4856H24.4451H24.4046H24.364H24.3235H24.283H24.2424H24.2019H24.1614H24.1208H24.0803H24.0398H23.9992H23.9587H23.9181H23.8776H23.837H23.7965H23.756H23.7154H23.6749H23.6344H23.5938H23.5533H23.5127H23.4722H23.4317H23.3911H23.3506H23.3101H23.2695H23.229H23.1885H23.1479H23.1074H23.0668H23.0263H22.9857H22.9452H22.9047H22.8641H22.8236H22.7831H22.7425H22.702H22.6615H22.6209H22.5804H22.5398H22.4993H22.4588H22.4182H22.3777H22.3372H22.2966H22.2561H22.2156H22.175H22.1345H22.0939H22.0534H22.0128H21.9723H21.9318H21.8912H21.8507H21.8102H21.7696H21.7291H21.6886H21.648H21.6075H21.5669H21.5264H21.4858H21.4453H21.4048H21.3642H21.3237H21.2832H21.2426H21.2021H21.1616H21.121H21.0805H21.0399H20.9994H20.9589H20.9183H20.8778H20.8373H20.7968H20.7562H20.7157H20.6751H20.6346H20.5941H20.5535H20.513H20.4725H20.4319H20.3914H20.3508H20.3103H20.2698H20.2292H20.1887H20.1481H20.1076H20.0671H20.0265H19.986H19.9455H19.9049H19.8644H19.7773V20.01C19.8057 20.0345 19.8348 20.0588 19.8644 20.0828C19.8778 20.0937 19.8913 20.1046 19.9049 20.1153C19.9183 20.126 19.9318 20.1366 19.9455 20.1471C19.9589 20.1575 19.9723 20.1679 19.986 20.1782C19.9994 20.1882 20.013 20.1982 20.0265 20.2081C20.0399 20.218 20.0535 20.2277 20.0671 20.2375C20.0805 20.2471 20.094 20.2566 20.1076 20.2662L20.1481 20.2941C20.1615 20.3032 20.1751 20.3123 20.1887 20.3213L20.2292 20.3479C20.2426 20.3567 20.2561 20.3654 20.2698 20.374C20.2831 20.3825 20.2967 20.3909 20.3103 20.3993L20.3508 20.4241C20.3643 20.4323 20.3778 20.4404 20.3914 20.4484C20.4048 20.4564 20.4183 20.4642 20.4319 20.472L20.4725 20.4952L20.513 20.5178L20.5535 20.5399C20.567 20.5471 20.5804 20.5545 20.5941 20.5617C20.6074 20.5687 20.621 20.5757 20.6346 20.5827L20.6751 20.6034L20.7157 20.6237L20.7562 20.6434C20.7697 20.6499 20.7831 20.6565 20.7968 20.6629L20.8373 20.6818L20.8778 20.7004L20.9183 20.7185L20.9589 20.7363L20.9994 20.7538L21.0399 20.7708L21.0805 20.7875L21.121 20.8038L21.1616 20.8199L21.2021 20.8354C21.2156 20.8406 21.229 20.8458 21.2426 20.8509L21.2832 20.8657C21.2967 20.8707 21.3101 20.8757 21.3237 20.8806C21.3371 20.8854 21.3507 20.8901 21.3642 20.8948L21.4048 20.909L21.4453 20.9227C21.4588 20.9272 21.4722 20.9319 21.4858 20.9364C21.4992 20.9408 21.5129 20.9451 21.5264 20.9494L21.5669 20.9625L21.6075 20.9751L21.648 20.9876L21.6886 20.9996L21.7291 21.0115L21.7696 21.0231L21.8102 21.0345L21.8507 21.0456L21.8912 21.0565L21.9318 21.0672L21.9723 21.0776L22.0128 21.0879L22.0534 21.0978L22.0939 21.1077L22.1345 21.1171L22.175 21.1264L22.2156 21.1355L22.2561 21.1444L22.2966 21.1532L22.3372 21.1616L22.3777 21.17L22.4182 21.178L22.4588 21.186L22.4993 21.1937L22.5398 21.2012L22.5804 21.2087L22.6209 21.2158L22.6615 21.2228L22.702 21.2296L22.7425 21.2363L22.7831 21.2429L22.8236 21.2491L22.8641 21.2553L22.9047 21.2613L22.9452 21.2671L22.9857 21.2728L23.0263 21.2782L23.0668 21.2836L23.1074 21.2888L23.1479 21.2938L23.1885 21.2988L23.229 21.3035L23.2695 21.3081L23.3101 21.3126L23.3506 21.3168L23.3911 21.321L23.4317 21.3251L23.4722 21.3289L23.5127 21.3328L23.5533 21.3363L23.5938 21.3398L23.6344 21.3433L23.6749 21.3464L23.7154 21.3495L23.756 21.3526L23.7965 21.3554L23.837 21.3581L23.8776 21.3608L23.9181 21.3632L23.9587 21.3656L23.9992 21.3679L24.0398 21.3699L24.0803 21.3719L24.1208 21.3739L24.1614 21.3756L24.2019 21.3773L24.2424 21.3789L24.283 21.3803L24.3235 21.3816L24.364 21.3829L24.4046 21.384L24.4451 21.385L24.4856 21.386L24.5262 21.3867L24.5667 21.3874L24.6073 21.3881L24.6478 21.3885L24.6884 21.3889L24.7289 21.3892L24.7694 21.3894L24.81 21.3895L24.8505 21.3895L24.891 21.3894L24.9316 21.3892L24.9721 21.389L25.0127 21.3886L25.0532 21.3881L25.0937 21.3876L25.1343 21.387L25.1748 21.3862L25.2153 21.3853L25.2559 21.3845L25.2964 21.3834L25.3369 21.3823L25.3775 21.3812L25.418 21.38L25.4585 21.3785L25.4991 21.3771L25.5396 21.3757L25.5802 21.374L25.6207 21.3723L25.6613 21.3706L25.6811 21.3698L25.7018 21.3688L25.7423 21.3668L25.7829 21.3646L25.8234 21.3624L25.8639 21.3599L25.9045 21.3574L25.945 21.3547L25.9855 21.3519L26.0261 21.3489L26.0666 21.3458L26.1072 21.3426L26.1477 21.3393L26.1882 21.3358L26.2288 21.3322L26.2693 21.3285L26.3099 21.3246L26.3504 21.3207L26.3909 21.3166L26.4315 21.3125L26.472 21.3082L26.5125 21.3038L26.5531 21.2993L26.5936 21.2947L26.6342 21.29L26.6747 21.2852L26.7152 21.2803L26.7558 21.2753L26.7963 21.2701L26.8368 21.2649L26.8774 21.2597L26.9179 21.2542L26.9584 21.2487L26.999 21.2431L27.0395 21.2374L27.0801 21.2317L27.1206 21.2258L27.1612 21.2198L27.2017 21.2137L27.2422 21.2076L27.2828 21.2014L27.3233 21.1951L27.3638 21.1887L27.4044 21.1822L27.4449 21.1756L27.4854 21.169L27.526 21.1622L27.5665 21.1554L27.6071 21.1485L27.6476 21.1415L27.6882 21.1344L27.7287 21.1273L27.7692 21.12L27.8098 21.1127L27.8503 21.1053L27.8908 21.0979L27.9314 21.0903L27.9719 21.0826L28.0124 21.0749L28.053 21.0671L28.0935 21.0593L28.1341 21.0513L28.1746 21.0433L28.2151 21.0352L28.2557 21.027L28.2962 21.0187L28.3367 21.0103L28.3773 21.0019L28.4178 20.9934L28.4583 20.9848L28.4989 20.976L28.5394 20.9673L28.58 20.9584L28.6205 20.9495L28.661 20.9404L28.7016 20.9313L28.7421 20.9221L28.7827 20.9127L28.8232 20.9033L28.8637 20.8937L28.9043 20.884L28.9448 20.8741L28.9851 20.8641V19.0723L28.9448 19.0925L28.9043 19.1127L28.8637 19.1326L28.8232 19.1524L28.7827 19.1721L28.7421 19.1915L28.7016 19.2107L28.661 19.2297L28.6205 19.2485L28.58 19.2672L28.5394 19.2856L28.4989 19.3038L28.4583 19.3217L28.4178 19.3395L28.3773 19.3571L28.3367 19.3744L28.2962 19.3916L28.2557 19.4085L28.2151 19.4252L28.1746 19.4417L28.1341 19.4579L28.0935 19.474L28.053 19.4897L28.0124 19.5053L27.9719 19.5207L27.9314 19.5358L27.8908 19.5506L27.8503 19.5653L27.8098 19.5797L27.7692 19.5939L27.7287 19.6079L27.6882 19.6216L27.6476 19.6351L27.6071 19.6484L27.5665 19.6614L27.526 19.6742L27.4854 19.6868L27.4449 19.6991L27.4044 19.7112L27.3638 19.7231L27.3233 19.7347L27.2828 19.7461L27.2422 19.7573L27.2017 19.7682L27.1612 19.7789L27.1206 19.7894L27.0801 19.7996L27.0395 19.8096L26.999 19.8195L26.9584 19.829L26.9179 19.8383L26.8774 19.8474L26.8368 19.8563L26.7963 19.8649L26.7558 19.8733L26.7152 19.8815L26.6747 19.8895L26.6342 19.8972L26.5936 19.9047L26.5531 19.912L26.5125 19.919L26.472 19.9258L26.4315 19.9324L26.3909 19.9389L26.3504 19.945L26.3099 19.9509L26.2693 19.9567L26.2288 19.9622L26.1882 19.9674L26.1477 19.9725L26.1072 19.9774L26.0666 19.982L26.0261 19.9864L25.9855 19.9906L25.945 19.9946L25.9045 19.9984L25.8639 20.002L25.8234 20.0053L25.7988 20.0072L25.7829 20.0083L25.7423 20.0111L25.7018 20.0138L25.6613 20.016L25.6207 20.0181L25.5802 20.02L25.5396 20.0213L25.4991 20.0226L25.4585 20.0235L25.418 20.0241L25.3775 20.0246C25.3639 20.0247 25.3505 20.0245 25.3369 20.0245L25.2964 20.0243L25.2559 20.0237L25.2153 20.0228L25.1748 20.0218C25.1612 20.0213 25.1478 20.0207 25.1343 20.0201C25.1208 20.0196 25.1072 20.0191 25.0937 20.0185L25.0532 20.0162L25.0127 20.0138L24.9721 20.0109L24.9316 20.0077C24.9181 20.0066 24.9045 20.0056 24.891 20.0044C24.8774 20.0031 24.864 20.0016 24.8505 20.0003L24.81 19.9962L24.7694 19.9914L24.7289 19.9865L24.6884 19.9811L24.6478 19.9753C24.6343 19.9733 24.6207 19.9714 24.6073 19.9692C24.5937 19.9671 24.5802 19.9648 24.5667 19.9625C24.5532 19.9602 24.5396 19.9581 24.5262 19.9557C24.5126 19.9533 24.4992 19.9506 24.4856 19.9481C24.4721 19.9455 24.4585 19.9431 24.4451 19.9404C24.4315 19.9377 24.4181 19.9347 24.4046 19.9319C24.3911 19.929 24.3775 19.9263 24.364 19.9233C24.3504 19.9203 24.337 19.9171 24.3235 19.9139L24.283 19.9043C24.2694 19.901 24.2559 19.8975 24.2424 19.894C24.2289 19.8905 24.2153 19.8871 24.2019 19.8835C24.1883 19.8798 24.1748 19.876 24.1614 19.8722C24.1478 19.8684 24.1342 19.8645 24.1208 19.8605C24.1072 19.8565 24.0937 19.8523 24.0803 19.8482C24.0667 19.844 24.0532 19.8398 24.0398 19.8354C24.0262 19.831 24.0127 19.8265 23.9992 19.8219C23.9857 19.8174 23.9721 19.8128 23.9587 19.8081C23.9451 19.8033 23.9316 19.7983 23.9181 19.7934C23.9046 19.7884 23.891 19.7835 23.8776 19.7784C23.864 19.7731 23.8505 19.7677 23.837 19.7623C23.8235 19.7569 23.8099 19.7516 23.7965 19.7461C23.7829 19.7404 23.7695 19.7345 23.756 19.7287C23.7424 19.7228 23.7288 19.717 23.7154 19.711C23.7018 19.7049 23.6884 19.6985 23.6749 19.6922C23.6613 19.6858 23.6478 19.6794 23.6344 19.6729C23.6208 19.6663 23.6072 19.6595 23.5938 19.6527C23.5802 19.6458 23.5667 19.6388 23.5533 19.6317C23.5397 19.6246 23.5261 19.6175 23.5127 19.6101C23.4991 19.6027 23.4857 19.5949 23.4722 19.5872C23.4587 19.5795 23.4451 19.5718 23.4317 19.5638C23.418 19.5557 23.4046 19.5474 23.3911 19.5392C23.3775 19.5307 23.364 19.5221 23.3506 19.5135C23.337 19.5048 23.3234 19.4962 23.3101 19.4872C23.2964 19.4781 23.283 19.4686 23.2695 19.4592C23.2559 19.4498 23.2424 19.4401 23.229 19.4304C23.2154 19.4206 23.2018 19.4107 23.1885 19.4006C23.1748 19.3902 23.1613 19.3796 23.1479 19.369C23.1343 19.3582 23.1208 19.3474 23.1074 19.3364C23.0938 19.3252 23.0802 19.314 23.0668 19.3025C23.0531 19.2907 23.0397 19.2787 23.0263 19.2666C23.0126 19.2543 22.9991 19.2419 22.9857 19.2293C22.9721 19.2164 22.9585 19.2035 22.9452 19.1904C22.9316 19.1769 22.918 19.1635 22.9047 19.1498C22.8909 19.1356 22.8775 19.1212 22.8641 19.1067C22.8504 19.0918 22.8369 19.0767 22.8236 19.0615C22.8099 19.0458 22.7964 19.03 22.7831 19.014C22.7694 18.9976 22.7558 18.9809 22.7425 18.9641C22.7288 18.9467 22.7152 18.929 22.702 18.9112C22.6883 18.8928 22.6747 18.8742 22.6615 18.8553C22.6477 18.8357 22.6341 18.8159 22.6209 18.7958C22.6071 18.7748 22.5936 18.7536 22.5804 18.7321C22.5665 18.7096 22.553 18.6868 22.5398 18.6638C22.5259 18.6396 22.5125 18.615 22.4993 18.5902C22.4853 18.5638 22.4719 18.5371 22.4588 18.5101C22.4448 18.4814 22.4313 18.4523 22.4182 18.4228C22.4042 18.3911 22.3906 18.3592 22.3777 18.3266C22.3634 18.2907 22.3501 18.254 22.3372 18.217C22.3228 18.176 22.3093 18.1344 22.2966 18.0921C22.282 18.0431 22.2684 17.9933 22.2561 17.9426C22.2407 17.8795 22.2273 17.8151 22.2156 17.7495C22.1981 17.6522 22.1845 17.5523 22.175 17.4496C22.1641 17.331 22.1585 17.2089 22.1585 17.0832C22.1585 16.9574 22.1641 16.8352 22.175 16.7165C22.1845 16.6138 22.1981 16.5138 22.2156 16.4165C22.2273 16.3509 22.2407 16.2864 22.2561 16.2233C22.2684 16.1726 22.282 16.1228 22.2966 16.0737C22.3093 16.0314 22.3228 15.9898 22.3372 15.9487C22.3501 15.9117 22.3634 15.8751 22.3777 15.8391C22.3906 15.8065 22.4042 15.7745 22.4182 15.7428C22.4313 15.7133 22.4448 15.6842 22.4588 15.6555C22.4719 15.6285 22.4853 15.6017 22.4993 15.5754C22.5125 15.5506 22.5259 15.526 22.5398 15.5017C22.553 15.4787 22.5665 15.4559 22.5804 15.4334C22.5936 15.4119 22.6071 15.3907 22.6209 15.3697C22.6341 15.3496 22.6477 15.3298 22.6615 15.3101C22.6747 15.2912 22.6883 15.2726 22.702 15.2542C22.7152 15.2363 22.7288 15.2188 22.7425 15.2013C22.7558 15.1845 22.7694 15.1678 22.7831 15.1514C22.7964 15.1353 22.8099 15.1195 22.8236 15.1039C22.8369 15.0886 22.8504 15.0735 22.8641 15.0586C22.8775 15.0441 22.8909 15.0296 22.9047 15.0155C22.918 15.0018 22.9316 14.9883 22.9452 14.9749C22.9585 14.9618 22.9721 14.9488 22.9857 14.936C22.9991 14.9234 23.0126 14.9109 23.0263 14.8986C23.0397 14.8865 23.0531 14.8745 23.0668 14.8627C23.0802 14.8512 23.0938 14.8401 23.1074 14.8288C23.1208 14.8178 23.1343 14.8069 23.1479 14.7962C23.1613 14.7855 23.1748 14.7749 23.1885 14.7646C23.2018 14.7545 23.2154 14.7446 23.229 14.7347C23.2424 14.725 23.2559 14.7154 23.2695 14.7059C23.283 14.6965 23.2964 14.6871 23.3101 14.6779C23.3234 14.669 23.337 14.6603 23.3506 14.6516C23.364 14.6429 23.3775 14.6344 23.3911 14.626C23.4046 14.6177 23.418 14.6094 23.4317 14.6013C23.4451 14.5934 23.4587 14.5856 23.4722 14.5779C23.4857 14.5702 23.4991 14.5625 23.5127 14.555C23.5261 14.5476 23.5397 14.5405 23.5533 14.5334C23.5667 14.5263 23.5802 14.5193 23.5938 14.5123C23.6072 14.5055 23.6208 14.4988 23.6344 14.4921C23.6478 14.4856 23.6613 14.4792 23.6749 14.4729C23.6884 14.4666 23.7018 14.4602 23.7154 14.4541C23.7288 14.448 23.7424 14.4422 23.756 14.4364C23.7695 14.4305 23.7829 14.4246 23.7965 14.419C23.8099 14.4134 23.8235 14.4081 23.837 14.4027C23.8505 14.3973 23.864 14.3919 23.8776 14.3867C23.891 14.3815 23.9046 14.3766 23.9181 14.3717C23.9316 14.3667 23.9451 14.3617 23.9587 14.3569C23.9721 14.3522 23.9857 14.3476 23.9992 14.3431C24.0127 14.3385 24.0262 14.334 24.0398 14.3296C24.0532 14.3252 24.0667 14.321 24.0803 14.3168C24.0937 14.3127 24.1072 14.3085 24.1208 14.3045L24.1614 14.2928C24.1748 14.289 24.1883 14.2852 24.2019 14.2815C24.2153 14.2779 24.2289 14.2744 24.2424 14.2709C24.2559 14.2675 24.2694 14.264 24.283 14.2606L24.3235 14.2511C24.337 14.2479 24.3504 14.2447 24.364 14.2417C24.3775 14.2387 24.3911 14.2359 24.4046 14.2331C24.4181 14.2302 24.4315 14.2272 24.4451 14.2245C24.4585 14.2218 24.4721 14.2194 24.4856 14.2169C24.4992 14.2144 24.5126 14.2117 24.5262 14.2092C24.5396 14.2068 24.5532 14.2047 24.5667 14.2025C24.5802 14.2002 24.5937 14.1979 24.6073 14.1957L24.6478 14.1897L24.6884 14.1838L24.7289 14.1784L24.7694 14.1735L24.81 14.1687L24.8505 14.1646C24.864 14.1633 24.8774 14.1618 24.891 14.1605C24.9045 14.1593 24.9181 14.1583 24.9316 14.1572L24.9721 14.154L25.0127 14.1512L25.0532 14.1487L25.0937 14.1464C25.1072 14.1458 25.1208 14.1453 25.1343 14.1448C25.1478 14.1442 25.1612 14.1436 25.1748 14.1431L25.2153 14.1421L25.2559 14.1412L25.2964 14.1406L25.3369 14.1405C25.3505 14.1404 25.3639 14.1402 25.3775 14.1403L25.418 14.1408L25.4585 14.1414L25.4991 14.1423L25.5396 14.1436L25.5802 14.145L25.6207 14.1469L25.6613 14.1489L25.7018 14.1511L25.7423 14.1538L25.7829 14.1566L25.7988 14.1577L25.8234 14.1596L25.8639 14.1629L25.9045 14.1664L25.945 14.1701L25.9855 14.174L26.0261 14.1782L26.0666 14.1826L26.1072 14.1871L26.1477 14.1919L26.1882 14.1969L26.2288 14.2021L26.2693 14.2075L26.3099 14.2132L26.3504 14.219L26.3909 14.2251L26.4315 14.2314L26.472 14.238L26.5125 14.2447L26.5531 14.2517L26.5936 14.2589L26.6342 14.2663L26.6747 14.274L26.7152 14.2819L26.7558 14.29L26.7963 14.2983L26.8368 14.3069L26.8774 14.3157L26.9179 14.3247L26.9584 14.3339L26.999 14.3434L27.0395 14.3532L27.0801 14.3631L27.1206 14.3733L27.1612 14.3837L27.2017 14.3944L27.2422 14.4053L27.2828 14.4164L27.3233 14.4278L27.3638 14.4394L27.4044 14.4513L27.4449 14.4634L27.4854 14.4757L27.526 14.4883L27.5665 14.501L27.6071 14.5141L27.6476 14.5274L27.6882 14.5409L27.7287 14.5546L27.7692 14.5686L27.8098 14.5828L27.8503 14.5973L27.8908 14.612L27.9314 14.6269L27.9719 14.6421L28.0124 14.6575L28.053 14.6732L28.0935 14.689L28.1341 14.7051L28.1746 14.7215L28.2151 14.738L28.2557 14.7548L28.2962 14.7719L28.3367 14.7891L28.3773 14.8066L28.4178 14.8243L28.4583 14.8422L28.4989 14.8603L28.5394 14.8787L28.58 14.8973L28.6205 14.916L28.661 14.935L28.7016 14.9542L28.7421 14.9736L28.7827 14.9932L28.8232 15.013L28.8637 15.033L28.9043 15.0531L28.9448 15.0735L28.9851 15.0939V13.3008L28.9448 13.2908L28.9043 13.2809L28.8637 13.2712L28.8232 13.2617L28.7827 13.2522L28.7421 13.2429L28.7016 13.2336L28.661 13.2245L28.6205 13.2155L28.58 13.2065L28.5394 13.1976L28.4989 13.1889L28.4583 13.1802L28.4178 13.1716L28.3773 13.1631L28.3367 13.1546L28.2962 13.1463L28.2557 13.138L28.2151 13.1298L28.1746 13.1217L28.1341 13.1137L28.0935 13.1057L28.053 13.0979L28.0124 13.0901L27.9719 13.0824L27.9314 13.0747L27.8908 13.0672L27.8503 13.0597L27.8098 13.0523L27.7692 13.045L27.7287 13.0378L27.6882 13.0307L27.6476 13.0236L27.6071 13.0166L27.5665 13.0097L27.526 13.0029L27.4854 12.9962L27.4449 12.9895L27.4044 12.983L27.3638 12.9765L27.3233 12.9701L27.2828 12.9638L27.2422 12.9576L27.2017 12.9514L27.1612 12.9454L27.1206 12.9394L27.0801 12.9336L27.0395 12.9278L26.999 12.9222L26.9584 12.9166L26.9179 12.9111L26.8774 12.9057L26.8368 12.9004L26.7963 12.8952L26.7558 12.8901L26.7152 12.8851L26.6747 12.8802L26.6342 12.8754L26.5936 12.8707L26.5531 12.8661L26.5125 12.8616L26.472 12.8573L26.4315 12.853L26.3909 12.8489L26.3504 12.8448L26.3099 12.8409L26.2693 12.837L26.2288 12.8334L26.1882 12.8298L26.1477 12.8263L26.1072 12.823L26.0666 12.8198L26.0261 12.8168L25.9855 12.8138L25.945 12.811L25.9045 12.8083L25.8639 12.8058L25.8234 12.8034L25.7829 12.8011L25.7423 12.799L25.7018 12.797L25.6811 12.796L25.6613 12.7952L25.6207 12.7935L25.5802 12.7918L25.5396 12.7901L25.4991 12.7887L25.4585 12.7873L25.418 12.7859L25.3775 12.7846L25.3369 12.7835L25.2964 12.7824L25.2559 12.7813L25.2153 12.7805L25.1748 12.7797L25.1343 12.7788L25.0937 12.7783L25.0532 12.7777L25.0127 12.7772L24.9721 12.7769L24.9316 12.7767L24.891 12.7764L24.8505 12.7763L24.81 12.7764L24.7694 12.7764L24.7289 12.7766L24.6884 12.777L24.6478 12.7774L24.6073 12.7778L24.5667 12.7785L24.5262 12.7792L24.4856 12.7799L24.4451 12.7809L24.4046 12.7819L24.364 12.7829L24.3235 12.7842L24.283 12.7856L24.2424 12.787L24.2019 12.7886L24.1614 12.7903L24.1208 12.792L24.0803 12.794L24.0398 12.796L23.9992 12.798L23.9587 12.8003L23.9181 12.8027L23.8776 12.8051L23.837 12.8078L23.7965 12.8105L23.756 12.8133L23.7154 12.8164L23.6749 12.8195L23.6344 12.8227L23.5938 12.8261L23.5533 12.8295L23.5127 12.8332L23.4722 12.837L23.4317 12.8408L23.3911 12.8449L23.3506 12.8491L23.3101 12.8533L23.2695 12.8579L23.229 12.8625L23.1885 12.8672L23.1479 12.8722L23.1074 12.8772L23.0668 12.8824L23.0263 12.8878L22.9857 12.8932L22.9452 12.8989L22.9047 12.9047L22.8641 12.9107L22.8236 12.9169L22.7831 12.9231L22.7425 12.9297L22.702 12.9364L22.6615 12.9432L22.6209 12.9502L22.5804 12.9573L22.5398 12.9648L22.4993 12.9723L22.4588 12.9801L22.4182 12.988L22.3777 12.996L22.3372 13.0045L22.2966 13.0129L22.2561 13.0216L22.2156 13.0306L22.175 13.0396L22.1345 13.049L22.0939 13.0584L22.0534 13.0683L22.0128 13.0782L21.9723 13.0885L21.9318 13.0989L21.8912 13.1096L21.8507 13.1205L21.8102 13.1316L21.7696 13.1431L21.7291 13.1546L21.6886 13.1666L21.648 13.1786L21.6075 13.1911L21.5669 13.2037L21.5264 13.2168C21.5129 13.2212 21.4992 13.2254 21.4858 13.2299L21.4453 13.2435C21.4318 13.2481 21.4182 13.2525 21.4048 13.2572L21.3642 13.2714L21.3237 13.2857C21.3101 13.2905 21.2967 13.2956 21.2832 13.3005C21.2696 13.3055 21.256 13.3104 21.2426 13.3154L21.2021 13.3309C21.1886 13.3361 21.175 13.3412 21.1616 13.3464L21.121 13.3625L21.0805 13.3788L21.0399 13.3956L20.9994 13.4126L20.9589 13.43L20.9183 13.4478L20.8778 13.4659L20.8373 13.4846L20.7968 13.5034C20.7831 13.5099 20.7697 13.5164 20.7562 13.523L20.7157 13.5427L20.6751 13.563L20.6346 13.5837C20.621 13.5908 20.6074 13.5977 20.5941 13.6048C20.5804 13.612 20.567 13.6193 20.5535 13.6266L20.513 13.6487L20.4725 13.6713C20.4588 13.679 20.4454 13.6867 20.4319 13.6945C20.4183 13.7023 20.4048 13.7101 20.3914 13.7181C20.3778 13.7261 20.3643 13.7342 20.3508 13.7424C20.3372 13.7506 20.3237 13.7589 20.3103 13.7672C20.2967 13.7756 20.2831 13.784 20.2698 13.7925C20.2561 13.8012 20.2426 13.8099 20.2292 13.8186L20.1887 13.8453C20.1751 13.8543 20.1615 13.8633 20.1481 13.8724L20.1076 13.9004C20.094 13.91 20.0805 13.9195 20.0671 13.9291C20.0535 13.9389 20.0399 13.9487 20.0265 13.9585C20.013 13.9685 19.9994 13.9784 19.986 13.9885C19.9723 13.9988 19.9589 14.0091 19.9455 14.0195C19.9318 14.0301 19.9183 14.0407 19.9049 14.0513C19.8913 14.0621 19.8778 14.0729 19.8644 14.0839V14.0838Z",fill:"#E10238"})),p=()=>Object(l.createElement)("svg",{width:"52",height:"35",viewBox:"0 0 52 35",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"0.878906",y:"0.5",width:"50",height:"34",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M44.0545 5.25735L34.3353 5.25488C34.3341 5.25488 34.3328 5.25488 34.3328 5.25488C34.3253 5.25488 34.3179 5.2562 34.3106 5.2562C32.9754 5.29641 31.3124 6.34915 31.0096 7.64726L26.4132 27.6401C26.1104 28.9503 26.9343 30.0165 28.2599 30.0361H38.4703C39.7756 29.9726 41.044 28.932 41.3417 27.6486L45.9382 7.65564C46.2459 6.33208 45.402 5.25735 44.0545 5.25735Z",fill:"#01798A"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M26.4134 27.6401L31.0097 7.64729C31.3126 6.34917 32.9755 5.29643 34.3107 5.25622L30.4464 5.25376L23.484 5.25244C22.1451 5.27936 20.4605 6.33949 20.1577 7.64729L15.5601 27.6401C15.2561 28.9503 16.0813 30.0165 17.4059 30.0361H28.26C26.9345 30.0165 26.1105 28.9503 26.4134 27.6401",fill:"#024381"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M15.5602 27.64L20.1578 7.64714C20.4606 6.33934 22.1452 5.27922 23.4841 5.2523L14.5649 5.25C13.2185 5.25 11.4923 6.32227 11.1846 7.64714L6.58694 27.64C6.55896 27.762 6.54344 27.8815 6.53418 27.9986V28.3695C6.62418 29.3246 7.36619 30.0201 8.43278 30.036H17.406C16.0814 30.0163 15.2562 28.9502 15.5602 27.64Z",fill:"#DD0228"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M23.6716 19.8205H23.8404C23.9955 19.8205 24.0999 19.7693 24.1488 19.668L24.5874 19.0227H25.762L25.5171 19.4472H26.9254L26.7467 20.0975H25.0709C24.8779 20.3829 24.6403 20.5171 24.3547 20.5012H23.4818L23.6716 19.8205H23.6716ZM23.4788 20.7527H26.5643L26.3676 21.4591H25.1268L24.9374 22.1409H26.1449L25.9482 22.8473H24.7407L24.4602 23.8548C24.3908 24.0232 24.4821 24.099 24.7327 24.0818H25.7168L25.5345 24.7382H23.6451C23.287 24.7382 23.1641 24.5368 23.2765 24.1331L23.6351 22.8473H22.8633L23.0593 22.1409H23.8313L24.0205 21.4591H23.2827L23.4788 20.7527H23.4788ZM28.4035 19.018L28.355 19.4315C28.355 19.4315 28.937 19.002 29.4656 19.002H31.4189L30.6719 21.6601C30.61 21.964 30.3443 22.1151 29.8752 22.1151H27.6612L27.1426 23.9817C27.1128 24.0817 27.155 24.133 27.2667 24.133H27.7023L27.5422 24.7124H26.4347C26.0096 24.7124 25.8328 24.5867 25.903 24.3343L27.3684 19.018H28.4035H28.4035ZM30.0576 19.7693H28.3141L28.1056 20.4866C28.1056 20.4866 28.3959 20.2805 28.8811 20.2731C29.365 20.2657 29.9173 20.2731 29.9173 20.2731L30.0576 19.7693ZM29.4261 21.4333C29.555 21.4504 29.6271 21.4003 29.6358 21.282L29.7425 20.9039H27.9964L27.85 21.4333H29.4261ZM28.2483 22.2921H29.2547L29.236 22.7203H29.504C29.6394 22.7203 29.7065 22.6776 29.7065 22.5935L29.7858 22.3166H30.6223L30.5106 22.7203C30.4161 23.057 30.1656 23.2327 29.7586 23.2499H29.2225L29.22 23.9817C29.2101 24.0989 29.318 24.1587 29.54 24.1587H30.0439L29.8813 24.7381H28.6727C28.3339 24.754 28.1678 24.5953 28.1713 24.2587L28.2483 22.2921V22.2921Z",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M16.0529 15.4764C15.9164 16.1339 15.6 16.639 15.1091 16.9976C14.6227 17.3502 13.9954 17.527 13.2273 17.527C12.5044 17.527 11.9745 17.3465 11.6364 16.9841C11.4018 16.7267 11.2852 16.3998 11.2852 16.0045C11.2852 15.8411 11.3051 15.6654 11.3448 15.4764L12.1631 11.5972H13.3991L12.5919 15.4325C12.5671 15.5386 12.5571 15.6374 12.5584 15.7265C12.5571 15.9229 12.6068 16.0839 12.7073 16.2095C12.8537 16.3962 13.0914 16.4889 13.4221 16.4889C13.8024 16.4889 14.1158 16.3974 14.359 16.2132C14.6022 16.0302 14.761 15.7704 14.8324 15.4325L15.6422 11.5972H16.8719L16.0529 15.4764Z",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M21.2436 13.9502H22.2116L21.4534 17.4123H20.4873L21.2436 13.9502ZM21.5482 12.689H22.5248L22.3424 13.5293H21.3659L21.5482 12.689Z",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M23.0688 17.1487C22.8156 16.9109 22.6878 16.59 22.6865 16.1826C22.6865 16.113 22.6908 16.0338 22.7002 15.9471C22.7095 15.8592 22.7214 15.7739 22.738 15.6946C22.8528 15.1323 23.0973 14.6858 23.4739 14.3564C23.8499 14.0258 24.3036 13.8599 24.8347 13.8599C25.2696 13.8599 25.6145 13.9794 25.8672 14.2185C26.1196 14.4589 26.2462 14.7833 26.2462 15.1957C26.2462 15.2664 26.2407 15.3481 26.2313 15.436C26.2201 15.525 26.2066 15.6104 26.1909 15.6946C26.0787 16.2484 25.8349 16.69 25.4583 17.0134C25.0816 17.3391 24.6293 17.5012 24.1019 17.5012C23.6651 17.5012 23.3213 17.3841 23.0688 17.1487M24.9136 16.4631C25.0843 16.2814 25.2065 16.0056 25.2809 15.6385C25.2921 15.5812 25.302 15.5214 25.3082 15.4616C25.3143 15.403 25.3168 15.3482 25.3168 15.2981C25.3168 15.0846 25.2616 14.9188 25.1506 14.8016C25.0402 14.6833 24.8832 14.6248 24.6804 14.6248C24.4122 14.6248 24.1939 14.7174 24.0227 14.9029C23.8501 15.0884 23.7279 15.3689 23.6509 15.7422C23.6404 15.7995 23.6317 15.8569 23.6237 15.913C23.6175 15.9703 23.6157 16.024 23.6168 16.0728C23.6168 16.285 23.6721 16.4485 23.7831 16.5644C23.8935 16.6803 24.0498 16.7376 24.2553 16.7376C24.5246 16.7376 24.743 16.6461 24.9136 16.4631Z",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M32.5262 19.8496L32.7596 19.0421H33.9397L33.8888 19.3385C33.8888 19.3385 34.4918 19.0421 34.9261 19.0421C35.3606 19.0421 36.3854 19.0421 36.3854 19.0421L36.1535 19.8496H35.9239L34.8231 23.6582H35.0527L34.8343 24.4146H34.6047L34.5092 24.7427H33.3664L33.4617 24.4146H31.207L31.4268 23.6582H31.6527L32.7544 19.8496H32.5262H32.5262ZM33.7993 19.8498L33.4989 20.8805C33.4989 20.8805 34.0128 20.6866 34.4558 20.6318C34.5536 20.2718 34.6815 19.8498 34.6815 19.8498H33.7993V19.8498ZM33.3598 21.3637L33.0585 22.4433C33.0585 22.4433 33.628 22.1676 34.0188 22.1444C34.1317 21.7271 34.2447 21.3637 34.2447 21.3637H33.3598V21.3637ZM33.5808 23.6583L33.8067 22.8751H32.9258L32.6987 23.6583H33.5808ZM36.4352 18.9922H37.5447L37.5918 19.3946C37.5844 19.4971 37.6463 19.546 37.7779 19.546H37.9739L37.7756 20.2279H36.9601C36.6487 20.2437 36.4886 20.1267 36.4738 19.8741L36.4352 18.9922ZM36.1102 20.4548H39.7039L39.493 21.1868H38.3488L38.1526 21.8673H39.2957L39.0835 22.5981H37.8104L37.5224 23.0264H38.1455L38.2894 23.8839C38.3066 23.9693 38.3836 24.0108 38.5151 24.0108H38.7086L38.5053 24.717H37.8202C37.4653 24.7342 37.2818 24.6171 37.2667 24.3646L37.1016 23.5814L36.5346 24.4146C36.4005 24.65 36.1945 24.7599 35.9167 24.7427H34.8705L35.074 24.0363H35.4004C35.5345 24.0363 35.646 23.9778 35.7465 23.8595L36.634 22.5981H35.4898L35.7018 21.8673H36.9428L37.1402 21.1868H35.898L36.1102 20.4548Z",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M17.1915 13.9492H18.0645L17.9647 14.4493L18.0899 14.3066C18.3729 14.009 18.7166 13.8613 19.1224 13.8613C19.4898 13.8613 19.7547 13.9663 19.921 14.1773C20.0847 14.3884 20.1294 14.6799 20.0519 15.0544L19.571 17.4137H18.6738L19.1081 15.2752C19.1529 15.0544 19.1405 14.8897 19.0715 14.7836C19.0033 14.6774 18.873 14.625 18.685 14.625C18.4542 14.625 18.26 14.6957 18.1017 14.8361C17.9429 14.9776 17.8381 15.174 17.7865 15.424L17.3863 17.4137H16.4873L17.1915 13.9492Z",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M27.2021 13.9492H28.0758L27.9767 14.4493L28.1006 14.3066C28.3837 14.009 28.7287 13.8613 29.1332 13.8613C29.5005 13.8613 29.766 13.9663 29.931 14.1773C30.0937 14.3884 30.1408 14.6799 30.0614 15.0544L29.5823 17.4137H28.6839L29.1184 15.2752C29.1629 15.0544 29.1506 14.8897 29.0823 14.7836C29.0115 14.6774 28.8836 14.625 28.6964 14.625C28.4655 14.625 28.272 14.6957 28.1119 14.8361C27.953 14.9776 27.8476 15.174 27.798 15.424L27.396 17.4137H26.498L27.2021 13.9492",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M31.5212 11.8018H34.0577C34.5454 11.8018 34.9225 11.9104 35.1818 12.1238C35.44 12.3398 35.5692 12.6497 35.5692 13.0534V13.0656C35.5692 13.1424 35.564 13.229 35.5567 13.3229C35.5441 13.4157 35.5279 13.5095 35.5071 13.6072C35.3954 14.1415 35.1359 14.571 34.7352 14.8967C34.333 15.2211 33.8567 15.3846 33.3082 15.3846H31.9479L31.5274 17.4133H30.3496L31.5212 11.8018M32.1554 14.4087H33.2835C33.5776 14.4087 33.8108 14.3415 33.9809 14.2086C34.1497 14.0744 34.2614 13.8695 34.3234 13.5914C34.3332 13.54 34.3394 13.4937 34.3469 13.451C34.3508 13.4108 34.3556 13.3704 34.3556 13.3315C34.3556 13.1326 34.2838 12.9887 34.1397 12.8984C33.9958 12.8068 33.7701 12.763 33.4572 12.763H32.4991L32.1554 14.4087",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M40.8406 18.0833C40.4683 18.8615 40.1135 19.3152 39.9051 19.5263C39.6964 19.735 39.2833 20.2205 38.2881 20.1839L38.3737 19.5898C39.2112 19.3361 39.6642 18.1929 39.9223 17.6867L39.6146 13.9587L40.2624 13.9502H40.8059L40.8643 16.2888L41.8829 13.9502H42.9143L40.8406 18.0833Z",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M37.9561 14.232L37.5464 14.509C37.1183 14.1796 36.7274 13.9759 35.9731 14.3199C34.9454 14.7883 34.0868 18.381 36.9161 17.1976L37.0774 17.3855L38.1905 17.4135L38.9215 14.1491L37.9561 14.232M37.3233 16.0168C37.1445 16.5353 36.7451 16.8781 36.4324 16.7805C36.1196 16.6853 36.008 16.1851 36.1891 15.6655C36.3678 15.1458 36.7698 14.8042 37.08 14.9018C37.3927 14.997 37.5056 15.4971 37.3233 16.0168Z",fill:"white"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M34.3328 5.26107L30.4463 5.25342L34.3106 5.26981C34.318 5.26981 34.3253 5.26107 34.3328 5.26107",fill:"#E02F41"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M30.4467 5.27406L23.5378 5.25C23.5204 5.25 23.5024 5.25765 23.4844 5.26531L30.4467 5.27406",fill:"#2E4F7D"}));const b=()=>Object(l.createElement)(l.Fragment,null,Object(l.createElement)(t.Text,{as:"h3",variant:"label",weight:"600",size:"12",lineHeight:"16px"},Object(c.__)("Accepted payment methods",'woocommerce')),Object(l.createElement)("div",{className:"woocommerce-task-payment-wcpay__accepted"},Object(l.createElement)(i,null),Object(l.createElement)(o,null),Object(l.createElement)(a,null),Object(l.createElement)(r,null),Object(l.createElement)(m,null),Object(l.createElement)(V,null),Object(l.createElement)(s,null),Object(l.createElement)(p,null),Object(l.createElement)(u,null),Object(l.createElement)(d,null)));var h=({width:C=196,height:e=41})=>Object(l.createElement)("svg",{width:C,height:e,viewBox:`0 0 ${C} ${e}`,fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("title",null,"WooCommerce Payments"),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M6.16119 0H60.1988C63.6186 0 66.387 2.74594 66.387 6.13799V26.598C66.387 29.99 63.6186 32.736 60.1988 32.736H40.8202L43.48 39.197L31.7823 32.736H6.18833C2.76858 32.736 0.000197874 29.99 0.000197874 26.598V6.13799C-0.0269431 2.77286 2.74143 0 6.16119 0Z",fill:"#7F54B3"}),Object(l.createElement)("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M3.88666 5.40393C4.26664 4.89243 4.8366 4.62322 5.59655 4.56938C6.98073 4.46169 7.76782 5.1078 7.95781 6.50769C8.79918 12.1342 9.72197 16.8992 10.699 20.8028L16.6429 9.57669C17.1857 8.55369 17.8643 8.01527 18.6785 7.96143C19.8727 7.88066 20.6055 8.63445 20.904 10.2228C21.5826 13.8033 22.4511 16.8454 23.4824 19.4298C24.1881 12.5918 25.3823 7.6653 27.065 4.62322C27.4722 3.86943 28.0692 3.49254 28.8563 3.4387C29.4806 3.38486 30.0505 3.5733 30.5662 3.97712C31.0819 4.38093 31.3533 4.89243 31.4076 5.51161C31.4347 5.99619 31.3533 6.40001 31.1362 6.80383C30.0777 8.74214 29.2092 11.9996 28.5035 16.5223C27.825 20.9104 27.5807 24.3294 27.7436 26.7792C27.7978 27.4522 27.6893 28.0445 27.4179 28.556C27.0922 29.1483 26.6036 29.4713 25.9794 29.5252C25.2737 29.579 24.5409 29.256 23.8353 28.5291C21.3112 25.9716 19.3027 22.1488 17.8371 17.0607C16.073 20.5066 14.7702 23.091 13.9288 24.814C12.3275 27.8561 10.9705 29.4175 9.83053 29.4982C9.09773 29.5521 8.47349 28.9329 7.93067 27.6407C6.54648 24.114 5.05373 17.303 3.45241 7.20764C3.37099 6.5077 3.50669 5.88851 3.88666 5.40393ZM62.24 9.6307C61.263 7.93467 59.8245 6.91167 57.8975 6.50786C57.3818 6.40017 56.8933 6.34633 56.4319 6.34633C53.8263 6.34633 51.7094 7.69238 50.0537 10.3845C48.6424 12.6728 47.9368 15.2033 47.9368 17.9762C47.9368 20.0491 48.371 21.8259 49.2395 23.3066C50.2166 25.0026 51.6551 26.0256 53.5821 26.4294C54.0978 26.5371 54.5863 26.5909 55.0477 26.5909C57.6804 26.5909 59.7974 25.2449 61.4258 22.5528C62.8371 20.2376 63.5428 17.707 63.5428 14.9341C63.5428 12.8343 63.1086 11.0844 62.24 9.6307ZM58.8203 17.0878C58.4403 18.8646 57.7618 20.1837 56.7576 21.0721C55.9705 21.7721 55.2377 22.0682 54.5592 21.9336C53.9078 21.799 53.365 21.2337 52.9578 20.1837C52.6321 19.3492 52.4693 18.5146 52.4693 17.7339C52.4693 17.0609 52.5236 16.3879 52.6593 15.7687C52.9036 14.6649 53.365 13.5881 54.0978 12.5651C54.9934 11.246 55.9433 10.7075 56.9204 10.896C57.5718 11.0306 58.1146 11.5959 58.5217 12.6458C58.8474 13.4804 59.0103 14.315 59.0103 15.0957C59.0103 15.7956 58.9288 16.4686 58.8203 17.0878ZM40.8794 6.50786C42.7793 6.91167 44.2449 7.93467 45.222 9.6307C46.0905 11.0844 46.5247 12.8343 46.5247 14.9341C46.5247 17.707 45.8191 20.2376 44.4077 22.5528C42.7793 25.2449 40.6623 26.5909 38.0296 26.5909C37.5682 26.5909 37.0797 26.5371 36.564 26.4294C34.637 26.0256 33.1985 25.0026 32.2214 23.3066C31.3529 21.8259 30.9187 20.0491 30.9187 17.9762C30.9187 15.2033 31.6243 12.6728 33.0357 10.3845C34.6913 7.69238 36.8083 6.34633 39.4138 6.34633C39.8752 6.34633 40.3637 6.40017 40.8794 6.50786ZM39.7395 21.0721C40.7437 20.1837 41.4222 18.8646 41.8022 17.0878C41.9379 16.4686 41.9922 15.7956 41.9922 15.0957C41.9922 14.315 41.8293 13.4804 41.5036 12.6458C41.0965 11.5959 40.5537 11.0306 39.9023 10.896C38.9253 10.7075 37.9753 11.246 37.0797 12.5651C36.3469 13.5881 35.8855 14.6649 35.6412 15.7687C35.5055 16.3879 35.4512 17.0609 35.4512 17.7339C35.4512 18.5146 35.6141 19.3492 35.9398 20.1837C36.3469 21.2337 36.8897 21.799 37.5411 21.9336C38.2196 22.0682 38.9524 21.7721 39.7395 21.0721Z",fill:"white"}),Object(l.createElement)("path",{d:"M143.023 29.9316V38.217H144.057V35.26H146.141C147.697 35.26 148.805 34.1633 148.805 32.613C148.805 31.0341 147.72 29.9316 146.153 29.9316H143.023ZM144.057 30.8503H145.883C147.083 30.8503 147.743 31.4762 147.743 32.613C147.743 33.7097 147.06 34.3413 145.883 34.3413H144.057V30.8503Z",fill:"black"}),Object(l.createElement)("path",{d:"M151.866 38.3261C152.693 38.3261 153.37 37.9643 153.772 37.304H153.864V38.217H154.806V33.9796C154.806 32.6934 153.961 31.9183 152.451 31.9183C151.131 31.9183 150.155 32.5728 150.023 33.5662H151.022C151.159 33.0781 151.676 32.7968 152.417 32.7968C153.341 32.7968 153.818 33.2159 153.818 33.9796V34.5423L152.032 34.6514C150.591 34.7375 149.776 35.3748 149.776 36.483C149.776 37.6141 150.666 38.3261 151.866 38.3261ZM152.049 37.4591C151.332 37.4591 150.798 37.0916 150.798 36.46C150.798 35.8399 151.211 35.5126 152.153 35.4495L153.818 35.3404V35.9088C153.818 36.793 153.066 37.4591 152.049 37.4591Z",fill:"black"}),Object(l.createElement)("path",{d:"M156.93 40.4563C158.027 40.4563 158.52 40.0314 159.049 38.5959L161.466 32.0274H160.415L158.721 37.1203H158.63L156.93 32.0274H155.862L158.153 38.2227L158.038 38.5902C157.78 39.3366 157.47 39.6065 156.901 39.6065C156.763 39.6065 156.608 39.6007 156.488 39.5778V40.4218C156.626 40.4448 156.798 40.4563 156.93 40.4563Z",fill:"black"}),Object(l.createElement)("path",{d:"M162.787 38.217H163.774V34.3815C163.774 33.5087 164.4 32.8083 165.21 32.8083C165.99 32.8083 166.501 33.2791 166.501 34.014V38.217H167.489V34.238C167.489 33.4513 168.063 32.8083 168.924 32.8083C169.797 32.8083 170.228 33.2561 170.228 34.1691V38.217H171.215V33.9394C171.215 32.6417 170.509 31.9183 169.246 31.9183C168.39 31.9183 167.684 32.3489 167.351 33.0035H167.259C166.972 32.3604 166.387 31.9183 165.548 31.9183C164.722 31.9183 164.101 32.3145 163.82 33.0035H163.728V32.0274H162.787V38.217Z",fill:"black"}),Object(l.createElement)("path",{d:"M177.118 36.615C176.86 37.1605 176.32 37.4533 175.522 37.4533C174.471 37.4533 173.788 36.6782 173.736 35.4552V35.4093H178.186V35.0303C178.186 33.1068 177.17 31.9183 175.499 31.9183C173.799 31.9183 172.708 33.1815 172.708 35.1279C172.708 37.0859 173.782 38.3261 175.499 38.3261C176.854 38.3261 177.818 37.6715 178.106 36.615H177.118ZM175.487 32.791C176.469 32.791 177.124 33.5145 177.147 34.6112H173.736C173.811 33.5145 174.5 32.791 175.487 32.791Z",fill:"black"}),Object(l.createElement)("path",{d:"M179.736 38.217H180.724V34.5537C180.724 33.4686 181.361 32.8083 182.349 32.8083C183.336 32.8083 183.807 33.3365 183.807 34.4504V38.217H184.795V34.2092C184.795 32.7394 184.02 31.9183 182.63 31.9183C181.683 31.9183 181.08 32.3202 180.77 33.0035H180.678V32.0274H179.736V38.217Z",fill:"black"}),Object(l.createElement)("path",{d:"M187.017 30.4254V32.0274H186.018V32.8542H187.017V36.6093C187.017 37.7921 187.528 38.2629 188.802 38.2629C188.998 38.2629 189.187 38.24 189.382 38.2055V37.3729C189.199 37.3902 189.101 37.3959 188.923 37.3959C188.28 37.3959 188.004 37.0859 188.004 36.3567V32.8542H189.382V32.0274H188.004V30.4254H187.017Z",fill:"black"}),Object(l.createElement)("path",{d:"M190.617 33.7212C190.617 34.6169 191.145 35.1164 192.305 35.3978L193.367 35.6562C194.027 35.8169 194.349 36.104 194.349 36.5289C194.349 37.0973 193.752 37.4935 192.919 37.4935C192.127 37.4935 191.633 37.1605 191.467 36.638H190.45C190.559 37.6658 191.507 38.3261 192.885 38.3261C194.292 38.3261 195.365 37.5624 195.365 36.4543C195.365 35.5643 194.803 35.059 193.637 34.7777L192.684 34.548C191.955 34.37 191.61 34.1059 191.61 33.681C191.61 33.1298 192.184 32.7566 192.919 32.7566C193.666 32.7566 194.148 33.0839 194.28 33.5777H195.256C195.124 32.5614 194.223 31.9183 192.925 31.9183C191.61 31.9183 190.617 32.6934 190.617 33.7212Z",fill:"black"}),Object(l.createElement)("path",{d:"M73.2688 9.52456C71.4503 11.3014 70.5547 13.5627 70.5547 16.3087C70.5547 19.2431 71.4503 21.639 73.2416 23.4427C75.0329 25.2464 77.3671 26.1618 80.2711 26.1618C81.1125 26.1618 82.0625 26.0272 83.0938 25.731V21.3698C82.1439 21.639 81.3296 21.7736 80.624 21.7736C79.1855 21.7736 78.0456 21.289 77.1771 20.3468C76.3086 19.3777 75.8743 18.0854 75.8743 16.4433C75.8743 14.9088 76.3086 13.6435 77.1499 12.6743C78.0185 11.6782 79.0769 11.1937 80.3797 11.1937C81.2211 11.1937 82.1167 11.3283 83.0938 11.5975V7.23628C82.1982 6.99399 81.1939 6.8863 80.1354 6.8863C77.3671 6.85938 75.0872 7.74778 73.2688 9.52456ZM92.1046 6.85938C89.6076 6.85938 87.6535 7.69393 86.2422 9.33611C84.8308 10.9783 84.1523 13.2935 84.1523 16.2548C84.1523 19.4584 84.858 21.9082 86.2422 23.6043C87.6263 25.3003 89.6619 26.1618 92.3217 26.1618C94.9001 26.1618 96.8814 25.3003 98.2656 23.6043C99.6498 21.9082 100.355 19.5123 100.355 16.4433C100.355 13.3743 99.6498 11.0052 98.2384 9.33611C96.8 7.69393 94.7644 6.85938 92.1046 6.85938ZM94.2487 20.8583C93.7602 21.6121 93.0274 21.989 92.1046 21.989C91.2361 21.989 90.5847 21.6121 90.1233 20.8583C89.6619 20.1045 89.4448 18.5969 89.4448 16.3087C89.4448 12.782 90.3404 11.0321 92.1589 11.0321C94.0587 11.0321 95.0358 12.8089 95.0358 16.3894C95.0087 18.597 94.7373 20.1045 94.2487 20.8583ZM113.763 7.37088L112.786 11.4898C112.542 12.5397 112.297 13.6166 112.08 14.7203L111.538 17.5739C111.022 14.7203 110.316 11.3283 109.421 7.37088H103.124L100.763 25.7041H105.485L106.761 13.0781L109.99 25.7041H113.356L116.45 13.1051L117.78 25.7041H122.72L120.223 7.37088H113.763ZM136.371 7.37088L135.394 11.4898C135.15 12.5397 134.906 13.6166 134.689 14.7203L134.146 17.5739C133.63 14.7203 132.925 11.3283 132.029 7.37088H125.732L123.371 25.7041H128.093L129.369 13.0781L132.599 25.7041H135.964L139.031 13.1051L140.361 25.7041H145.301L142.804 7.37088H136.371ZM151.733 18.4623H156.157V14.6665H151.733V11.3013H156.836V7.3978H146.739V25.731H156.863V21.8275H151.733V18.4623ZM170.922 15.5549C171.438 14.7203 171.709 13.8588 171.709 12.9705C171.709 11.2475 171.03 9.87453 169.673 8.87845C168.316 7.88237 166.444 7.37088 164.11 7.37088H158.301V25.7041H163.295V17.3586H163.377L167.421 25.7041H172.686L168.696 17.4393C169.646 17.0086 170.406 16.3894 170.922 15.5549ZM163.268 15.2587V10.8975C164.462 10.9245 165.304 11.1129 165.819 11.4898C166.335 11.8667 166.579 12.4589 166.579 13.3204C166.579 14.5857 165.467 15.2318 163.268 15.2587ZM174.64 9.52456C172.822 11.3014 171.926 13.5627 171.926 16.3087C171.926 19.2431 172.822 21.639 174.613 23.4427C176.404 25.2464 178.738 26.1618 181.643 26.1618C182.484 26.1618 183.434 26.0272 184.465 25.731V21.3698C183.515 21.639 182.701 21.7736 181.995 21.7736C180.557 21.7736 179.417 21.289 178.548 20.3468C177.68 19.3777 177.246 18.0854 177.246 16.4433C177.246 14.9088 177.68 13.6435 178.521 12.6743C179.39 11.6782 180.448 11.1937 181.751 11.1937C182.592 11.1937 183.488 11.3283 184.465 11.5975V7.23628C183.57 6.99399 182.565 6.8863 181.507 6.8863C178.766 6.85938 176.459 7.74778 174.64 9.52456ZM190.843 21.7736V18.4354H195.267V14.6396H190.843V11.2744H195.973V7.37088H185.877V25.7041H196V21.8005H190.843V21.7736Z",fill:"black"}));const f=({logoWidth:C=196,logoHeight:e=41,children:H})=>Object(l.createElement)(L.CardHeader,{as:"h2"},Object(l.createElement)(h,{width:C,height:e}),H),O=({description:C,heading:e,onLinkClick:H=(()=>{})})=>Object(l.createElement)(L.CardBody,null,e&&Object(l.createElement)(t.Text,{as:"h2"},e),Object(l.createElement)(t.Text,{className:"woocommerce-task-payment-wcpay__description",as:"p",lineHeight:"1.5em"},C,Object(l.createElement)("br",null),Object(l.createElement)(n.Link,{target:"_blank",type:"external",rel:"noreferrer",href:"https://woocommerce.com/payments/?utm_medium=product",onClick:H},Object(c.__)("Learn more",'woocommerce'))),Object(l.createElement)(b,null)),j=({children:C})=>Object(l.createElement)(L.CardFooter,null,C),M=({children:C})=>Object(l.createElement)(L.Card,{className:"woocommerce-task-payment-wcpay"},C),E=({isLocalPartner:C=!1})=>{const e=C?Object(c.__)("Local Partner",'woocommerce'):Object(c.__)("Recommended",'woocommerce');return Object(l.createElement)("div",{className:"woocommerce-task-payment__recommended-ribbon"},Object(l.createElement)("span",null,e))};var Z=H(61),w=H.n(Z);const v=()=>Object(l.createElement)("span",{className:"woocommerce-task-payment__setup_required"},Object(l.createElement)(w.a,null),Object(l.createElement)(t.Text,{variant:"small",size:"14",lineHeight:"20px"},Object(c.__)("Setup required",'woocommerce')));var g=()=>Object(l.createElement)("svg",{width:"36",height:"25",viewBox:"0 0 36 25",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(l.createElement)("rect",{x:"1.41431",y:"1",width:"33.7586",height:"23",rx:"3.5",fill:"white",stroke:"#F3F3F3"}),Object(l.createElement)("path",{d:"M17.645 14.9708V12.2642V12.2636H19.1074C19.7104 12.264 20.2171 12.0743 20.6276 11.6946C21.0425 11.3342 21.2745 10.816 21.2627 10.2759C21.2709 9.73881 21.0393 9.22448 20.6276 8.86525C20.2207 8.48344 19.6734 8.27511 19.1074 8.28658H16.7598V14.9708H17.645ZM17.6451 11.4427V9.10938V9.10885H19.1295C19.4604 9.09983 19.7793 9.22898 20.0054 9.46351C20.2328 9.678 20.3611 9.97262 20.3611 10.2803C20.3611 10.588 20.2328 10.8826 20.0054 11.0971C19.7766 11.3267 19.4586 11.4522 19.1295 11.4427H17.6451Z",fill:"#5F6368"}),Object(l.createElement)("path",{d:"M24.8518 10.7568C24.4731 10.4176 23.9567 10.248 23.3024 10.248C22.462 10.248 21.8273 10.5467 21.3985 11.144L22.1781 11.6203C22.4662 11.2157 22.8574 11.0134 23.3519 11.0134C23.6672 11.0098 23.9722 11.1216 24.2063 11.3264C24.4397 11.5136 24.5739 11.7927 24.5719 12.0864V12.2827C24.2318 12.096 23.7989 12.0027 23.2733 12.0027C22.6575 12.0034 22.1652 12.1435 21.7965 12.423C21.4278 12.7024 21.2434 13.0788 21.2434 13.552C21.2354 13.983 21.4281 14.3944 21.7679 14.672C22.1176 14.9707 22.5521 15.12 23.0715 15.12C23.68 15.12 24.1674 14.8587 24.534 14.336H24.5725V14.9707H25.4192V12.152C25.4195 11.5611 25.2304 11.096 24.8518 10.7568ZM22.4508 14.1307C22.2654 14.0011 22.156 13.7924 22.1572 13.5707C22.1572 13.3216 22.2776 13.1142 22.5201 12.9435C22.7602 12.7753 23.06 12.6912 23.4196 12.6912C23.9133 12.6912 24.2982 12.7979 24.5742 13.0112C24.5742 13.3718 24.4276 13.6859 24.1343 13.9536C23.8702 14.2099 23.5122 14.3541 23.1386 14.3547C22.8896 14.3592 22.6466 14.2801 22.4508 14.1307Z",fill:"#5F6368"}),Object(l.createElement)("path",{d:"M30.2792 10.3975L27.3235 16.9868H26.4097L27.5065 14.6812L25.563 10.3975H26.5251L27.9299 13.6828H27.9491L29.3154 10.3975H30.2792Z",fill:"#5F6368"}),Object(l.createElement)("path",{d:"M14.0677 11.6812C14.068 11.4195 14.0452 11.1583 13.9995 10.9004H10.2664V12.3793H12.4045C12.3161 12.8566 12.0305 13.2782 11.6139 13.5463V14.5063H12.89C13.6372 13.838 14.0677 12.8497 14.0677 11.6812Z",fill:"#4285F4"}),Object(l.createElement)("path",{d:"M10.2666 15.4332C11.3349 15.4332 12.2344 15.0929 12.8903 14.5063L11.6142 13.5463C11.259 13.7799 10.8016 13.9132 10.2666 13.9132C9.23409 13.9132 8.35771 13.238 8.04432 12.3281H6.72974V13.3175C7.40168 14.6145 8.77018 15.4331 10.2666 15.4332Z",fill:"#34A853"}),Object(l.createElement)("path",{d:"M8.04421 12.3283C7.87853 11.8516 7.87853 11.3353 8.04421 10.8585V9.86914H6.72962C6.1676 10.954 6.1676 12.2328 6.72962 13.3177L8.04421 12.3283Z",fill:"#FBBC04"}),Object(l.createElement)("path",{d:"M10.2666 9.27318C10.8312 9.26424 11.3766 9.47114 11.7852 9.84918L12.915 8.75318C12.1986 8.10042 11.2495 7.74205 10.2666 7.75318C8.77018 7.75325 7.40168 8.57187 6.72974 9.86892L8.04432 10.8582C8.35771 9.94838 9.23409 9.27318 10.2666 9.27318Z",fill:"#EA4335"}));const R=({id:C,...e})=>Object(l.createElement)(L.Fill,Object.assign({name:"woocommerce_payment_gateway_setup_"+C},e));R.Slot=({id:C,fillProps:e})=>Object(l.createElement)(L.Slot,{name:"woocommerce_payment_gateway_setup_"+C,fillProps:e});const _=({id:C,...e})=>Object(l.createElement)(L.Fill,Object.assign({name:"woocommerce_payment_gateway_configure_"+C},e));_.Slot=({id:C,fillProps:e})=>Object(l.createElement)(L.Slot,{name:"woocommerce_payment_gateway_configure_"+C,fillProps:e});const y=({id:C,...e})=>Object(l.createElement)(L.Fill,Object.assign({name:"woocommerce_onboarding_task_"+C},e));y.Slot=({id:C,fillProps:e})=>Object(l.createElement)(L.Slot,{name:"woocommerce_onboarding_task_"+C,fillProps:e});const k=({id:C,...e})=>Object(l.createElement)(L.Fill,Object.assign({name:"woocommerce_onboarding_task_list_item_"+C},e));k.Slot=({id:C,fillProps:e})=>Object(l.createElement)(L.Slot,{name:"woocommerce_onboarding_task_list_item_"+C,fillProps:e})},507:function(C,e,H){"use strict";H.d(e,"a",(function(){return t}));var L=H(7);function t(C){const{createNotice:e}=Object(L.dispatch)("core/notices");C.error_data&&C.errors&&Object.keys(C.errors).length?Object.keys(C.errors).forEach(H=>{e("error",C.errors[H].join(" "))}):C.message&&e(C.code?"error":"success",C.message)}},514:function(C,e,H){"use strict";var L=H(0),t=H(2),l=H(14),n=H(7),c=H(18),i=H.n(c),o=H(3),a=H(21),r=H(11),d=H(122);class V extends L.Component{constructor(C){super(C),this.state={isLoadingScripts:!1,isRequestStarted:!1}}async componentDidUpdate(C,e){const{hasErrors:H,isRequesting:L,onClose:l,onContinue:n,createNotice:c}=this.props,{isLoadingScripts:i,isRequestStarted:o}=this.state;if(!o)return;const a=!L&&!i&&(C.isRequesting||e.isLoadingScripts)&&!H,r=!L&&C.isRequesting&&H;a&&(l(),n()),r&&(c("error",Object(t.__)("There was a problem updating your preferences",'woocommerce')),l())}updateTracking({allowTracking:C}){const{updateOptions:e}=this.props;C&&"function"==typeof window.wcTracks.enable?(this.setState({isLoadingScripts:!0}),window.wcTracks.enable(()=>{this._isMounted&&(Object(d.initializeExPlat)(),this.setState({isLoadingScripts:!1}))})):C||(window.wcTracks.isEnabled=!1);const H=C?"yes":"no";this.setState({isRequestStarted:!0}),e({woocommerce_allow_tracking:H})}componentDidMount(){this._isMounted=!0}componentWillUnmount(){this._isMounted=!1}render(){const{allowTracking:C,isResolving:e,onClose:H,onContinue:l}=this.props;if(e)return null;if(C)return H(),l(),null;const{isRequesting:n,title:c=Object(t.__)("Build a better WooCommerce",'woocommerce'),message:r=i()({mixedString:Object(t.__)("Get improved features and faster fixes by sharing non-sensitive data via {{link}}usage tracking{{/link}} that shows us how WooCommerce is used. No personal data is tracked or stored.",'woocommerce'),components:{link:Object(L.createElement)(a.Link,{href:"https://woocommerce.com/usage-tracking?utm_medium=product",target:"_blank",type:"external"})}}),dismissActionText:d=Object(t.__)("No thanks",'woocommerce'),acceptActionText:V=Object(t.__)("Yes, count me in!",'woocommerce')}=this.props,{isRequestStarted:m}=this.state,s=m&&n;return Object(L.createElement)(o.Modal,{title:c,isDismissible:this.props.isDismissible,onRequestClose:()=>this.props.onClose(),className:"woocommerce-usage-modal"},Object(L.createElement)("div",{className:"woocommerce-usage-modal__wrapper"},Object(L.createElement)("div",{className:"woocommerce-usage-modal__message"},r),Object(L.createElement)("div",{className:"woocommerce-usage-modal__actions"},Object(L.createElement)(o.Button,{isSecondary:!0,isBusy:s,onClick:()=>this.updateTracking({allowTracking:!1})},d),Object(L.createElement)(o.Button,{isPrimary:!0,isBusy:s,onClick:()=>this.updateTracking({allowTracking:!0})},V))))}}e.a=Object(l.compose)(Object(n.withSelect)(C=>{const{getOption:e,getOptionsUpdatingError:H,isOptionsUpdating:L,hasFinishedResolution:t}=C(r.OPTIONS_STORE_NAME);return{allowTracking:"yes"===e("woocommerce_allow_tracking"),isRequesting:Boolean(L()),isResolving:!t("getOption",["woocommerce_allow_tracking"])||void 0===e("woocommerce_allow_tracking"),hasErrors:Boolean(H())}}),Object(n.withDispatch)(C=>{const{createNotice:e}=C("core/notices"),{updateOptions:H}=C(r.OPTIONS_STORE_NAME);return{createNotice:e,updateOptions:H}}))(V)},519:function(C,e,H){"use strict";H.r(e),H.d(e,"UsageModal",(function(){return a}));var L=H(0),t=H(2),l=H(12),n=H(18),c=H.n(n),i=H(21),o=H(514);const a=()=>{const C="1"===Object(l.getQuery)()["wcpay-connection-success"],[e,H]=Object(L.useState)(C);if(!e)return null;const n=()=>{H(!1),Object(l.updateQueryString)({"wcpay-connection-success":void 0})},a=Object(t.__)("Help us build a better WooCommerce Payments experience",'woocommerce'),r=c()({mixedString:Object(t.__)("By agreeing to share non-sensitive {{link}}usage data{{/link}}, you’ll help us improve features and optimize the WooCommerce Payments experience. You can opt out at any time.",'woocommerce'),components:{link:Object(L.createElement)(i.Link,{href:"https://woocommerce.com/usage-tracking?utm_medium=product",target:"_blank",type:"external"})}});return Object(L.createElement)(o.a,{isDismissible:!1,title:a,message:r,acceptActionText:Object(t.__)("I agree",'woocommerce'),dismissActionText:Object(t.__)("No thanks",'woocommerce'),onContinue:n,onClose:n})};e.default=a},539:function(C,e,H){"use strict";H.d(e,"b",(function(){return o})),H.d(e,"c",(function(){return a})),H.d(e,"a",(function(){return h}));var L=H(2),t=H(17),l=H.n(t),n=H(11),c=H(16),i=H(507);function o(C,e,H){const t=Object(L.__)("There was an error connecting to WooCommerce Payments. Please try again or connect later in store settings.",'woocommerce');H(["woocommerce-payments"]).then(()=>{Object(c.recordEvent)("woocommerce_payments_install",{context:"tasklist"}),l()({path:n.WC_ADMIN_NAMESPACE+"/plugins/connect-wcpay",method:"POST"}).then(C=>{window.location=C.connectUrl}).catch(()=>{e("error",t),C()})}).catch(e=>{Object(i.a)(e),C()})}function a(C){return["US","PR","AU","CA","DE","ES","FR","GB","IE","IT","NZ","AT","BE","NL","PL","PT","CH","HK","SG"].includes(C)}var r=H(0),d=H(18),V=H.n(d),m=H(21),s=H(20),u=H(271),p=H(542);const b=()=>V()({mixedString:Object(L.__)('Upon clicking "Get started", you agree to the {{link}}Terms of service{{/link}}. Next we’ll ask you to share a few details about your business to create your account.','woocommerce'),components:{link:Object(r.createElement)(m.Link,{href:"https://wordpress.com/tos/",target:"_blank",type:"external"})}}),h=({paymentGateway:C,onSetupCallback:e=null})=>{const{description:H,id:t,needsSetup:l,installed:n,enabled:i,installed:o}=C;return Object(r.createElement)(u.WCPayCard,null,Object(r.createElement)(u.WCPayCardHeader,null,n&&l?Object(r.createElement)(u.SetupRequired,null):Object(r.createElement)(m.Pill,null,Object(L.__)("Recommended",'woocommerce'))),Object(r.createElement)(u.WCPayCardBody,{description:H,onLinkClick:()=>{Object(c.recordEvent)("tasklist_payment_learn_more")}}),Object(r.createElement)(u.WCPayCardFooter,null,Object(r.createElement)(r.Fragment,null,Object(r.createElement)(s.Text,{lineHeight:"1.5em"},Object(r.createElement)(b,null)),Object(r.createElement)(p.a,{id:t,hasSetup:!0,needsSetup:l,isEnabled:i,isRecommended:!0,isInstalled:o,hasPlugins:!0,setupButtonText:Object(L.__)("Get started",'woocommerce'),onSetupCallback:e}))))};H(519)},542:function(C,e,H){"use strict";H.d(e,"a",(function(){return i}));var L=H(0),t=H(2),l=H(3),n=H(12),c=H(16);const i=({hasSetup:C=!1,needsSetup:e=!0,id:H,isEnabled:i=!1,isLoading:o=!1,isInstalled:a=!1,isRecommended:r=!1,hasPlugins:d,manageUrl:V=null,markConfigured:m,onSetUp:s=(()=>{}),onSetupCallback:u,setupButtonText:p=Object(t.__)("Set up",'woocommerce')})=>{const[b,h]=Object(L.useState)(!1),f="woocommerce-task-payment__action";if(o)return Object(L.createElement)(l.Spinner,null);const O=async()=>{if(s(H),u)return h(!0),void await new Promise(u).then(()=>{h(!1)}).catch(()=>{h(!1)});Object(n.updateQueryString)({id:H})},j=()=>Object(L.createElement)(l.Button,{className:f,isSecondary:!0,role:"button",href:V,onClick:()=>Object(c.recordEvent)("tasklist_payment_manage",{id:H})},Object(t.__)("Manage",'woocommerce')),M=()=>Object(L.createElement)(l.Button,{className:f,isPrimary:r,isSecondary:!r,isBusy:b,disabled:b,onClick:()=>O()},p);return C?d?e?a&&d?Object(L.createElement)("div",null,Object(L.createElement)(l.Button,{className:f,isPrimary:r,isSecondary:!r,isBusy:b,disabled:b,onClick:()=>O()},Object(t.__)("Finish setup",'woocommerce'))):Object(L.createElement)(M,null):Object(L.createElement)(j,null):i?Object(L.createElement)(j,null):Object(L.createElement)(M,null):i?Object(L.createElement)(j,null):Object(L.createElement)(l.Button,{className:f,isSecondary:!0,onClick:()=>m(H)},Object(t.__)("Enable",'woocommerce'))}}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/31.style.css b/packages/woocommerce-admin/dist/chunks/31.style.css new file mode 100644 index 0000000..a22ee2b --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/31.style.css @@ -0,0 +1 @@ +.woocommerce-layout__activity-panel-header{height:50px;background:#e0e0e0;padding:16px;display:flex;justify-content:space-between;align-items:center}@media(min-width:783px){.woocommerce-layout__activity-panel-header{padding:16px 24px}}.woocommerce-layout__activity-panel-header h3{font-size:13px;font-weight:600;line-height:16px;margin:0;padding:0}.woocommerce-layout__activity-panel-header .woocommerce-ellipsis-menu__toggle.components-button:not(:disabled):not([aria-disabled=true]):focus,.woocommerce-layout__activity-panel-header .woocommerce-ellipsis-menu__toggle.components-button:not(:disabled):not([aria-disabled=true]):hover{box-shadow:none;border-radius:10px;background:#ccc}.woocommerce-layout__inbox-title{color:#1e1e1e;display:flex;align-items:center}.woocommerce-layout__inbox-subtitle{color:#757575}.woocommerce-layout__inbox-badge{margin-left:6px;background-color:#757575;border-radius:13px;padding:0 6px;color:#fff;display:inline-block;text-align:center;vertical-align:top}.woocommerce-homescreen .woocommerce-activity-panel{background:transparent;border:0}.woocommerce-homescreen .woocommerce-activity-panel .components-panel__body{background:#fff;border:1px solid #e0e0e0}.woocommerce-homescreen .woocommerce-activity-panel .components-panel__body.is-opened{margin-bottom:24px}.woocommerce-homescreen .woocommerce-activity-panel.components-panel>.components-panel__body:last-child,.woocommerce-homescreen .woocommerce-activity-panel .components-panel__body.is-opened .components-panel__body-toggle{border-bottom:1px solid #e0e0e0}.woocommerce-homescreen .woocommerce-activity-panel .components-panel__row{margin:0 -16px -16px}.woocommerce-homescreen .woocommerce-activity-panel .components-panel__row>div{width:100%}.woocommerce-homescreen .woocommerce-activity-panel .components-panel__body-toggle{border-radius:0}.woocommerce-homescreen .woocommerce-activity-panel .components-panel__body-toggle:disabled{opacity:1}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-activity-panel{margin-bottom:24px}.woocommerce-homescreen .woocommerce-activity-panel .components-panel__body-title p{margin-right:16px}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-activity-card{padding:20px var(--main-gap) var(--main-gap)}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-activity-card:not(:last-of-type){border-bottom:1px solid #e0e0e0}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-activity-card__header{margin-bottom:12px}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-empty-activity-card{margin:0;background:unset;text-align:center;padding:24px var(--main-gap) 4px}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-empty-activity-card h4{color:#1e1e1e}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-outbound-link{display:flex;justify-content:space-between;align-items:center;height:50px;border-bottom:1px solid #f0f0f0;padding:16px 24px;padding:16px var(--main-gap);font-size:13px;font-weight:500;line-height:18px;margin:0;text-decoration:none}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-outbound-link .gridicon{display:none}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-outbound-link:active,.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-outbound-link:hover{background-color:#f0f0f0}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-outbound-link:focus{background-color:#f0f0f0;box-shadow:inset 0 0 0 1px #5b9dd9,inset 0 0 0 2px #f0f0f0}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-outbound-link:active .gridicon,.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-outbound-link:focus .gridicon,.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-outbound-link:hover .gridicon{display:initial}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-layout__activity-panel-empty{border-top:1px solid #e0e0e0;border-bottom:0}.woocommerce-homescreen .woocommerce-activity-panel .woocommerce-activity-card__button:focus{box-shadow:unset;outline:unset;border:1px solid #e0e0e0}.woocommerce-activity-panel .woocommerce-activity-card{position:relative;padding:24px;padding:var(--main-gap);background:#fff;border-bottom:1px solid #e0e0e0;color:#757575;font-size:13px;font-size:.8125rem}.woocommerce-activity-panel .woocommerce-activity-card:not(.woocommerce-empty-activity-card){display:grid;grid-template-columns:50px 1fr;grid-template-areas:"icon header" "icon body" "icon actions"}.woocommerce-activity-panel .woocommerce-activity-card__button{display:block;height:unset;background:none;align-items:unset;transition:unset;text-align:left;width:100%;padding:0}.woocommerce-activity-card__unread{position:absolute;top:18px;top:calc(var(--main-gap) - 6px);right:18px;right:calc(var(--main-gap) - 6px);width:6px;height:6px;border-radius:50%;background:#ca4a1f}.woocommerce-activity-card__icon{-ms-grid-row:1;-ms-grid-row-span:3;-ms-grid-column:1;grid-area:icon;fill:#e0e0e0}.woocommerce-activity-card__header{margin-bottom:16px;display:flex;flex-direction:column}.woocommerce-activity-card__header .woocommerce-activity-card__title{margin:0;font-size:14px;font-size:.875rem;order:2}.woocommerce-empty-activity-card .woocommerce-activity-card__header .woocommerce-activity-card__title{color:#1e1e1e;font-style:normal;line-height:24px;font-weight:400}.woocommerce-activity-card__button .woocommerce-activity-card__header .woocommerce-activity-card__title{margin-bottom:8px}.woocommerce-activity-card__header .woocommerce-activity-card__title a{text-decoration:none}.woocommerce-activity-card__header .woocommerce-activity-card__date{color:#757575;font-size:12px;font-size:.75rem;margin-bottom:12px;order:1}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{order:3}.woocommerce-activity-card__button .woocommerce-activity-card__header .woocommerce-activity-card__subtitle{margin-bottom:4px}@media(min-width:783px){.woocommerce-activity-card__header{-ms-grid-row:1;-ms-grid-column:2;grid-area:header;display:grid;grid-template:"title date" "subtitle date"/1fr auto}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__header{-ms-grid-row:1;-ms-grid-column:1}.woocommerce-activity-card__header .woocommerce-activity-card__title{grid-area:title}.woocommerce-activity-card__header .woocommerce-activity-card__date{display:block;grid-area:date;justify-self:end;margin-bottom:0}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{grid-area:subtitle}}@media (min-width:783px){.woocommerce-activity-card__header .woocommerce-activity-card__title{-ms-grid-row:1;-ms-grid-column:1}.woocommerce-activity-card__header .woocommerce-activity-card__date{-ms-grid-row:1;-ms-grid-row-span:2;-ms-grid-column:2}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{-ms-grid-row:2;-ms-grid-column:1}}.woocommerce-activity-card__body{-ms-grid-row:2;-ms-grid-column:2;grid-area:body}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__body{-ms-grid-row:2;-ms-grid-column:1}.woocommerce-activity-card__body>p:first-child{margin-top:0}.woocommerce-activity-card__body>p:last-child{margin-bottom:0}.woocommerce-empty-activity-card .woocommerce-activity-card__body{color:#757575;font-style:normal;font-weight:400;font-size:13px;font-size:.8125rem;line-height:20px}.woocommerce-activity-card__actions{-ms-grid-row:3;-ms-grid-column:2;grid-area:actions;margin-top:16px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__actions{-ms-grid-row:3;-ms-grid-column:1}.woocommerce-activity-card__actions>*+*{margin-left:.5em}.woocommerce-activity-card__actions .components-button{height:24px;padding:4px 10px;font-size:11px;font-size:.6875rem}.woocommerce-activity-card__actions .components-button.is-destructive:not(:hover){box-shadow:none}.woocommerce-activity-card.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;height:16px}.woocommerce-activity-card.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-activity-card.is-loading .is-placeholder{animation:none}}.woocommerce-activity-card.is-loading .woocommerce-activity-card__title{width:80%}.woocommerce-activity-card.is-loading .woocommerce-activity-card__subtitle{margin-top:4px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__date{width:100%;margin-bottom:16px}@media(min-width:783px){.woocommerce-activity-card.is-loading .woocommerce-activity-card__date{text-align:right;margin-bottom:0}}.woocommerce-activity-card.is-loading .woocommerce-activity-card__date .is-placeholder{width:68px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__icon{margin-right:24px;margin-right:var(--main-gap)}.woocommerce-activity-card.is-loading .woocommerce-activity-card__icon .is-placeholder{height:33px;width:33px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__body .is-placeholder{width:100%;margin-bottom:4px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__body .is-placeholder:last-of-type{width:65%;margin-bottom:0}.woocommerce-activity-card.is-loading .woocommerce-activity-card__actions .is-placeholder{width:91px;height:24px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card{grid-template-columns:1fr;grid-template-areas:"header" "body" "actions"}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__icon{display:none}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-flag{display:inline-block}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__subtitle span+span:before{content:" • "}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{grid-template-columns:72px 1fr;height:100%;opacity:1;padding:24px;padding:var(--main-gap)}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{transition:opacity .3s,height 0s,padding 0s}}@media(max-width:782px){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{grid-template-columns:64px 1fr}}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card .woocommerce-activity-card__header{margin-bottom:12px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card.actioned{height:0;opacity:0;padding:0}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card.actioned{transition:opacity .3s,height 0s .3s,padding 0s .3s}}.woocommerce-stock-activity-card__image-overlay__product{height:33px;position:relative;width:33px}.woocommerce-stock-activity-card__image-overlay__product.is-placeholder:before{background-color:#757575;border-radius:2px;content:"";position:absolute;left:0;right:0;bottom:0;top:0;opacity:.1}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-stock-activity-card{transition:opacity .3s}}.woocommerce-stock-activity-card.is-dimmed{opacity:.7}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__stock-quantity{background:#f0f0f0;color:#757575;padding:3px 8px;border-radius:3px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__stock-quantity.out-of-stock{color:#d94f4f}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity{display:inline-flex;width:50px;margin-right:10px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input{border-radius:2px;height:30px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]{-moz-appearance:textfield}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]::-webkit-inner-spin-button,.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.woocommerce-stock-activity-card .woocommerce-activity-card__subtitle{color:#757575;font-size:12px;font-size:.75rem}.woocommerce-empty-activity-card{background:#f0f0f0;margin:20px;border-bottom:unset}.woocommerce-activity-panel .woocommerce-order-empty__success-icon{font-size:36px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__header{margin-bottom:4px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__header .woocommerce-activity-card__title.is-placeholder{width:45%}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__body>.is-placeholder{width:30%}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__actions>.is-placeholder{height:24px;margin-top:4px;width:65px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-order-activity-card{cursor:pointer}.woocommerce-review-activity-card .woocommerce-rating{margin-top:4px}.woocommerce-review-activity-card .woocommerce-rating .gridicon{fill:#ffb900}.woocommerce-review-activity-card .woocommerce-activity-card__body>span>p:first-child{margin-top:0}.woocommerce-review-activity-card .woocommerce-activity-card__body>span>p:last-child{margin-bottom:0}.woocommerce-review-activity-card .woocommerce-review-activity-card__verified{display:inline-flex;margin-left:4px;position:relative;top:4px;color:#4ab866;font-size:12px;font-size:.75rem}.woocommerce-review-activity-card .woocommerce-review-activity-card__verified svg{width:18px;height:18px;fill:#4ab866}@media(max-width:782px){.woocommerce-review-activity-card .woocommerce-review-activity-card__image-overlay{margin-top:4px}}.woocommerce-review-activity-card.woocommerce-activity-card{padding:16px 24px}.woocommerce-review-activity-card .woocommerce-activity-card__header .woocommerce-activity-card__title{line-height:20px}#activity-panel-inbox{margin:0 24px}.woocommerce-layout__inbox-panel-header{padding:24px}.woocommerce-homepage-column .woocommerce-layout__inbox-panel-header{padding:0 24px}.woocommerce-inbox-message-enter{opacity:0;max-height:0;transform:translateX(50%)}.woocommerce-inbox-message-enter-active{transition:opacity .5s,transform .5s,max-height .5s}.woocommerce-inbox-message-enter-active,.woocommerce-inbox-message-exit{opacity:1;max-height:100vh;transform:translateX(0)}.woocommerce-inbox-message-exit-active{opacity:0;max-height:0;transform:translateX(50%);transition:opacity .5s,transform .5s,max-height .5s}.woocommerce-navigation-intro-modal{width:670px}@media(max-width:782px){.woocommerce-navigation-intro-modal{width:350px}}.woocommerce-navigation-intro-modal .components-guide__page-control{order:3;margin:16px 0}.woocommerce-navigation-intro-modal .components-guide__page-control li{margin:0}.woocommerce-navigation-intro-modal .components-modal__header{display:none}.woocommerce-navigation-intro-modal .components-guide__container{margin-top:0}.woocommerce-navigation-intro-modal .components-guide__footer{box-sizing:border-box;margin:0;height:0;overflow:visible}.woocommerce-navigation-intro-modal .components-guide__footer .components-button{position:absolute;bottom:100%;margin-bottom:16px}.woocommerce-navigation-intro-modal .components-guide__footer .components-guide__back-button{display:none}.woocommerce-navigation-intro-modal.components-modal__frame.components-guide{height:auto}.woocommerce-navigation-intro-modal .woocommerce-navigation-intro-modal__page-wrapper{display:grid;grid-template-columns:1fr 1fr}.woocommerce-navigation-intro-modal .woocommerce-navigation-intro-modal__page-wrapper img{max-width:100%}@media(max-width:782px){.woocommerce-navigation-intro-modal .woocommerce-navigation-intro-modal__page-wrapper{grid-template-columns:1fr}.woocommerce-navigation-intro-modal .woocommerce-navigation-intro-modal__page-wrapper .woocommerce-navigation-intro-modal__image-wrapper{grid-row:1;max-height:328px;overflow:hidden}}.woocommerce-navigation-intro-modal .woocommerce-navigation-intro-modal__page-text{padding:24px;display:flex;flex-direction:column;justify-content:center}.woocommerce-navigation-intro-modal .woocommerce-navigation-intro-modal__page-text h2{font-weight:700;margin-bottom:8px}.woocommerce-stats-overview .woocommerce-card__body{padding:0}.woocommerce-stats-overview .woocommerce-summary{margin:0}.woocommerce-stats-overview__more-btn{display:inline-block;padding:16px}.woocommerce-stats-overview__tabs .components-tab-panel__tabs{display:flex;justify-content:space-between;border-bottom:1px solid #e0e0e0}.woocommerce-stats-overview__tabs .components-tab-panel__tabs .components-button{width:33.33%;padding-right:24px;padding-left:24px}.woocommerce-stats-overview__stats{margin:0}.woocommerce-stats-overview__stats .woocommerce-summary__item-container{width:50%;display:inline-block}.woocommerce-stats-overview__stats.is-even .woocommerce-summary__item-container:nth-last-of-type(2) .woocommerce-summary__item,.woocommerce-stats-overview__stats .woocommerce-summary__item-container:last-of-type .woocommerce-summary__item{border-bottom:none}.woocommerce-stats-overview__stats .woocommerce-summary__item-container:nth-of-type(2n) .woocommerce-summary__item{border-right:none}.woocommerce-stats-overview__stats .woocommerce-summary__item{background-color:#fff}.woocommerce-stats-overview__stats .woocommerce-summary__item:active,.woocommerce-stats-overview__stats .woocommerce-summary__item:hover{background-color:#f0f0f0}article.woocommerce-stats-overview__install-jetpack-promo .woocommerce-stats-overview__install-jetpack-promo__content{padding:16px 24px}article.woocommerce-stats-overview__install-jetpack-promo h2{color:#1e1e1e;font-size:16px;font-size:1rem;font-style:normal;line-height:1.5;font-weight:400;margin:8px 0}article.woocommerce-stats-overview__install-jetpack-promo p{color:#757575;font-style:normal;font-weight:400;font-size:14px;font-size:.875rem;line-height:20px;margin:8px 0}article.woocommerce-stats-overview__install-jetpack-promo footer{padding:16px 24px;border-top:1px solid #f0f0f0;border-bottom:1px solid #f0f0f0}article.woocommerce-stats-overview__install-jetpack-promo footer button{margin-left:8px}article.woocommerce-stats-overview__install-jetpack-promo footer button:first-child{margin-left:0}.woocommerce-store-management-links__card-body.is-size-custom{padding:24px 0 8px}.woocommerce-quick-links__category{display:flex;flex-flow:row wrap;margin-bottom:8px}.woocommerce-quick-links__category .woocommerce-quick-links__category-header{margin:0 24px 8px;text-transform:uppercase;color:#757575;line-height:16px;font-size:11px;flex:1 100%}.woocommerce-quick-links__item{display:flex;flex:1 50%}.woocommerce-quick-links__item:hover{background-color:#f0f0f0}.woocommerce-quick-links__item .woocommerce-quick-links__item-link{width:100%;display:flex;align-items:center;text-decoration:none;padding:16px 27px 16px 24px}.woocommerce-quick-links__item .woocommerce-quick-links__item-link .woocommerce-quick-links__item-link__icon{fill:#007cba;fill:var(--wp-admin-theme-color)}.woocommerce-quick-links__item .woocommerce-quick-links__item-link .woocommerce-quick-links__item-link__text{margin-left:16px;flex:1;line-height:16px;font-size:13px}.woocommerce-homescreen.two-columns .woocommerce-quick-links__item{flex:1 100%}.woocommerce-task-dashboard__body .woocommerce-card__description{color:#646970}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-payments{width:680px;margin:auto;max-width:100%}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card .wooocommerce-task-card__header{display:flex}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card .wooocommerce-task-card__header .woocommerce-badge{margin-left:16px}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card .woocommerce-list__item-text .woocommerce-pill{padding:1px 8px;margin-left:8px}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card .components-popover__content{min-width:unset}.woocommerce-task-dashboard__body .woocommerce-task-list__item-expandable-content,.woocommerce-task-dashboard__body .woocommerce-task__additional-info,.woocommerce-task-dashboard__body .woocommerce-task__estimated-time{color:#757575;font-weight:400;font-size:12px}.woocommerce-task-dashboard__body .woocommerce-task-list__item-expandable-content{font-size:13px}.woocommerce-task-dashboard__body #wpbody-content{position:relative}.woocommerce-task-dashboard__body .components-modal__screen-overlay{background:rgba(43,45,47,.4)}.woocommerce-task-dashboard__body .components-modal__frame .components-modal__header{border-bottom:0;margin-bottom:0}.woocommerce-task-dashboard__body .components-modal__frame .woocommerce-task-payments__stripe-error-wrapper{align-items:flex-end;flex-grow:1;display:flex;flex-direction:column}.woocommerce-shipping-rate{display:flex;padding-top:12px;padding-bottom:12px}.woocommerce-shipping-rate .woocommerce-shipping-rate__main{width:100%}.woocommerce-shipping-rate .woocommerce-shipping-rate__icon{padding-top:16px;margin-right:24px}.woocommerce-shipping-rate .woocommerce-shipping-rate__name{align-items:center;display:flex;padding-top:16px;font-size:16px;line-height:22px;color:#1d2327;margin-bottom:12px;border-top:1px solid #dcdcde}.woocommerce-shipping-rate .woocommerce-shipping-rate__name .components-form-toggle{margin-left:auto;height:18px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-base-control{margin-bottom:0}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-base-control__label{display:block;position:relative;top:-8px;width:100%;font-size:12px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__prefix,.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__suffix{font-size:16px;line-height:24px;color:#646970;border:0;padding:0;align-items:center;display:flex;top:-11px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .components-text-control__input{position:relative;top:-11px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__prefix{margin-right:4px}.woocommerce-shipping-rate .woocommerce-shipping-rate__control-wrapper .text-control-with-affixes__suffix{margin-left:4px}.woocommerce-task-tax__automated-tax-control{display:flex;align-items:center;margin-top:16px}.woocommerce-task-tax__automated-tax-control i{margin-left:16px;margin-right:24px}.woocommerce-task-tax__automated-tax-control .woocommerce-task-tax__automated-tax-control-inner{border-top:1px solid #dcdcde;display:flex;align-items:center;flex:1;font-size:16px;padding-top:16px;padding-bottom:16px}.woocommerce-task-tax__automated-tax-control .components-form-toggle{margin-left:auto}.woocommerce-task-tax__success{display:flex;flex-direction:column;align-items:center;padding:40px;text-align:center}.woocommerce-task-tax__success .woocommerce-task-tax__success-icon{font-size:48px;height:48px;align-items:center;display:flex}.woocommerce-task-tax__success #woocommerce-task-tax__success-message{font-size:32px;font-weight:400}.woocommerce-task-tax__success p{margin-top:0;font-size:16px}.woocommerce-task-payments .components-card+.components-card{margin-top:24px}.woocommerce-task-payments .woocommerce-task-payment__setup_required{display:flex;align-items:center;font-size:14px;margin-left:12px;font-weight:400;gap:3px}.woocommerce-task-payments .woocommerce-task-payment__setup_required>svg{fill:#efb854}.woocommerce-task-payments .components-card__header{font-size:20px;font-weight:400;line-height:28px;margin:0}.woocommerce-task-payments .woocommerce-task-payment__recommended-pill{border:1px solid #dcdcde;border-radius:28px;display:inline-block;font-size:13px;margin-left:12px;padding:1px 10px}.woocommerce-task-payments .woocommerce-task-payment__recommended-pill span{max-width:70px}.woocommerce-task-payments .components-card__divider:last-child{display:none}.woocommerce-task-payment-method>h3{margin:0;color:#1d2327}.woocommerce-task-payment-method p{font-size:14px;color:#646970;font-weight:400;margin-top:16px;margin-bottom:16px}.woocommerce-task-payment-method__fields{display:grid;grid-template-columns:1fr 1fr;grid-gap:0 16px;margin-bottom:8px}.woocommerce-task-payment-method__fields .components-base-control{margin-bottom:0}.woocommerce-task-dashboard__container .woocommerce-stepper button.components-button.is-primary{margin:0 8px 0 0}.woocommerce-task-dashboard__container button.components-button.is-link{margin:0;height:auto;color:#50575e;font-weight:400}.woocommerce-task-payments__paypal-auto-create-account{margin-top:16px;margin-bottom:16px}.woocommerce-task-card__prompt{width:100%;min-width:100%;margin-bottom:24px;margin-top:-4px;cursor:default}.woocommerce-task-card__prompt .components-snackbar__content{display:block;align-items:unset;justify-content:unset}.woocommerce-task-card__prompt .components-snackbar__content span{margin-left:-24px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link,.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:active,.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:focus{color:#fff;margin-left:24px;background:transparent}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions button.is-link:hover{color:#fff}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-pointer{border-bottom:10px solid #1e1e1e;border-left:10px solid transparent;border-right:10px solid transparent;position:relative;width:0;height:0;display:inline-block;top:-30px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-content{display:flex;align-items:baseline;justify-content:space-between;max-height:10px;margin-left:24px;position:relative;top:-40px}.woocommerce-task-card__prompt .woocommerce-task-card__prompt-actions{margin-right:-16px}.woocommerce-task-card__prompt:hover .woocommerce-task-card__prompt-pointer{border-bottom-color:#1e1e1e}.woocommerce-task-card__section-controls{text-align:center}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-card__body{border-top:1px solid #dcdcde}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;height:16px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .is-placeholder{animation:none}}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-card__title .is-placeholder{width:70%;height:28px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-before .is-placeholder{height:36px;width:36px}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-text{width:100%}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-text .woocommerce-list__item-title .is-placeholder{height:22px;width:60%}.woocommerce-task-dashboard__container .woocommerce-task-card.is-loading .woocommerce-list__item-after .is-placeholder{height:18px;width:60px}.woocommerce-task-dashboard__container .woocommerce-task__caption{color:#757575;margin-top:16px}.components-guide.woocommerce__welcome-modal.woocommerce__welcome-from-calypso-modal{height:440px}.components-guide.woocommerce__welcome-modal.woocommerce__welcome-from-calypso-modal ul.components-guide__page-control{display:none}.components-guide.woocommerce__welcome-modal.woocommerce__welcome-from-calypso-modal .woocommerce__welcome-modal__page-content{margin-top:16px}.components-guide.woocommerce__welcome-modal{max-width:517px;height:460px}.components-guide.woocommerce__welcome-modal .components-modal__header{height:0}.components-guide.woocommerce__welcome-modal .components-modal__header .components-button{color:#fff}.components-guide.woocommerce__welcome-modal .components-guide__container{margin-top:0}.components-guide.woocommerce__welcome-modal .components-guide__container .woocommerce__welcome-modal__page-content{padding:0 24px}.components-guide.woocommerce__welcome-modal .components-guide__container .woocommerce__welcome-modal__page-content .woocommerce__welcome-modal__page-content__header{font-size:24px;line-height:32px;margin:0 0 24px}.components-guide.woocommerce__welcome-modal .components-guide__container .woocommerce__welcome-modal__page-content .woocommerce__welcome-modal__page-content__body{font-size:16px;line-height:24px;margin:0 0 24px}.components-guide.woocommerce__welcome-modal .components-guide__footer{padding:0;margin:0 24px 24px;justify-content:flex-end;width:auto}.components-guide.woocommerce__welcome-modal .components-guide__footer .components-guide__back-button,.components-guide.woocommerce__welcome-modal .components-guide__footer .components-guide__finish-button,.components-guide.woocommerce__welcome-modal .components-guide__footer .components-guide__forward-button{position:static;padding:0 16px;font-weight:500;font-size:14px;line-height:18px}.components-guide.woocommerce__welcome-modal .components-guide__footer .components-guide__back-button,.components-guide.woocommerce__welcome-modal .components-guide__footer .components-guide__forward-button{color:#007cba;color:var(--wp-admin-theme-color)}.components-guide.woocommerce__welcome-modal .components-guide__footer .components-guide__finish-button,.components-guide.woocommerce__welcome-modal .components-guide__footer .components-guide__forward-button{margin-left:10px}.components-guide.woocommerce__welcome-modal .components-guide__footer .components-guide__forward-button{box-shadow:inset 0 0 0 1px #007cba;box-shadow:inset 0 0 0 1px var(--wp-admin-theme-color);outline:1px solid transparent}.components-guide.woocommerce__welcome-modal .fill-theme-color{fill:#007cba;fill:var(--wp-admin-theme-color)}.woocommerce-page #wpcontent,.woocommerce-page.woocommerce_page_wc-admin #wpbody-content{overflow-x:inherit!important}.woocommerce-homescreen{display:flex;max-width:1032px;margin:0 auto;justify-content:space-between;flex-direction:column}.woocommerce-homescreen .woocommerce-task-dashboard__container{width:100%;margin-bottom:24px}.woocommerce-homescreen.two-columns{flex-direction:row}.woocommerce-homescreen.two-columns .woocommerce-homescreen-column{width:calc(50% - 12px);margin:0}@media(max-width:782px){.woocommerce-homescreen.two-columns .woocommerce-homescreen-column{width:100%;position:inherit;top:auto}}.woocommerce-homescreen.two-columns .your-store-today{display:block}@media(max-width:782px){.woocommerce-homescreen.two-columns{flex-direction:column}.woocommerce-homescreen.two-columns .your-store-today{display:none}}@media(max-width:782px){.woocommerce-homescreen{margin-left:-16px;margin-right:-16px}}.woocommerce-homescreen-column{width:682px;top:100px;margin:0 auto;align-self:flex-start}.woocommerce-homescreen-column>div{margin-bottom:24px}@media(max-width:782px){.woocommerce-homescreen-column{width:100%;position:inherit;top:auto}}.woocommerce-homescreen-card .components-card__header.is-size-large,.woocommerce-homescreen-card .components-card__header.is-size-medium{min-height:63px;min-height:unset;display:grid;grid-template-columns:auto 24px}.woocommerce-homescreen-card .components-card__header.is-size-large>*,.woocommerce-homescreen-card .components-card__header.is-size-medium>*{align-self:center}.woocommerce-homescreen-card .components-card__footer.is-size-large{padding:0 8px}.woocommerce-layout__inbox-panel-header.your-store-today{display:none}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}#klarna-kp-banner,.woocommerce-page:not(.woocommerce-embed-page) #klarna-banner{display:none}.woocommerce-dashboard__columns{display:grid;grid-template-columns:calc(50% - 12px) calc(50% - 12px);grid-column-gap:24px}.woocommerce-dashboard__columns>div:first-child{grid-column-start:1;grid-column-end:2;grid-row-start:1;grid-row-end:2}.woocommerce-dashboard__columns>div:nth-child(2){grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:2}.woocommerce-dashboard__columns>div:nth-child(3){grid-column-start:1;grid-column-end:2;grid-row-start:2;grid-row-end:3}.woocommerce-dashboard__columns>div:nth-child(4){grid-column-start:2;grid-column-end:3;grid-row-start:2;grid-row-end:3}.woocommerce-dashboard__columns>div:nth-child(5){grid-column-start:1;grid-column-end:2;grid-row-start:3;grid-row-end:4}.woocommerce-dashboard__columns>div:nth-child(6){grid-column-start:2;grid-column-end:3;grid-row-start:3;grid-row-end:4}.woocommerce-dashboard__columns>div:nth-child(7){grid-column-start:1;grid-column-end:2;grid-row-start:4;grid-row-end:5}.woocommerce-dashboard__columns>div:nth-child(8){grid-column-start:2;grid-column-end:3;grid-row-start:4;grid-row-end:5}.woocommerce-dashboard__columns>div:nth-child(9){grid-column-start:1;grid-column-end:2;grid-row-start:5;grid-row-end:6}.woocommerce-dashboard__columns>div:nth-child(10){grid-column-start:2;grid-column-end:3;grid-row-start:5;grid-row-end:6}.woocommerce-dashboard__columns>div:nth-child(11){grid-column-start:1;grid-column-end:2;grid-row-start:6;grid-row-end:7}.woocommerce-dashboard__columns>div:nth-child(12){grid-column-start:2;grid-column-end:3;grid-row-start:6;grid-row-end:7}.woocommerce-dashboard__columns>div:nth-child(13){grid-column-start:1;grid-column-end:2;grid-row-start:7;grid-row-end:8}.woocommerce-dashboard__columns>div:nth-child(14){grid-column-start:2;grid-column-end:3;grid-row-start:7;grid-row-end:8}@media(max-width:960px){.woocommerce-dashboard__columns{grid-template-columns:100%}.woocommerce-dashboard__columns>div:first-child{grid-column-start:1;grid-column-end:2;grid-row-start:1;grid-row-end:2}.woocommerce-dashboard__columns>div:nth-child(2){grid-column-start:1;grid-column-end:2;grid-row-start:2;grid-row-end:3}.woocommerce-dashboard__columns>div:nth-child(3){grid-column-start:1;grid-column-end:2;grid-row-start:3;grid-row-end:4}.woocommerce-dashboard__columns>div:nth-child(4){grid-column-start:1;grid-column-end:2;grid-row-start:4;grid-row-end:5}.woocommerce-dashboard__columns>div:nth-child(5){grid-column-start:1;grid-column-end:2;grid-row-start:5;grid-row-end:6}.woocommerce-dashboard__columns>div:nth-child(6){grid-column-start:1;grid-column-end:2;grid-row-start:6;grid-row-end:7}.woocommerce-dashboard__columns>div:nth-child(7){grid-column-start:1;grid-column-end:2;grid-row-start:7;grid-row-end:8}.woocommerce-dashboard__columns>div:nth-child(8){grid-column-start:1;grid-column-end:2;grid-row-start:8;grid-row-end:9}.woocommerce-dashboard__columns>div:nth-child(9){grid-column-start:1;grid-column-end:2;grid-row-start:9;grid-row-end:10}.woocommerce-dashboard__columns>div:nth-child(10){grid-column-start:1;grid-column-end:2;grid-row-start:10;grid-row-end:11}.woocommerce-dashboard__columns>div:nth-child(11){grid-column-start:1;grid-column-end:2;grid-row-start:11;grid-row-end:12}.woocommerce-dashboard__columns>div:nth-child(12){grid-column-start:1;grid-column-end:2;grid-row-start:12;grid-row-end:13}.woocommerce-dashboard__columns>div:nth-child(13){grid-column-start:1;grid-column-end:2;grid-row-start:13;grid-row-end:14}.woocommerce-dashboard__columns>div:nth-child(14){grid-column-start:1;grid-column-end:2;grid-row-start:14;grid-row-end:15}}.woocommerce-dashboard__widget{display:flex;align-items:center;text-align:center}.woocommerce-dashboard__widget-item{flex:1}.woocommerce-dashboard-section__add-more{margin:0 auto;width:84px;padding:0 24px 24px}.woocommerce-dashboard-section__add-more .components-popover__content{padding:0 16px 8px}.woocommerce-dashboard-section__add-more>button svg{fill:#757575}.woocommerce-dashboard-section__add-more-choices{display:flex;justify-content:center}.woocommerce-dashboard-section__add-more-btn{display:flex;flex-direction:column;align-items:center;padding:16px;margin:8px}.woocommerce-dashboard-section__add-more-btn.components-button{height:auto}.woocommerce-dashboard-section__add-more-btn .store-performance__icon{transform:rotate(-45deg)}.woocommerce-dashboard-section__add-more-btn-title{color:#757575;padding-top:8px}.woocommerce-dashboard-section-controls{border-top:1px solid #f0f0f0;padding-top:8px}.woocommerce-dashboard-section-controls .icon-control{margin:0 8px 0 0;vertical-align:bottom;fill:#757575}.woocommerce-dashboard-section-controls .woocommerce-ellipsis-menu__item{padding-bottom:10px}.components-card .woocommerce-ellipsis-menu__toggle{padding:0}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card{max-width:680px;margin-left:auto;margin-right:auto;margin-bottom:24px}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card .components-card__header.is-size-large{padding-bottom:12px}.woocommerce-task-dashboard__body .woocommerce-task-dashboard__container .woocommerce-task-card .components-card__header.is-size-large .woocommerce-card__menu{margin-top:8px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/32.style.css b/packages/woocommerce-admin/dist/chunks/32.style.css new file mode 100644 index 0000000..d921b6f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/32.style.css @@ -0,0 +1 @@ +.woocommerce-leaderboard.woocommerce-empty-content{margin-bottom:24px}.woocommerce-leaderboard .woocommerce-card__body{padding:0}.woocommerce-leaderboard .woocommerce-table__table{margin-bottom:0}.woocommerce-leaderboard .woocommerce-table__table tr:last-child{border-bottom-style:none}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-dashboard__dashboard-leaderboards .components-base-control__field{width:100%}.woocommerce-dashboard__dashboard-leaderboards .components-select-control__input{border:1px solid #ccc;height:34px}.woocommerce-dashboard__dashboard-leaderboards .woocommerce-dashboard__dashboard-leaderboards__select .components-base-control__field{padding:0 12px 4px}.woocommerce-dashboard__dashboard-leaderboards .woocommerce-dashboard__dashboard-leaderboards__select .woocommerce-ellipsis-menu__title{padding:10px 0 14px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/34.style.css b/packages/woocommerce-admin/dist/chunks/34.style.css new file mode 100644 index 0000000..272da95 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/34.style.css @@ -0,0 +1 @@ +.woocommerce-marketing-overview{max-width:1032px;margin:0 auto}.woocommerce-marketing-overview .components-card{margin-bottom:24px}.woocommerce-marketing-installed-extensions-card__item{display:flex;align-items:center;padding:18px 24px}.woocommerce-marketing-installed-extensions-card__item h4{font-weight:400;font-size:16px;margin:0 0 5px;color:#1e1e1e}.woocommerce-marketing-installed-extensions-card__item p{color:#757575;margin:0}.woocommerce-marketing-installed-extensions-card__item:not(:last-child){border-bottom:1px solid #e0e0e0}.woocommerce-marketing-installed-extensions-card__item-text-and-actions{display:flex;flex-wrap:wrap;align-items:center;flex-grow:2;min-width:0}@media(min-width:601px){.woocommerce-marketing-installed-extensions-card__item-text-and-actions{flex-wrap:nowrap}}@media(min-width:601px){.woocommerce-marketing-installed-extensions-card__item-actions{text-align:right;white-space:nowrap;padding-left:25px}}@media(min-width:961px){.woocommerce-marketing-installed-extensions-card__item-description{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:550px}}.woocommerce-marketing-installed-extensions-card__item-links{margin:0;padding:0}.woocommerce-marketing-installed-extensions-card__item-links li{display:inline-block;margin:0 25px 0 0}@media(min-width:601px){.woocommerce-marketing-installed-extensions-card__item-links li{margin:0 0 0 30px}}.woocommerce-marketing-installed-extensions-card__item-links a{font-weight:600;color:#007cba!important;color:var(--wp-admin-theme-color)!important;text-decoration:none;font-size:14px}.woocommerce-marketing-installed-extensions-card .woocommerce-admin-marketing-product-icon{align-self:flex-start;margin-right:14px;margin-top:2px}.woocommerce-marketing-installed-extensions-card__item-text{min-width:0;flex-grow:2;margin:0 0 10px;width:100%}@media(min-width:601px){.woocommerce-marketing-installed-extensions-card__item-text{margin:0;width:auto}}.components-button.woocommerce-admin-marketing-button:not([disabled]){border-color:#007cba!important;border-color:var(--wp-admin-theme-color)!important;color:#007cba!important;color:var(--wp-admin-theme-color)!important}.woocommerce-admin-marketing-card .components-card__header .woocommerce-admin-marketing-card-subtitle{font-weight:400;margin-top:4px;color:#757575}.woocommerce-admin-marketing-product-icon{position:relative;overflow:hidden;width:36px;height:36px;border-radius:8px;background:#f0f0f0;color:#f0f0f0}.woocommerce-admin-marketing-product-icon svg{width:36px;height:36px}.woocommerce-marketing-slider,.woocommerce-marketing-slider>div{display:block;width:100%;overflow:hidden}.woocommerce-marketing-slider>div{white-space:normal;position:relative;height:100%}.woocommerce-marketing-slider__slide{top:0;left:0;width:100%;transition:transform .3s ease-in;position:relative}.woocommerce-marketing-slider .slide-enter,.woocommerce-marketing-slider .slide-exit{position:absolute}.woocommerce-marketing-slider.animate-right .slide-enter{transform:translateX(-100%)}.woocommerce-marketing-slider.animate-right .slide-enter-active,.woocommerce-marketing-slider.animate-right .slide-exit{transform:translateX(0)}.woocommerce-marketing-slider.animate-left .slide-enter,.woocommerce-marketing-slider.animate-right .slide-exit-active{transform:translateX(100%)}.woocommerce-marketing-slider.animate-left .slide-enter-active,.woocommerce-marketing-slider.animate-left .slide-exit{transform:translateX(0)}.woocommerce-marketing-slider.animate-left .slide-exit-active{transform:translateX(-100%)}@media screen and (prefers-reduced-motion:reduce){.woocommerce-marketing-slider .slide-enter-active,.woocommerce-marketing-slider .slide-exit-active{transition:none!important}}.woocommerce-marketing-recommended-extensions-card .components-card__body{padding:12px}.woocommerce-marketing-recommended-extensions-card__items{display:flex;flex-wrap:wrap}@media(min-width:601px){.woocommerce-marketing-recommended-extensions-card__items>.is-loading.woocommerce-marketing-recommended-extensions-item,.woocommerce-marketing-recommended-extensions-card__items>a{width:50%}}@media(min-width:961px){.woocommerce-marketing-recommended-extensions-card__items>.is-loading.woocommerce-marketing-recommended-extensions-item,.woocommerce-marketing-recommended-extensions-card__items>a{width:33.3%}.woocommerce-marketing-recommended-extensions-card__items--count-1>.is-loading.woocommerce-marketing-recommended-extensions-item,.woocommerce-marketing-recommended-extensions-card__items--count-1>a{width:100%}.woocommerce-marketing-recommended-extensions-card__items--count-2>.is-loading.woocommerce-marketing-recommended-extensions-item,.woocommerce-marketing-recommended-extensions-card__items--count-2>a{width:50%}.woocommerce-marketing-recommended-extensions-card__items--count-4>.is-loading.woocommerce-marketing-recommended-extensions-item,.woocommerce-marketing-recommended-extensions-card__items--count-4>a{width:25%}}.woocommerce-marketing-recommended-extensions-item{display:block;padding:12px;text-decoration:none;position:relative}.woocommerce-marketing-recommended-extensions-item h4{color:#1e1e1e;margin:-2px 0 3px;font-size:16px;line-height:1.3;transition:color .2s ease}.woocommerce-marketing-recommended-extensions-item p{color:#757575;margin:0}.woocommerce-marketing-recommended-extensions-item:hover h4{color:#007cba;color:var(--wp-admin-theme-color)}.woocommerce-marketing-recommended-extensions-item:hover p{color:#1e1e1e}.woocommerce-marketing-recommended-extensions-item__text{padding-left:46px}.woocommerce-marketing-recommended-extensions-item .woocommerce-admin-marketing-product-icon{position:absolute;top:12px;left:12px}.woocommerce-marketing-recommended-extensions-item.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;min-height:16px}.woocommerce-marketing-recommended-extensions-item.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-marketing-recommended-extensions-item.is-loading .is-placeholder{animation:none}}.woocommerce-marketing-recommended-extensions-item.is-loading h4{width:80%}.woocommerce-marketing-recommended-extensions-item.is-loading p :last-child{width:60%}.woocommerce-marketing-knowledgebase-card .woocommerce-marketing-slider{margin:0 0 14px}.woocommerce-marketing-knowledgebase-card .woocommerce-pagination{justify-content:flex-start;flex-direction:row}.woocommerce-marketing-knowledgebase-card__page{width:100%;display:flex}@media(max-width:960px){.woocommerce-marketing-knowledgebase-card__page{display:block}}.woocommerce-marketing-knowledgebase-card__post{display:flex;flex-wrap:wrap;width:100%;text-decoration:none}.woocommerce-marketing-knowledgebase-card__post:not(:last-child){margin-bottom:16px}@media(min-width:961px){.woocommerce-marketing-knowledgebase-card__post{flex-wrap:nowrap;width:50%}.woocommerce-marketing-knowledgebase-card__post:not(:last-child){margin-bottom:0;padding-right:16px}}.woocommerce-marketing-knowledgebase-card__post-img{width:100%;padding-bottom:52%;overflow:hidden;position:relative;flex:none;border-radius:3px}@media(min-width:961px){.woocommerce-marketing-knowledgebase-card__post-img{width:144px;height:103px;margin-right:16px;padding-bottom:0}}.woocommerce-marketing-knowledgebase-card__post-img img{position:absolute;top:0;right:0;display:block;width:100%}@media(min-width:961px){.woocommerce-marketing-knowledgebase-card__post-img img{position:absolute;top:0;right:0;height:100%;width:auto}}.woocommerce-marketing-knowledgebase-card__post-text{margin:10px 0 0;flex:1}@media(min-width:961px){.woocommerce-marketing-knowledgebase-card__post-text{margin:0}}.woocommerce-marketing-knowledgebase-card__post h3{margin-top:0;margin-bottom:6px;font-size:16px;line-height:24px;font-weight:600;white-space:normal;color:#1e1e1e;transition:color .2s ease}.woocommerce-marketing-knowledgebase-card__post:hover h3{color:#007cba;color:var(--wp-admin-theme-color)}.woocommerce-marketing-knowledgebase-card__post-meta{display:flex;justify-content:flex-start;align-items:center;font-size:12px;line-height:17px;color:#757575;margin:0;padding:0;height:17px}.woocommerce-marketing-knowledgebase-card__post-meta .woocommerce-gravatar{margin:1px 0 0 5px}.woocommerce-marketing-knowledgebase-card__post.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent}.woocommerce-marketing-knowledgebase-card__post.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-marketing-knowledgebase-card__post.is-loading .is-placeholder{animation:none}}.woocommerce-marketing-knowledgebase-card__post.is-loading p{width:40%}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-marketing-overview-welcome-card{position:relative}.woocommerce-marketing-overview-welcome-card .components-card__body{display:flex;justify-content:center;align-items:center;flex-wrap:wrap;padding:22px}@media(min-width:601px){.woocommerce-marketing-overview-welcome-card .components-card__body{flex-wrap:nowrap}}@media(min-width:961px){.woocommerce-marketing-overview-welcome-card .components-card__body{padding:32px 108px}}.woocommerce-marketing-overview-welcome-card__hide-button{display:flex;align-items:center;padding:8px;margin:0;border:none;background:none;color:#757575;overflow:hidden;border-radius:4px;position:absolute;top:10px;right:10px}.woocommerce-marketing-overview-welcome-card__hide-button svg{fill:currentColor;outline:none}.woocommerce-marketing-overview-welcome-card__hide-button:not(:disabled):not([aria-disabled=true]):not(.is-default):hover{background-color:#fff;color:#1e1e1e;box-shadow:inset 0 0 0 1px #ccc,inset 0 0 0 2px #fff,0 1px 1px rgba(30,30,30,.2)}.woocommerce-marketing-overview-welcome-card__hide-button:not(:disabled):not([aria-disabled=true]):not(.is-default):active{outline:none;background-color:#fff;color:#1e1e1e;box-shadow:inset 0 0 0 1px #ccc,inset 0 0 0 2px #fff}.woocommerce-marketing-overview-welcome-card__hide-button:disabled:focus,.woocommerce-marketing-overview-welcome-card__hide-button[aria-disabled=true]:focus{box-shadow:none}.woocommerce-marketing-overview-welcome-card h3{font-size:20px;line-height:26px;font-weight:400;text-align:center;margin:1em 0 0}@media(min-width:601px){.woocommerce-marketing-overview-welcome-card h3{text-align:left;margin:0 0 0 20px}}@media(min-width:961px){.woocommerce-marketing-overview-welcome-card h3{font-size:24px;line-height:32px}}.woocommerce-marketing-overview-welcome-card img{width:231px;flex:none} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/4.style.css b/packages/woocommerce-admin/dist/chunks/4.style.css new file mode 100644 index 0000000..f4ec473 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/4.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-layout__activity-panel-header{height:50px;background:#e0e0e0;padding:16px;display:flex;justify-content:space-between;align-items:center}@media(min-width:783px){.woocommerce-layout__activity-panel-header{padding:16px 24px}}.woocommerce-layout__activity-panel-header h3{font-size:13px;font-weight:600;line-height:16px;margin:0;padding:0}.woocommerce-layout__activity-panel-header .woocommerce-ellipsis-menu__toggle.components-button:not(:disabled):not([aria-disabled=true]):focus,.woocommerce-layout__activity-panel-header .woocommerce-ellipsis-menu__toggle.components-button:not(:disabled):not([aria-disabled=true]):hover{box-shadow:none;border-radius:10px;background:#ccc}.woocommerce-layout__inbox-title{color:#1e1e1e;display:flex;align-items:center}.woocommerce-layout__inbox-subtitle{color:#757575}.woocommerce-layout__inbox-badge{margin-left:6px;background-color:#757575;border-radius:13px;padding:0 6px;color:#fff;display:inline-block;text-align:center;vertical-align:top} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/45.style.css b/packages/woocommerce-admin/dist/chunks/45.style.css new file mode 100644 index 0000000..6cb5b43 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/45.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-recommended-payments-card{margin:0 15px 10px 0;animation:isLoaded;animation-duration:.25s}.woocommerce-recommended-payments-card .woocommerce-list__item>.woocommerce-list__item-inner{align-items:flex-start}.woocommerce-recommended-payments-card .woocommerce-list__item:hover{background-color:#fff}.woocommerce-recommended-payments-card .woocommerce-list__item:hover .woocommerce-list__item-title{color:#1e1e1e}.woocommerce-recommended-payments-card .woocommerce-list__item-title{font-size:14px;color:#1e1e1e;font-weight:600}.woocommerce-recommended-payments-card .woocommerce-review-activity-card__section-controls{text-align:center}.woocommerce-recommended-payments-card .woocommerce-pill{margin-left:4px;padding:2px 8px}@media(max-width:480px){.woocommerce-recommended-payments-card .woocommerce-pill{margin-top:4px;margin-bottom:4px}}.woocommerce-recommended-payments-card .components-card__footer .gridicon{margin-left:4px}.woocommerce-recommended-payments-card .woocommerce-list__item-enter{opacity:0;max-height:100vh;transform:none}.woocommerce-recommended-payments-card .woocommerce-list__item-enter-active{opacity:1;transition:opacity .2s}.woocommerce-recommended-payments-card .woocommerce-list__item-after .components-button{margin-left:12px}.woocommerce-recommended-payments-card .woocommerce-list__item-text,.woocommerce-recommended-payments-card .woocommerce-recommended-payments__header-heading{max-width:749px}@media(max-width:782px){.woocommerce-recommended-payments-card{margin:0 0 10px}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/47.style.css b/packages/woocommerce-admin/dist/chunks/47.style.css new file mode 100644 index 0000000..1a547dd --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/47.style.css @@ -0,0 +1 @@ +.woocommerce-profile-wizard__business-details__free-features{display:flex;flex-direction:column;justify-content:center}.woocommerce-profile-wizard__business-details__free-features .woocommerce-admin__business-details__selective-extensions-bundle .woocommerce-admin__business-details__selective-extensions-bundle__category{font-size:11px;text-transform:uppercase;margin:25px 0 0 30px;color:#000;font-weight:500}.woocommerce-profile-wizard__business-details__free-features .woocommerce-admin__business-details__selective-extensions-bundle .woocommerce-admin__business-details__selective-extensions-bundle__extension{display:flex;padding:24px 30px;border-bottom:1px solid #e0e0e0;align-items:center}.woocommerce-profile-wizard__business-details__free-features .woocommerce-admin__business-details__selective-extensions-bundle .woocommerce-admin__business-details__selective-extensions-bundle__description{font-size:16px;color:#1e1e1e;margin:0;line-height:18px}.woocommerce-profile-wizard__business-details__free-features .woocommerce-admin__business-details__selective-extensions-bundle .woocommerce-admin__business-details__free-badge{padding:0 8px;border:1px solid #757575;border-radius:16px;margin-left:8px;color:#757575;font-size:12px;line-height:18px;height:20px}.woocommerce-profile-wizard__business-details__free-features .woocommerce-admin__business-details__selective-extensions-bundle .woocommerce-admin__business-details__selective-extensions-bundle__link{text-decoration:none}.woocommerce-profile-wizard__business-details__free-features .woocommerce-card__body{padding:0}.woocommerce-profile-wizard__business-details__free-features .components-base-control .components-base-control__field{margin-bottom:0;margin-right:21px}.woocommerce-profile-wizard__business-details__free-features .components-base-control .components-base-control__field .components-checkbox-control__input-container{margin-right:0;vertical-align:baseline}.woocommerce-profile-wizard__business-details__free-features .woocommerce-admin__business-details__selective-extensions-bundle__expand{margin-left:9px}.woocommerce-profile-wizard__business-details__free-features .woocommerce-profile-wizard__business-details__free-features__illustration{flex:1;width:100%;display:flex;justify-content:center;margin-top:34px;margin-bottom:18px}.woocommerce-profile-wizard__business-details__free-features .woocommerce-profile-wizard__business-details__free-features__illustration .fill-theme-color{fill:#007cba;fill:var(--wp-admin-theme-color)}.woocommerce-profile-wizard__business-details__free-features .woocommerce-profile-wizard__business-details__free-features__action{padding-top:12px;display:flex;justify-content:center;margin-bottom:12px}.woocommerce-profile-wizard__container.business-features .components-tab-panel__tabs{justify-content:center}.woocommerce-profile-wizard__container.business-features .components-tab-panel__tabs .components-tab-panel__tabs-item.is-disabled{color:#949494;cursor:default;pointer-events:none}.woocommerce-profile-wizard__container.business-features .components-tab-panel__tabs .components-tab-panel__tabs-item.is-active{pointer-events:none}.woocommerce-profile-wizard__container.business-features .components-card__body{padding:16px 16px 0}.business-details.woocommerce-profile-wizard__container .woocommerce-admin__business-details__spinner,.business-features.woocommerce-profile-wizard__container .woocommerce-admin__business-details__spinner{display:flex;justify-content:center}.business-details.woocommerce-profile-wizard__container .woocommerce-profile-wizard__step-header,.business-features.woocommerce-profile-wizard__container .woocommerce-profile-wizard__step-header{margin-top:28px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .woocommerce-product-wizard__product-types-label{display:inline-block;margin-right:4px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .woocommerce-profile-wizard__checkbox-group .woocommerce-profile-wizard__checkbox{display:flex;align-items:center;min-height:64px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .woocommerce-profile-wizard__checkbox-group .woocommerce-profile-wizard__checkbox .components-button{padding:0;height:auto}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .components-base-control__field,.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .components-checkbox-control__label{display:flex;width:100%;align-items:center}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .components-checkbox-control__label .woocommerce-pill{margin-left:auto}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .components-popover .components-popover__content{min-width:360px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .woocommerce-profile-wizard__product-types-pricing-toggle.woocommerce-profile-wizard__checkbox{display:flex;align-items:center;justify-content:flex-end;color:#949494;margin-bottom:16px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .woocommerce-profile-wizard__product-types-pricing-toggle.woocommerce-profile-wizard__checkbox label{display:inline-flex;align-items:center;margin:auto}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .woocommerce-profile-wizard__product-types-pricing-toggle.woocommerce-profile-wizard__checkbox .components-form-toggle{display:inline-flex;margin-left:16px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types__spinner{text-align:center}@media screen and (max-width:438px){.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .woocommerce-product-wizard__product-types-label{width:min-content}}@media screen and (max-width:375px){.woocommerce-profile-wizard__body .woocommerce-profile-wizard__product-types .woocommerce-pill{white-space:nowrap}}.woocommerce-profile-wizard__store-details .woocommerce-admin__store-details__spinner{display:flex;justify-content:center}.woocommerce-profile-wizard__store-details .components-popover .components-popover__content{min-width:360px}.woocommerce-profile-wizard__newsletter-signup .components-base-control__field{display:flex;align-items:center}.woocommerce-profile-wizard__newsletter-signup .woocommerce-profile-wizard__powered-by-mailchimp{color:#a7aaad}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container>.woocommerce-profile-wizard__themes-tab-panel{margin-bottom:24px}@media(min-width:783px){.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container>.woocommerce-profile-wizard__themes-tab-panel{max-width:810px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container>.woocommerce-profile-wizard__themes-tab-panel .woocommerce-profile-wizard__themes{display:grid;grid-gap:24px;grid-template-columns:1fr 1fr}}.woocommerce-profile-wizard__themes-tab-panel .components-tab-panel__tabs{display:flex;justify-content:space-between;background:#fff;box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);border-radius:3px;margin-top:24px;margin-bottom:24px}.woocommerce-profile-wizard__themes-tab-panel .components-tab-panel__tabs button{border:0;color:#646970;display:flex;align-items:center;justify-content:center;background:transparent;height:48px;width:100%;font-size:14px;font-size:.875rem;font-weight:500;outline:none;padding:0 24px}p.woocommerce-profile-wizard__themes-skip-this-step{text-align:center}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__theme.components-card{overflow:hidden}@media(min-width:783px){.woocommerce-profile-wizard__body .woocommerce-profile-wizard__theme.components-card{margin:0}}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__theme.components-card .woocommerce-profile-wizard__theme-image{width:100%;height:300px;background-size:cover}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__theme.components-card .woocommerce-profile-wizard__theme-name{margin-top:auto;margin-bottom:8px;font-size:24px;font-size:1.5rem;font-weight:400}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__theme.components-card .woocommerce-profile-wizard__theme-name svg{max-width:18px;height:18px;margin-left:8px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__theme.components-card .woocommerce-profile-wizard__theme-name svg path{fill:#d63638}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__theme.components-card .woocommerce-profile-wizard__theme-status{margin:0;font-size:14px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__theme.components-card .woocommerce-profile-wizard__theme-learn-more{display:inline-block}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card{margin:0;position:relative}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card.is-uploading .woocommerce-theme-uploader__dropzone-wrapper{min-height:382px}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card .woocommerce-theme-uploader__dropzone-wrapper{display:flex;flex-direction:column;align-items:center;justify-content:center;border-radius:2px;background:#f6f6f6;border:1px dashed #b0b5b8}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card .components-form-file-upload{flex:1 1 auto;width:100%;display:flex}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card .components-form-file-upload>.components-button{flex:1 1 auto;flex-direction:column;justify-content:center;margin:0;width:100%;height:100%;min-height:380px}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card .components-form-file-upload>.components-button>.gridicon{width:48px;height:48px}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card .components-form-file-upload>.components-button>.gridicon path{fill:#50575d}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card .components-form-file-upload>.components-button .dashicons-upload{display:none}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card .woocommerce-theme-uploader__title{margin:8px 0;font-size:24px;font-size:1.5rem;font-weight:400}.woocommerce-profile-wizard__body .woocommerce-theme-uploader.components-card p{font-size:14px;font-size:.875rem;margin:0}.woocommerce-theme-preview{position:fixed;top:0;left:0;width:100%;height:100%;max-width:100%!important;display:flex;flex-direction:column}.woocommerce-theme-preview .woocommerce-theme-preview__toolbar{background:#fff;flex-direction:row;display:flex;height:60px;border-bottom:1px solid #dcdcde;padding-left:16px;padding-right:16px;align-items:center}.woocommerce-theme-preview .woocommerce-theme-preview__toolbar .is-button.is-primary{height:40px;margin:0}@media(max-width:782px){.woocommerce-theme-preview .woocommerce-theme-preview__toolbar .is-button.is-primary{margin-left:auto}}.woocommerce-theme-preview .woocommerce-theme-preview__theme-name{padding-left:16px;color:#1d2327;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%}.woocommerce-theme-preview .woocommerce-theme-preview__close{padding:0 16px 0 0;color:#646970}.woocommerce-theme-preview .woocommerce-theme-preview__devices{margin-left:auto;margin-right:16px}.woocommerce-theme-preview .woocommerce-theme-preview__devices .woocommerce-theme-preview__device{padding:12px;color:#646970;margin:0;border-radius:50%}.woocommerce-theme-preview .woocommerce-theme-preview__devices .woocommerce-theme-preview__device.is-selected,.woocommerce-theme-preview .woocommerce-theme-preview__devices .woocommerce-theme-preview__device:focus{background:#646970;color:#fff}@media(max-width:782px){.woocommerce-theme-preview .woocommerce-theme-preview__devices{display:none}}.woocommerce-theme-preview .woocommerce-web-preview{flex:1;padding:40px 16px;overflow:scroll}.woocommerce-theme-preview .woocommerce-web-preview .woocommerce-web-preview__iframe-wrapper{height:100%;border-radius:3px;box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);overflow:hidden;margin:0 auto}.woocommerce-theme-preview .woocommerce-web-preview .woocommerce-web-preview__iframe-wrapper iframe{display:block}.woocommerce-theme-preview .woocommerce-web-preview.is-mobile .woocommerce-web-preview__iframe-wrapper{max-width:360px}.woocommerce-theme-preview .woocommerce-web-preview.is-tablet .woocommerce-web-preview__iframe-wrapper{max-width:768px}.woocommerce-theme-preview .woocommerce-web-preview.is-desktop{width:100%;padding:0}.woocommerce-theme-preview .woocommerce-web-preview.is-desktop .woocommerce-web-preview__iframe-wrapper{border-radius:0;box-shadow:none}.woocommerce-theme-preview-active{overflow:hidden}.woocommerce-theme-preview-active .woocommerce-profile-wizard__header{display:none}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__step-header{text-align:center;margin:16px 0 24px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__step-header h2{margin-bottom:4px;color:#1e1e1e}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__step-header p{color:#757575;display:flex;align-items:center;justify-content:center;line-height:1.5em}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__step-header p .woocommerce-profile-wizard__tooltip-icon{height:16px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__header{height:60px;border-bottom:1px solid #dcdcde;display:flex;align-items:center;justify-content:center;background:#fff}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__header-subtitle{font-weight:400;text-align:center;color:#757575;font-size:16px;line-height:24px;margin-top:8px;margin-bottom:32px;margin-right:8px;display:flex;justify-content:center}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__intro-paragraph{margin-top:5px;margin-bottom:18px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container{margin:36px auto 16px;text-align:left}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container>*{max-width:504px;margin-left:auto;margin-right:auto}@media(max-width:782px){.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container{padding-left:16px;padding-right:16px;margin-bottom:72px;margin-top:0}}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container .components-popover__content>div{padding:16px 24px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container .woocommerce-profile-wizard__footer{margin:34px auto;display:flex;justify-content:center}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container .woocommerce-profile-wizard__footer-link{display:flex;text-decoration:none}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container .woocommerce-profile-wizard__footnote{max-width:424px;margin:32px auto 25px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__container .woocommerce-profile-wizard__footnote p{color:#757575;text-align:center;margin-bottom:12px}.woocommerce-profile-wizard__body #woocommerce-layout__primary{text-align:center;margin:0;width:100%}.woocommerce-profile-wizard__body .woocommerce-layout .woocommerce-layout__main{padding-right:0}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__error{display:block;padding:16px 24px;font-size:12px;color:#d63638}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__benefit{display:flex}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__benefit svg:first-child{width:24px;min-width:24px;margin-right:24px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__benefit .woocommerce-profile-wizard__benefit-title{font-size:16px;font-weight:400;margin-top:0;margin-bottom:8px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__benefit .woocommerce-profile-wizard__benefit-content{margin-left:16px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__benefit .woocommerce-profile-wizard__benefit-content p{padding-bottom:16px;margin-top:0;border-bottom:1px solid #dcdcde;font-size:14px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__benefit .woocommerce-profile-wizard__benefit-toggle{padding-top:36px;margin-left:16px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__benefit:last-child p{border-bottom:0;margin-bottom:0}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__business-extension{background:#f6f6f6;height:100%;padding:12px 4px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__business-extension img{box-sizing:unset;max-width:100px;max-height:50%;vertical-align:middle}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__tracking .woocommerce-profile-wizard__tracking-checkbox{margin-top:16px}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__tracking .components-form-toggle{display:none}@media(max-width:782px){.woocommerce-profile-wizard__body .woocommerce-profile-wizard__tracking{display:flex;flex-direction:row-reverse;align-items:center;justify-content:flex-end}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__tracking .components-form-toggle{display:inline-block}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__tracking .components-checkbox-control__input{display:none}}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__checkbox{margin-top:0;margin-bottom:0;position:relative;padding:12px 24px;min-height:62px;border-bottom:1px solid #f0f0f0;display:flex;align-items:center}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__checkbox .components-base-control{position:relative}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__checkbox .components-base-control__field{width:100%;margin:0}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__checkbox label.components-checkbox-control__label{font-size:16px;margin-left:0}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__checkbox .components-base-control__help{margin-left:48px;font-style:normal;color:#949494;font-size:14px;line-height:20px;margin-top:3px;margin-bottom:0}.woocommerce-profile-wizard__body .woocommerce-profile-wizard__checkbox svg.dashicon.components-checkbox-control__checked{left:1px;top:-1px}@media(max-width:600px){.woocommerce-profile-wizard__body svg.dashicon.components-checkbox-control__checked{left:-2px;top:-1px;width:21px;height:21px}}.woocommerce-profile-wizard__body .woocommerce-select-control__control{margin:16px 0;padding-right:40px;box-shadow:0 2px 6px rgba(0,0,0,.05)}.woocommerce-profile-wizard__body .woocommerce-select-control__control.is-active{border-color:#007cba;border-color:var(--wp-admin-theme-color)}.woocommerce-profile-wizard__body .woocommerce-select-control__control .components-base-control__label{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:calc(100% - 56px)}.woocommerce-profile-wizard__body .woocommerce-select-control__control:after{display:block;pointer-events:none;cursor:pointer;position:absolute;float:right;line-height:56px;font-family:dashicons,sans-serif;font-size:20px;content:"";z-index:1;height:24px;width:24px;margin-top:0;top:0;right:16px;bottom:16px;color:#000}.woocommerce-profile-wizard__body #wpadminbar{display:none}.woocommerce-profile-wizard__body #wpbody{padding-top:0}.woocommerce-profile-wizard__plugins-card .woocommerce-profile-wizard__plugins-actions{text-align:left;margin-left:44px}.woocommerce-profile-wizard__plugins-card .woocommerce-profile-wizard__plugins-actions button.is-button{height:40px;min-width:auto;display:initial;margin:16px 12px 0 0}.woocommerce-profile-wizard__header svg>g{transform:none}@media(max-width:782px){.woocommerce-profile-wizard__header{position:fixed;z-index:999;width:100%;bottom:0;border-top:1px solid #dcdcde;border-bottom:none}}.woocommerce-profile-wizard__header .woocommerce-stepper{margin:0 16px;width:100%}.woocommerce-profile-wizard__header .woocommerce-stepper__steps{margin:0}.woocommerce-profile-wizard__tooltip-icon{color:#50575e;display:flex;align-items:center;margin-left:4px;cursor:help}.woocommerce-business-extensions{display:flex;align-items:center;width:100%}.woocommerce-business-extensions label{display:flex;align-items:center}.woocommerce-business-extensions .components-checkbox-control__input-container{margin-right:16px}.woocommerce-business-extensions .woocommerce-business-extensions__label-subtext{display:block;color:#949494}.woocommerce-business-extensions .woocommerce-business-extensions__popover-wrapper{margin-left:auto} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/48.style.css b/packages/woocommerce-admin/dist/chunks/48.style.css new file mode 100644 index 0000000..5e16525 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/48.style.css @@ -0,0 +1 @@ +.woocommerce-dismissable-list{margin:0 20px 10px;animation:isLoaded;animation-duration:.25s}@media(min-width:782px){.woocommerce-dismissable-list{margin-left:0;margin-right:0}}.woocommerce-dismissable-list .woocommerce-dismissable-list__controls{text-align:center}.woocommerce-services-item__logo{width:36px;height:auto}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-recommended-shipping-extensions__more_options_cta .gridicon{margin-left:4px}.woocommerce-recommended-shipping-extensions .woocommerce-list__item>.woocommerce-list__item-inner{align-items:flex-start}.woocommerce-recommended-shipping-extensions .woocommerce-list__item:hover{background-color:#fff}.woocommerce-recommended-shipping-extensions .woocommerce-list__item:hover .woocommerce-list__item-title{color:#1e1e1e}.woocommerce-recommended-shipping-extensions .woocommerce-list__item-title{font-size:14px;color:#1e1e1e;font-weight:600}.woocommerce-recommended-shipping-extensions .woocommerce-pill{margin-left:4px;margin-top:4px;margin-bottom:4px;padding:2px 8px}@media(min-width:480px){.woocommerce-recommended-shipping-extensions .woocommerce-pill{margin-top:0;margin-bottom:0}}.woocommerce-recommended-shipping-extensions .woocommerce-list__item-after .components-button{margin-left:12px}.woocommerce-recommended-shipping-extensions .woocommerce-list__item-text,.woocommerce-recommended-shipping-extensions .woocommerce-recommended-shipping__header-heading{max-width:749px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/49.style.css b/packages/woocommerce-admin/dist/chunks/49.style.css new file mode 100644 index 0000000..21b6047 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/49.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-store-alerts{position:relative;margin:0 0 40px;margin-right:var(--large-gap);padding:24px;border:0;box-shadow:0 0 8px -2px rgba(0,0,0,.3)}.woocommerce-store-alerts:before{content:"";position:absolute;top:0;left:0;width:4px;height:100%;background:transparent}.woocommerce-store-alerts .components-card__header{margin-bottom:12px}.woocommerce-store-alerts a.components-button{min-height:36px;padding:4px 16px}@media(max-width:782px){.woocommerce-store-alerts a.components-button{min-height:36px;font-size:16px;line-height:1.625;padding:5px 16px}}.woocommerce-store-alerts a.components-button+.components-button{margin-left:10px}.woocommerce-store-alerts a.components-button.is-button{color:#757575}@media(max-width:782px){.woocommerce-store-alerts{margin-bottom:24px;padding:16px}.woocommerce-store-alerts .components-card__header{display:flex;flex-direction:column-reverse;align-items:flex-start}}.woocommerce-embed-page .woocommerce-store-alerts{margin:40px 20px 20px}@media(max-width:782px){.woocommerce-embed-page .woocommerce-store-alerts{margin-top:24px}}.is-alert-error:before{background-color:#dc3232}.is-alert-error .components-card__header h2 svg{color:#dc3232}.is-alert-update:before{background-color:#11a0d2}.is-alert-update .components-card__header h2 svg{color:#11a0d2}.components-card__body .woocommerce-store-alerts__message{margin-bottom:16px}.woocommerce-store-alerts__pagination{display:inline-flex;align-items:center;border:1px solid #b5bfc9;border-radius:4px;background:#f0f2f4;margin-left:16px;min-width:120px}.woocommerce-store-alerts__pagination .components-button{padding:4px/*!rtl:ignore*/}.rtl .woocommerce-store-alerts__pagination .components-button .arrow-left-icon,.rtl .woocommerce-store-alerts__pagination .components-button .arrow-right-icon{transform:scaleX(-1)}.woocommerce-store-alerts__pagination .woocommerce-store-alerts__pagination-label{padding:5px 12px;border-color:#b5bfc9;border-style:solid;border-width:0 1px;flex:1 1;font-size:14px;font-size:.875rem}@media(max-width:782px){.woocommerce-store-alerts__pagination{margin-left:0;margin-bottom:14px}.woocommerce-store-alerts__pagination .woocommerce-store-alerts__pagination-label{text-align:center}}.woocommerce-store-alerts__pagination button:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.woocommerce-store-alerts__pagination button:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.woocommerce-store-alerts.is-loading:before{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent}.woocommerce-store-alerts.is-loading:before:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-store-alerts.is-loading:before{animation:none}}.woocommerce-store-alerts.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;height:16px}.woocommerce-store-alerts.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-store-alerts.is-loading .is-placeholder{animation:none}}.woocommerce-store-alerts.is-loading .components-card__header .is-placeholder:first-child{width:340px;height:32px}.woocommerce-store-alerts.is-loading .components-card__header .is-placeholder:nth-child(2){width:137px;height:38px}.woocommerce-store-alerts.is-loading .components-card__footer .is-placeholder{min-width:120px;height:30px}.woocommerce-store-alerts.is-loading .components-card__body .is-placeholder:first-child{width:75%}.woocommerce-store-alerts.is-loading .components-card__body .is-placeholder:nth-child(2){width:45%}.woocommerce-store-alerts.is-loading .woocommerce-store-alerts__actions .is-placeholder{width:110px;height:26px}@media(max-width:782px){.woocommerce-store-alerts.is-loading .components-card__header h2{width:100%}.woocommerce-store-alerts.is-loading .components-card__header h2 .is-placeholder{width:50%}.woocommerce-store-alerts.is-loading .components-card__footer{margin-bottom:14px}}.woocommerce-store-alerts__snooze{display:inline-flex;margin-left:10px}.woocommerce-store-alerts__snooze .components-select-control__input{vertical-align:initial;min-height:36px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/5.style.css b/packages/woocommerce-admin/dist/chunks/5.style.css new file mode 100644 index 0000000..e41fe15 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/5.style.css @@ -0,0 +1 @@ +.woocommerce-layout__activity-panel-content .woocommerce-inbox-message,.woocommerce-layout__activity-panel-content .woocommerce-notification-panels>div{margin-top:24px}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications{border-top:1px solid #e0e0e0}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications>div{border:none;margin-bottom:0}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification h3{color:#007cba;color:var(--wp-admin-theme-color);font-weight:700;font-size:14px}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification div,.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification p{color:#757575}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification .woocommerce-abbreviated-card__content{padding:12px 24px 12px 0}.woocommerce-layout__activity-panel-content .woocommerce-abbreviated-notifications .woocommerce-abbreviated-notification .woocommerce-abbreviated-card__content>:not(:first-child){margin-top:4px}.woocommerce-activity-panel .woocommerce-activity-card{position:relative;padding:24px;padding:var(--main-gap);background:#fff;border-bottom:1px solid #e0e0e0;color:#757575;font-size:13px;font-size:.8125rem}.woocommerce-activity-panel .woocommerce-activity-card:not(.woocommerce-empty-activity-card){display:grid;grid-template-columns:50px 1fr;grid-template-areas:"icon header" "icon body" "icon actions"}.woocommerce-activity-panel .woocommerce-activity-card__button{display:block;height:unset;background:none;align-items:unset;transition:unset;text-align:left;width:100%;padding:0}.woocommerce-activity-card__unread{position:absolute;top:18px;top:calc(var(--main-gap) - 6px);right:18px;right:calc(var(--main-gap) - 6px);width:6px;height:6px;border-radius:50%;background:#ca4a1f}.woocommerce-activity-card__icon{-ms-grid-row:1;-ms-grid-row-span:3;-ms-grid-column:1;grid-area:icon;fill:#e0e0e0}.woocommerce-activity-card__header{margin-bottom:16px;display:flex;flex-direction:column}.woocommerce-activity-card__header .woocommerce-activity-card__title{margin:0;font-size:14px;font-size:.875rem;order:2}.woocommerce-empty-activity-card .woocommerce-activity-card__header .woocommerce-activity-card__title{color:#1e1e1e;font-style:normal;line-height:24px;font-weight:400}.woocommerce-activity-card__button .woocommerce-activity-card__header .woocommerce-activity-card__title{margin-bottom:8px}.woocommerce-activity-card__header .woocommerce-activity-card__title a{text-decoration:none}.woocommerce-activity-card__header .woocommerce-activity-card__date{color:#757575;font-size:12px;font-size:.75rem;margin-bottom:12px;order:1}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{order:3}.woocommerce-activity-card__button .woocommerce-activity-card__header .woocommerce-activity-card__subtitle{margin-bottom:4px}@media(min-width:783px){.woocommerce-activity-card__header{-ms-grid-row:1;-ms-grid-column:2;grid-area:header;display:grid;grid-template:"title date" "subtitle date"/1fr auto}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__header{-ms-grid-row:1;-ms-grid-column:1}.woocommerce-activity-card__header .woocommerce-activity-card__title{grid-area:title}.woocommerce-activity-card__header .woocommerce-activity-card__date{display:block;grid-area:date;justify-self:end;margin-bottom:0}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{grid-area:subtitle}}@media (min-width:783px){.woocommerce-activity-card__header .woocommerce-activity-card__title{-ms-grid-row:1;-ms-grid-column:1}.woocommerce-activity-card__header .woocommerce-activity-card__date{-ms-grid-row:1;-ms-grid-row-span:2;-ms-grid-column:2}.woocommerce-activity-card__header .woocommerce-activity-card__subtitle{-ms-grid-row:2;-ms-grid-column:1}}.woocommerce-activity-card__body{-ms-grid-row:2;-ms-grid-column:2;grid-area:body}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__body{-ms-grid-row:2;-ms-grid-column:1}.woocommerce-activity-card__body>p:first-child{margin-top:0}.woocommerce-activity-card__body>p:last-child{margin-bottom:0}.woocommerce-empty-activity-card .woocommerce-activity-card__body{color:#757575;font-style:normal;font-weight:400;font-size:13px;font-size:.8125rem;line-height:20px}.woocommerce-activity-card__actions{-ms-grid-row:3;-ms-grid-column:2;grid-area:actions;margin-top:16px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card>.woocommerce-activity-card__actions{-ms-grid-row:3;-ms-grid-column:1}.woocommerce-activity-card__actions>*+*{margin-left:.5em}.woocommerce-activity-card__actions .components-button{height:24px;padding:4px 10px;font-size:11px;font-size:.6875rem}.woocommerce-activity-card__actions .components-button.is-destructive:not(:hover){box-shadow:none}.woocommerce-activity-card.is-loading .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;height:16px}.woocommerce-activity-card.is-loading .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-activity-card.is-loading .is-placeholder{animation:none}}.woocommerce-activity-card.is-loading .woocommerce-activity-card__title{width:80%}.woocommerce-activity-card.is-loading .woocommerce-activity-card__subtitle{margin-top:4px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__date{width:100%;margin-bottom:16px}@media(min-width:783px){.woocommerce-activity-card.is-loading .woocommerce-activity-card__date{text-align:right;margin-bottom:0}}.woocommerce-activity-card.is-loading .woocommerce-activity-card__date .is-placeholder{width:68px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__icon{margin-right:24px;margin-right:var(--main-gap)}.woocommerce-activity-card.is-loading .woocommerce-activity-card__icon .is-placeholder{height:33px;width:33px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__body .is-placeholder{width:100%;margin-bottom:4px}.woocommerce-activity-card.is-loading .woocommerce-activity-card__body .is-placeholder:last-of-type{width:65%;margin-bottom:0}.woocommerce-activity-card.is-loading .woocommerce-activity-card__actions .is-placeholder{width:91px;height:24px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card{grid-template-columns:1fr;grid-template-areas:"header" "body" "actions"}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__icon{display:none}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-flag{display:inline-block}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-order-activity-card .woocommerce-activity-card__subtitle span+span:before{content:" • "}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{grid-template-columns:72px 1fr;height:100%;opacity:1;padding:24px;padding:var(--main-gap)}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{transition:opacity .3s,height 0s,padding 0s}}@media(max-width:782px){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card{grid-template-columns:64px 1fr}}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card .woocommerce-activity-card__header{margin-bottom:12px}.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card.actioned{height:0;opacity:0;padding:0}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-activity-panel .woocommerce-activity-card.woocommerce-inbox-activity-card.actioned{transition:opacity .3s,height 0s .3s,padding 0s .3s}}.woocommerce-stock-activity-card__image-overlay__product{height:33px;position:relative;width:33px}.woocommerce-stock-activity-card__image-overlay__product.is-placeholder:before{background-color:#757575;border-radius:2px;content:"";position:absolute;left:0;right:0;bottom:0;top:0;opacity:.1}@media screen and (prefers-reduced-motion:no-preference){.woocommerce-stock-activity-card{transition:opacity .3s}}.woocommerce-stock-activity-card.is-dimmed{opacity:.7}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__stock-quantity{background:#f0f0f0;color:#757575;padding:3px 8px;border-radius:3px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__stock-quantity.out-of-stock{color:#d94f4f}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity{display:inline-flex;width:50px;margin-right:10px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input{border-radius:2px;height:30px}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]{-moz-appearance:textfield}.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]::-webkit-inner-spin-button,.woocommerce-stock-activity-card .woocommerce-stock-activity-card__edit-quantity input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.woocommerce-stock-activity-card .woocommerce-activity-card__subtitle{color:#757575;font-size:12px;font-size:.75rem}.woocommerce-empty-activity-card{background:#f0f0f0;margin:20px;border-bottom:unset}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}#activity-panel-inbox{margin:0 24px}.woocommerce-layout__inbox-panel-header{padding:24px}.woocommerce-homepage-column .woocommerce-layout__inbox-panel-header{padding:0 24px}.woocommerce-inbox-message-enter{opacity:0;max-height:0;transform:translateX(50%)}.woocommerce-inbox-message-enter-active{transition:opacity .5s,transform .5s,max-height .5s}.woocommerce-inbox-message-enter-active,.woocommerce-inbox-message-exit{opacity:1;max-height:100vh;transform:translateX(0)}.woocommerce-inbox-message-exit-active{opacity:0;max-height:0;transform:translateX(50%);transition:opacity .5s,transform .5s,max-height .5s} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/50.style.css b/packages/woocommerce-admin/dist/chunks/50.style.css new file mode 100644 index 0000000..9637c03 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/50.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-dashboard__store-performance{margin-bottom:24px}@media(max-width:782px){.woocommerce-dashboard__store-performance{border-width:0}}.woocommerce-dashboard__store-performance .woocommerce-summary__item{background-color:#fff}.woocommerce-dashboard__store-performance .woocommerce-summary__item:hover{background-color:#f0f0f0}.woocommerce-dashboard__store-performance .woocommerce-summary{background-color:#f0f0f0;margin:0}@media(max-width:782px){.woocommerce-dashboard__store-performance .woocommerce-summary.is-placeholder{border-top:0}.woocommerce-dashboard__store-performance .woocommerce-summary:not(.is-placeholder) .woocommerce-summary__item-container:first-child .woocommerce-summary__item{border-top:1px solid #ccc}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/51.style.css b/packages/woocommerce-admin/dist/chunks/51.style.css new file mode 100644 index 0000000..6dc21cd --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/51.style.css @@ -0,0 +1 @@ +.woocommerce-task-marketing .components-card__header{flex-direction:column;align-items:flex-start;display:flex!important}.woocommerce-task-marketing .components-card__header h2{align-self:start!important;color:#1e1e1e;font-size:20px;margin-bottom:0}.woocommerce-task-marketing .components-card__header span{margin-left:0;margin-top:8px;color:#757575}.woocommerce-plugin-list__plugin{display:flex;padding:24px;border-top:1px solid #e0e0e0}.woocommerce-plugin-list__plugin:first-child{border-top:0}.woocommerce-plugin-list__plugin h4{margin-bottom:12px;font-weight:600;color:#1e1e1e}.woocommerce-plugin-list__plugin p{color:#757575;font-weight:400}.woocommerce-plugin-list__plugin-logo{margin-right:45px;display:flex;align-items:center}.woocommerce-plugin-list__plugin-logo img{width:50px}.woocommerce-plugin-list__plugin-text{max-width:370px;margin-right:16px}.woocommerce-plugin-list__plugin-action{display:flex;align-items:center;margin-left:auto}.woocommerce-plugin-list__title{padding:12px 30px;background:#e0e0e0;position:relative}.woocommerce-plugin-list__title h3{font-weight:500;color:#000}@media(min-width:601px){.woocommerce-product-template-modal{min-width:565px}}.woocommerce-product-template-modal .woocommerce-product-template-modal__actions{padding-top:24px}.woocommerce-product-template-modal__list .components-base-control{margin:0 -24px}.woocommerce-product-template-modal__list .components-base-control .components-base-control__field .components-radio-control__option{display:flex;text-decoration:none;width:100%;align-items:center;padding:16px 24px;margin-bottom:0;box-sizing:border-box;border-bottom:1px solid #f0f0f0}.woocommerce-product-template-modal__list .components-base-control .components-base-control__field .components-radio-control__option:first-child{border-top:1px solid #f0f0f0}.woocommerce-product-template-modal__list .components-base-control .components-base-control__field .components-radio-control__option .components-radio-control__input{margin-right:16px;flex:none}.woocommerce-product-template-modal__list .components-base-control .components-base-control__field .components-radio-control__option .components-radio-control__input:checked:before{box-sizing:border-box}.woocommerce-product-template-modal__list .components-base-control .components-base-control__field .components-radio-control__option .components-radio-control__input:focus{box-shadow:none}.woocommerce-product-template-modal__list .woocommerce-product-template-modal__list-title{color:#1e1e1e;display:block}.woocommerce-product-template-modal__list .woocommerce-product-template-modal__list-subtitle{color:#757575;margin-top:4px;display:block}.woocommerce-product-template-modal__actions{text-align:right}.woocommerce-task-payment{display:flex;flex-direction:row;align-items:center;position:relative;overflow:hidden}.woocommerce-task-payment .components-card__media{width:170px;flex-shrink:0}.woocommerce-task-payment .components-card__media .is-placeholder,.woocommerce-task-payment .components-card__media img,.woocommerce-task-payment .components-card__media svg{margin:auto;max-width:100px;display:block}.woocommerce-task-payment .components-card__media .is-placeholder{height:36px}.woocommerce-task-payment .woocommerce-task-payment__footer .is-placeholder{width:70px;height:36px}.woocommerce-task-payment>.components-form-toggle{min-width:52px}.woocommerce-task-payment .woocommerce-task-payment__title{display:flex;align-items:center;font-size:16px;font-weight:500;color:#2c3338;margin-top:0;margin-bottom:8px}.woocommerce-task-payment .woocommerce-task-payment__content{font-size:14px;color:#50575e;margin:0 36px 0 0}.woocommerce-task-payment .woocommerce-task-payment__content .text-style-strong{font-weight:700}.woocommerce-task-payment .woocommerce-task-payment__content p{font-size:12px}.woocommerce-task-payment .woocommerce-task-payment__description{flex:1}@media(max-width:600px){.woocommerce-task-payment{flex-wrap:wrap}.woocommerce-task-payment .woocommerce-task-payment__content{margin:0}.woocommerce-task-payment .components-card__media{order:1;flex-basis:50%}.woocommerce-task-payment .components-card__media>img,.woocommerce-task-payment .components-card__media>svg{margin:0 0 0 24px}.woocommerce-task-payment .woocommerce-task-payment__description{order:3;padding:24px 0 0 24px}.woocommerce-task-payment .woocommerce-task-payment__footer{flex-basis:50%;align-self:flex-start;order:2;text-align:right}}.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;max-width:240px;width:80%}.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-payment-gateway-suggestions-list-placeholder .is-placeholder{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent;display:inline-block;width:60%}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-label{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon{animation:loading-fade 1.6s ease-in-out infinite;background-color:#f0f0f0;color:transparent}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon:after{content:" "}@media screen and (prefers-reduced-motion:reduce){.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-icon{animation:none}}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step:first-child .woocommerce-stepper__step-label{width:30%}.woocommerce-task-payment-method.is-loading .woocommerce-stepper__step-number{display:none}:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-task-list__item{overflow:hidden}.woocommerce-task-list__item.woocommerce-list__item-enter{opacity:0;max-height:0}.woocommerce-task-list__item.woocommerce-list__item-enter-active{opacity:1;max-height:100px;transition:opacity .5s,max-height .5s}.woocommerce-task-list__item.woocommerce-list__item-exit{opacity:1;max-height:100px}.woocommerce-task-list__item.woocommerce-list__item-exit-active{opacity:0;max-height:0;transition:opacity .5s,max-height .5s} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/6.style.css b/packages/woocommerce-admin/dist/chunks/6.style.css new file mode 100644 index 0000000..68a621e --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/6.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-analytics__table-placeholder .woocommerce-card__body{padding:0}.woocommerce-analytics__table-placeholder .woocommerce-table__table{margin-bottom:0}.woocommerce-analytics__table-placeholder .woocommerce-table__table tr:last-child{border-bottom-style:none} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/7.style.css b/packages/woocommerce-admin/dist/chunks/7.style.css new file mode 100644 index 0000000..45294f2 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/7.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-table__product-categories>.woocommerce-table__breadcrumbs{display:inline-block;margin-right:12px}.woocommerce-table__product-categories .components-popover__content{padding:0 16px;text-align:left}.woocommerce-table__product-categories .components-popover__content .woocommerce-table__breadcrumbs{margin-top:12px;margin-bottom:12px} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/9.style.css b/packages/woocommerce-admin/dist/chunks/9.style.css new file mode 100644 index 0000000..f363d4f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/9.style.css @@ -0,0 +1 @@ +:root{--wp-admin-theme-color:#007cba;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-20:#005a87}body.admin-color-light{--wp-admin-theme-color:#0085ba;--wp-admin-theme-color-darker-10:#0073a1;--wp-admin-theme-color-darker-20:#006187}body.admin-color-modern{--wp-admin-theme-color:#3858e9;--wp-admin-theme-color-darker-10:#2145e6;--wp-admin-theme-color-darker-20:#183ad6}body.admin-color-blue{--wp-admin-theme-color:#096484;--wp-admin-theme-color-darker-10:#07526c;--wp-admin-theme-color-darker-20:#064054}body.admin-color-coffee{--wp-admin-theme-color:#46403c;--wp-admin-theme-color-darker-10:#383330;--wp-admin-theme-color-darker-20:#2b2724}body.admin-color-ectoplasm{--wp-admin-theme-color:#523f6d;--wp-admin-theme-color-darker-10:#46365d;--wp-admin-theme-color-darker-20:#3a2c4d}body.admin-color-midnight{--wp-admin-theme-color:#e14d43;--wp-admin-theme-color-darker-10:#dd382d;--wp-admin-theme-color-darker-20:#d02c21}body.admin-color-ocean{--wp-admin-theme-color:#627c83;--wp-admin-theme-color-darker-10:#576e74;--wp-admin-theme-color-darker-20:#4c6066}body.admin-color-sunrise{--wp-admin-theme-color:#dd823b;--wp-admin-theme-color-darker-10:#d97426;--wp-admin-theme-color-darker-20:#c36922}.woocommerce-report-table__scroll-point{position:relative;top:-48px}@media(max-width:782px){.woocommerce-report-table__scroll-point{top:-62px}}.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-108px}@media(max-width:782px){.woocommerce-feature-enabled-activity-panels .woocommerce-report-table__scroll-point{top:-122px}}.woocommerce-report-table .woocommerce-search{flex-grow:1}.woocommerce-report-table .components-card__header{display:grid;grid-gap:12px;grid-template-columns:min-content 1fr min-content}.woocommerce-report-table .woocommerce-table__compare.components-button{padding:8px}.woocommerce-report-table .woocommerce-ellipsis-menu{justify-self:flex-end}button.woocommerce-table__download-button{padding:6px 12px;color:#000;text-decoration:none;align-items:center}button.woocommerce-table__download-button svg{margin-right:8px;height:24px;width:24px}@media(max-width:782px){button.woocommerce-table__download-button svg{margin-right:0}button.woocommerce-table__download-button .woocommerce-table__download-button__label{display:none}} \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/activity-panels-help.js b/packages/woocommerce-admin/dist/chunks/activity-panels-help.js new file mode 100644 index 0000000..d85a5a9 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/activity-panels-help.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[4],{473:function(e,t,o){"use strict";var c=o(0),m=o(8),n=Object(c.createElement)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(m.Path,{d:"M10.6 6L9.4 7l4.6 5-4.6 5 1.2 1 5.4-6z"}));t.a=n},520:function(e,t,o){"use strict";var c=o(0),m=o(6),n=o.n(m),r=o(1),i=o.n(r),a=o(20),u=o(21);o(521);class s extends c.Component{render(){const{className:e,menu:t,subtitle:o,title:m,unreadMessages:r}=this.props,i=n()({"woocommerce-layout__inbox-panel-header":o,"woocommerce-layout__activity-panel-header":!o},e),u=r||0;return Object(c.createElement)("div",{className:i},Object(c.createElement)("div",{className:"woocommerce-layout__inbox-title"},Object(c.createElement)(a.Text,{size:16,weight:600,color:"#23282d"},m),Object(c.createElement)(a.Text,{variant:"button",weight:"600",size:"14",lineHeight:"20px"},u>0&&Object(c.createElement)("span",{className:"woocommerce-layout__inbox-badge"},r))),Object(c.createElement)("div",{className:"woocommerce-layout__inbox-subtitle"},o&&Object(c.createElement)(a.Text,{variant:"body.small",size:"14",lineHeight:"20px"},o)),t&&Object(c.createElement)("div",{className:"woocommerce-layout__activity-panel-header-menu"},t))}}s.propTypes={className:i.a.string,unreadMessages:i.a.number,title:i.a.string.isRequired,subtitle:i.a.string,menu:i.a.shape({type:i.a.oneOf([u.EllipsisMenu])})},t.a=s},521:function(e,t,o){},582:function(e,t,o){"use strict";(function(e,c){var m,n=o(584);m="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==e?e:c;var r=Object(n.a)(m);t.a=r}).call(this,o(78),o(583)(e))},583:function(e,t){e.exports=function(e){if(!e.webpackPolyfill){var t=Object.create(e);t.children||(t.children=[]),Object.defineProperty(t,"loaded",{enumerable:!0,get:function(){return t.l}}),Object.defineProperty(t,"id",{enumerable:!0,get:function(){return t.i}}),Object.defineProperty(t,"exports",{enumerable:!0}),t.webpackPolyfill=1}return t}},584:function(e,t,o){"use strict";function c(e){var t,o=e.Symbol;return"function"==typeof o?o.observable?t=o.observable:(t=o("observable"),o.observable=t):t="@@observable",t}o.d(t,"a",(function(){return c}))},619:function(e,t,o){"use strict";o.r(t),o.d(t,"SETUP_TASK_HELP_ITEMS_FILTER",(function(){return O})),o.d(t,"HelpPanel",(function(){return j}));var c=o(0),m=o(2),n=o(20),r=o(7),i=o(30),a=o(116),u=o(498),s=o(473),l=o(4),d=o(13),p=o(21),_=o(11),w=(o(582),function(){return Math.random().toString(36).substring(7).split("").join(".")});w(),w();function h(){for(var e=arguments.length,t=new Array(e),o=0;o{const{taskName:t}=e;Object(c.useEffect)(()=>{e.recordEvent("help_panel_open",{task_name:t||"homescreen"})},[t]);const o=function(e){const t=k(e),o={title:Object(m.__)("WooCommerce Docs",'woocommerce'),link:"https://docs.woocommerce.com/?utm_source=help_panel&utm_medium=product"};t.push(o);const r=Object(i.applyFilters)(O,t,e.taskName,e);let d=Array.isArray(r)?r.filter(e=>e instanceof Object&&e.title&&e.link):[];d.length||(d=[o]);const p=Object(l.partial)(y,e);return d.map(e=>({title:Object(c.createElement)(n.Text,{as:"div",variant:"button",weight:"600",size:"14",lineHeight:"20px"},e.title),before:Object(c.createElement)(a.a,{icon:u.a}),after:Object(c.createElement)(a.a,{icon:s.a}),linkType:"external",target:"_blank",href:e.link,onClick:p}))}(e);return Object(c.createElement)(c.Fragment,null,Object(c.createElement)(b.a,{title:Object(m.__)("Documentation",'woocommerce')}),Object(c.createElement)(p.Section,null,Object(c.createElement)(p.List,{items:o,className:"woocommerce-quick-links__list"})))};j.defaultProps={getSetting:d.f,recordEvent:g.recordEvent};t.default=h(Object(r.withSelect)(e=>{const{getSettings:t}=e(_.SETTINGS_STORE_NAME),{getActivePlugins:o}=e(_.PLUGINS_STORE_NAME),{general:c={}}=t("general"),m=o(),n=e(_.ONBOARDING_STORE_NAME).getPaymentGatewaySuggestions().reduce((e,t)=>{const{id:o}=t;return e[o]=!0,e},{});return{activePlugins:m,countryCode:Object(f.b)(c.woocommerce_default_country),paymentGatewaySuggestions:n}}))(j)}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/activity-panels-inbox.js b/packages/woocommerce-admin/dist/chunks/activity-panels-inbox.js new file mode 100644 index 0000000..cdd137f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/activity-panels-inbox.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[5],{512:function(e,t,c){"use strict";c.d(t,"a",(function(){return N})),c.d(t,"b",(function(){return O}));var a=c(0),n=c(6),o=c.n(n),s=c(61),i=c.n(s),r=c(9),l=c.n(r),m=c(1),d=c.n(m),u=c(21),b=c(3),_=(c(522),c(4));class p extends a.Component{render(){const{className:e,hasAction:t,hasDate:c,hasSubtitle:n,lines:s}=this.props,i=o()("woocommerce-activity-card is-loading",e);return Object(a.createElement)("div",{className:i,"aria-hidden":!0},Object(a.createElement)("span",{className:"woocommerce-activity-card__icon"},Object(a.createElement)("span",{className:"is-placeholder"})),Object(a.createElement)("div",{className:"woocommerce-activity-card__header"},Object(a.createElement)("div",{className:"woocommerce-activity-card__title is-placeholder"}),n&&Object(a.createElement)("div",{className:"woocommerce-activity-card__subtitle is-placeholder"}),c&&Object(a.createElement)("div",{className:"woocommerce-activity-card__date"},Object(a.createElement)("span",{className:"is-placeholder"}))),Object(a.createElement)("div",{className:"woocommerce-activity-card__body"},Object(_.range)(s).map(e=>Object(a.createElement)("span",{className:"is-placeholder",key:e}))),t&&Object(a.createElement)("div",{className:"woocommerce-activity-card__actions"},Object(a.createElement)("span",{className:"is-placeholder"})))}}p.propTypes={className:d.a.string,hasAction:d.a.bool,hasDate:d.a.bool,hasSubtitle:d.a.bool,lines:d.a.number},p.defaultProps={hasAction:!1,hasDate:!1,hasSubtitle:!1,lines:1};var O=p;class N extends a.Component{getCard(){const{actions:e,className:t,children:c,date:n,icon:s,subtitle:i,title:r,unread:m}=this.props,d=o()("woocommerce-activity-card",t),b=Array.isArray(e)?e:[e],_=/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(n)?l.a.utc(n).fromNow():n;return Object(a.createElement)("section",{className:d},m&&Object(a.createElement)("span",{className:"woocommerce-activity-card__unread"}),s&&Object(a.createElement)("span",{className:"woocommerce-activity-card__icon","aria-hidden":!0},s),r&&Object(a.createElement)("header",{className:"woocommerce-activity-card__header"},Object(a.createElement)(u.H,{className:"woocommerce-activity-card__title"},r),i&&Object(a.createElement)("div",{className:"woocommerce-activity-card__subtitle"},i),_&&Object(a.createElement)("span",{className:"woocommerce-activity-card__date"},_)),c&&Object(a.createElement)(u.Section,{className:"woocommerce-activity-card__body"},c),e&&Object(a.createElement)("footer",{className:"woocommerce-activity-card__actions"},b.map((e,t)=>Object(a.cloneElement)(e,{key:t}))))}render(){const{onClick:e}=this.props;return e?Object(a.createElement)(b.Button,{className:"woocommerce-activity-card__button",onClick:e},this.getCard()):this.getCard()}}N.propTypes={actions:d.a.oneOfType([d.a.arrayOf(d.a.element),d.a.element]),onClick:d.a.func,className:d.a.string,children:d.a.node,date:d.a.string,icon:d.a.node,subtitle:d.a.node,title:d.a.oneOfType([d.a.string,d.a.node]),unread:d.a.bool},N.defaultProps={icon:Object(a.createElement)(i.a,{size:48}),unread:!1}},517:function(e,t,c){"use strict";function a(e){return e?e.substr(1).split("&").reduce((e,t)=>{const c=t.split("="),a=c[0];let n=decodeURIComponent(c[1]);return n=isNaN(Number(n))?n:Number(n),e[a]=n,e},{}):{}}function n(){let e="";const{page:t,path:c,post_type:n}=a(window.location.search);if(t){const a="wc-admin"===t?"home_screen":t;e=c?c.replace(/\//g,"_").substring(1):a}else n&&(e=n);return e}c.d(t,"b",(function(){return a})),c.d(t,"a",(function(){return n}))},522:function(e,t,c){},523:function(e,t,c){"use strict";var a=c(0),n=c(2),o=c(21),s=c(11),i=c(7),r=c(16),l=c(172),m=c(169),d=c(20),u=c(512),b=c(164),_=c(517);c(524);const p=(e,t)=>{Object(r.recordEvent)("inbox_action_click",{note_name:e.name,note_title:e.title,note_content_inner_link:t})},O=({hasNotes:e,isBatchUpdating:t,lastRead:c,notes:o,onDismiss:s,onNoteActionClick:i})=>{if(t)return;if(!e)return Object(a.createElement)(u.a,{className:"woocommerce-empty-activity-card",title:Object(n.__)("Your inbox is empty",'woocommerce'),icon:!1},Object(n.__)("As things begin to happen in your store your inbox will start to fill up. You'll see things like achievements, new feature announcements, extension recommendations and more!",'woocommerce'));const b=Object(_.a)(),O=e=>{Object(r.recordEvent)("inbox_note_view",{note_content:e.content,note_name:e.name,note_title:e.title,note_type:e.type,screen:b})},N=Object.keys(o).map(e=>o[e]);return Object(a.createElement)(l.a,{role:"menu"},N.map(e=>{const{id:t,is_deleted:n}=e;return n?null:Object(a.createElement)(m.a,{key:t,timeout:500,classNames:"woocommerce-inbox-message"},Object(a.createElement)(d.InboxNoteCard,{key:t,note:e,lastRead:c,onDismiss:s,onNoteActionClick:i,onBodyLinkClick:p,onNoteVisible:O}))}))},N={page:1,per_page:s.QUERY_DEFAULTS.pageSize,status:"unactioned",type:s.QUERY_DEFAULTS.noteTypes,orderby:"date",order:"desc",_fields:["id","name","title","content","type","status","actions","date_created","date_created_gmt","layout","image","is_deleted"]};t.a=()=>{const{createNotice:e}=Object(i.useDispatch)("core/notices"),{batchUpdateNotes:t,removeAllNotes:c,removeNote:l,updateNote:m,triggerNoteAction:u}=Object(i.useDispatch)(s.NOTES_STORE_NAME),{isError:p,isResolvingNotes:h,isBatchUpdating:j,notes:v}=Object(i.useSelect)(e=>{const{getNotes:t,getNotesError:c,isResolving:a,isNotesRequesting:n}=e(s.NOTES_STORE_NAME);return{notes:t(N),isError:Boolean(c("getNotes",[N])),isResolvingNotes:a("getNotes",[N]),isBatchUpdating:n("batchUpdateNotes")}}),{updateUserPreferences:w,...y}=Object(s.useUserPreferences)(),[g]=Object(a.useState)(y.activity_panel_inbox_last_read),[E,f]=Object(a.useState)();Object(a.useEffect)(()=>{const e=Date.now();w({activity_panel_inbox_last_read:e})},[]);const C=async(a=!1)=>{const o="all"===E.type,s=Object(_.a)();if(Object(r.recordEvent)("inbox_action_dismiss",{note_name:E.note.name,note_title:E.note.title,note_name_dismiss_all:o,note_name_dismiss_confirmation:a,screen:s}),a){const a=E.note.id,s=!a||o;try{let o=[];if(s)o=await c({status:N.status});else{const e=await l(a);o=[e]}f(void 0),e("success",o.length>1?Object(n.__)("All messages dismissed",'woocommerce'):Object(n.__)("Message dismissed",'woocommerce'),{actions:[{label:Object(n.__)("Undo",'woocommerce'),onClick:()=>{o.length>1?t(o.map(e=>e.id),{is_deleted:0}):m(a,{is_deleted:0})}}]})}catch(t){const c=s?v.length:1;e("error",Object(n._n)("Message could not be dismissed","Messages could not be dismissed",c,'woocommerce')),f(void 0)}}else f(void 0)};if(p){const e=Object(n.__)("There was an error getting your inbox. Please try again.",'woocommerce'),t=Object(n.__)("Reload",'woocommerce'),c=()=>{window.location.reload()};return Object(a.createElement)(o.EmptyContent,{title:e,actionLabel:t,actionURL:null,actionCallback:c})}const k=Object(b.b)(v);return Object(a.createElement)(a.Fragment,null,Object(a.createElement)("div",{className:"woocommerce-homepage-notes-wrapper"},(h||j)&&Object(a.createElement)(o.Section,null,Object(a.createElement)(d.InboxNotePlaceholder,{className:"banner message-is-unread"})),Object(a.createElement)(o.Section,null,!h&&!j&&O({hasNotes:k,isBatchUpdating:j,lastRead:g,notes:v,onDismiss:(e,t)=>{f({note:e,type:t})},onNoteActionClick:(e,t)=>{u(e.id,t.id)}})),E&&Object(a.createElement)(d.InboxDismissConfirmationModal,{onClose:C,onDismiss:()=>C(!0)})))}},524:function(e,t,c){},585:function(e,t,c){},608:function(e,t,c){"use strict";c.r(t),c.d(t,"InboxPanel",(function(){return s}));var a=c(0),n=(c(585),c(523)),o=c(254);const s=({hasAbbreviatedNotifications:e,thingsToDoNextCount:t})=>Object(a.createElement)("div",{className:"woocommerce-notification-panels"},e&&Object(a.createElement)(o.b,{thingsToDoNextCount:t}),Object(a.createElement)(n.a,null));t.default=s},61:function(e,t,c){"use strict";var a=Object.assign||function(e){for(var t,c=1;c!0,filters:g}]);var p=r(14),y=r(4),O=r(12),j=r(21),h=r(120),f=r(11),w=r(518),v=r(506),C=r(501);class S extends o.Component{constructor(e){super(e),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(s.__)("Category",'woocommerce'),key:"category",required:!0,isSortable:!0,isLeftAligned:!0},{label:Object(s.__)("Items sold",'woocommerce'),key:"items_sold",required:!0,defaultSort:!0,isSortable:!0,isNumeric:!0},{label:Object(s.__)("Net sales",'woocommerce'),key:"net_revenue",isSortable:!0,isNumeric:!0},{label:Object(s.__)("Products",'woocommerce'),key:"products_count",isSortable:!0,isNumeric:!0},{label:Object(s.__)("Orders",'woocommerce'),key:"orders_count",isSortable:!0,isNumeric:!0}]}getRowsContent(e){const{render:t,formatDecimal:r,getCurrencyConfig:c}=this.context,{categories:a,query:s}=this.props;if(!a)return[];const n=c();return Object(y.map)(e,e=>{const{category_id:c,items_sold:i,net_revenue:m,products_count:l,orders_count:d}=e,u=a.get(c),b=Object(O.getPersistedQuery)(s);return[{display:Object(o.createElement)(w.a,{query:s,category:u,categories:a}),value:u&&u.name},{display:Object(h.formatValue)(n,"number",i),value:i},{display:t(m),value:r(m)},{display:u&&Object(o.createElement)(j.Link,{href:Object(O.getNewPath)(b,"/analytics/categories",{filter:"single_category",categories:u.id}),type:"wc-admin"},Object(h.formatValue)(n,"number",l)),value:l},{display:Object(h.formatValue)(n,"number",d),value:d}]})}getSummary(e,t=0){const{items_sold:r=0,net_revenue:o=0,orders_count:c=0}=e,{formatAmount:a,getCurrencyConfig:n}=this.context,i=n();return[{label:Object(s._n)("Category","Categories",t,'woocommerce'),value:Object(h.formatValue)(i,"number",t)},{label:Object(s._n)("Item sold","Items sold",r,'woocommerce'),value:Object(h.formatValue)(i,"number",r)},{label:Object(s.__)("Net sales",'woocommerce'),value:a(o)},{label:Object(s._n)("Order","Orders",c,'woocommerce'),value:Object(h.formatValue)(i,"number",c)}]}render(){const{advancedFilters:e,filters:t,isRequesting:r,query:c}=this.props,a={helpText:Object(s.__)("Check at least two categories below to compare",'woocommerce'),placeholder:Object(s.__)("Search by category name",'woocommerce')};return Object(o.createElement)(v.a,{compareBy:"categories",endpoint:"categories",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["items_sold","net_revenue","orders_count"],isRequesting:r,itemIdField:"category_id",query:c,searchBy:"categories",labels:a,tableQuery:{orderby:c.orderby||"items_sold",order:c.order||"desc",extended_info:!0},title:Object(s.__)("Categories",'woocommerce'),columnPrefsKey:"categories_report_columns",filters:t,advancedFilters:e})}}S.contextType=C.a;var k=Object(p.compose)(Object(i.withSelect)((e,t)=>{const{isRequesting:r,query:o}=t;if(r||o.search&&(!o.categories||!o.categories.length))return{};const{getItems:c,getItemsError:a,isResolving:s}=e(f.ITEMS_STORE_NAME),n={per_page:-1};return{categories:c("categories",n),isError:Boolean(a("categories",n)),isRequesting:s("getItems",["categories",n])}}))(S),E=r(510),q=r(508),A=r(511),N=r(530),R=r(505);class P extends o.Component{getChartMeta(){const{query:e}=this.props,t="compare-categories"===e.filter&&e.categories&&e.categories.split(",").length>1,r="single_category"===e.filter&&!!e.categories,o=t||r?"item-comparison":"time-comparison";return{isSingleCategoryView:r,itemsLabel:r?Object(s.__)("%d products",'woocommerce'):Object(s.__)("%d categories",'woocommerce'),mode:o}}render(){const{isRequesting:e,query:t,path:r}=this.props,{mode:c,itemsLabel:a,isSingleCategoryView:s}=this.getChartMeta(),n={...t};return"item-comparison"===c&&(n.segmentby=s?"product":"category"),Object(o.createElement)(o.Fragment,null,Object(o.createElement)(R.a,{query:t,path:r,filters:_,advancedFilters:b,report:"categories"}),Object(o.createElement)(A.a,{charts:u,endpoint:"products",isRequesting:e,limitProperties:s?["products","categories"]:["categories"],query:n,selectedChart:Object(E.a)(t.chart,u),filters:_,advancedFilters:b,report:"categories"}),Object(o.createElement)(q.a,{charts:u,filters:_,advancedFilters:b,mode:c,endpoint:"products",limitProperties:s?["products","categories"]:["categories"],path:r,query:n,isRequesting:e,itemsLabel:a,selectedChart:Object(E.a)(t.chart,u)}),s?Object(o.createElement)(N.a,{isRequesting:e,query:n,baseSearchQuery:{filter:"single_category"},hideCompare:s,filters:_,advancedFilters:b}):Object(o.createElement)(k,{isRequesting:e,query:t,filters:_,advancedFilters:b}))}}P.propTypes={query:a.a.object.isRequired,path:a.a.string.isRequired};t.default=P},502:function(e,t,r){"use strict";r.d(t,"e",(function(){return d})),r.d(t,"a",(function(){return u})),r.d(t,"b",(function(){return b})),r.d(t,"c",(function(){return g})),r.d(t,"d",(function(){return _})),r.d(t,"f",(function(){return p})),r.d(t,"h",(function(){return y})),r.d(t,"g",(function(){return O}));var o=r(15),c=r(17),a=r.n(c),s=r(4),n=r(12),i=r(11),m=r(13),l=r(503);function d(e,t=s.identity){return function(r="",c){const s="function"==typeof e?e(c):e,i=Object(n.getIdsFromQuery)(r);if(i.length<1)return Promise.resolve([]);const m={include:i.join(","),per_page:i.length};return a()({path:Object(o.addQueryArgs)(s,m)}).then(e=>e.map(t))}}d(i.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const u=d(i.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),b=d(i.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),g=d(i.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),_=d(i.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),p=d(i.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(l.a)(e)}));function y({attributes:e,name:t}){const r=Object(m.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(r)>-1)return t;const o=(e||[]).map(({option:e})=>e).join(", ");return o?t+r+o:t}const O=d(({products:e})=>e?i.NAMESPACE+`/products/${e}/variations`:i.NAMESPACE+"/variations",e=>({key:e.id,label:y(e)}))},503:function(e,t,r){"use strict";r.d(t,"a",(function(){return c}));var o=r(2);function c(e){return[e.country,e.state,e.name||Object(o.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},513:function(e,t,r){"use strict";function o(e,t,r){return!!t&&(e&&t<=r==="instock")}r.d(t,"a",(function(){return o}))},518:function(e,t,r){"use strict";r.d(t,"a",(function(){return i}));var o=r(0),c=r(4),a=r(3),s=r(21),n=r(12);class i extends o.Component{getCategoryAncestorIds(e,t){const r=[];let o=e.parent;for(;o;)r.unshift(o),o=t.get(o).parent;return r}getCategoryAncestors(e,t){const r=this.getCategoryAncestorIds(e,t);if(r.length)return 1===r.length?t.get(Object(c.first)(r)).name+" › ":2===r.length?t.get(Object(c.first)(r)).name+" › "+t.get(Object(c.last)(r)).name+" › ":t.get(Object(c.first)(r)).name+" … "+t.get(Object(c.last)(r)).name+" › "}render(){const{categories:e,category:t,query:r}=this.props,c=Object(n.getPersistedQuery)(r);return t?Object(o.createElement)("div",{className:"woocommerce-table__breadcrumbs"},this.getCategoryAncestors(t,e),Object(o.createElement)(s.Link,{href:Object(n.getNewPath)(c,"/analytics/categories",{filter:"single_category",categories:t.id}),type:"wc-admin"},t.name)):Object(o.createElement)(a.Spinner,null)}}},530:function(e,t,r){"use strict";var o=r(0),c=r(2),a=r(14),s=r(28),n=r(7),i=r(4),m=r(12),l=r(21),d=r(120),u=r(13),b=r(11),g=r(518),_=r(513),p=r(506),y=r(501);r(531);const O=Object(u.f)("manageStock","no"),j=Object(u.f)("stockStatuses",{});class h extends o.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(c.__)("Product title",'woocommerce'),key:"product_name",required:!0,isLeftAligned:!0,isSortable:!0},{label:Object(c.__)("SKU",'woocommerce'),key:"sku",hiddenByDefault:!0,isSortable:!0},{label:Object(c.__)("Items sold",'woocommerce'),key:"items_sold",required:!0,defaultSort:!0,isSortable:!0,isNumeric:!0},{label:Object(c.__)("Net sales",'woocommerce'),screenReaderLabel:Object(c.__)("Net sales",'woocommerce'),key:"net_revenue",required:!0,isSortable:!0,isNumeric:!0},{label:Object(c.__)("Orders",'woocommerce'),key:"orders_count",isSortable:!0,isNumeric:!0},{label:Object(c.__)("Category",'woocommerce'),key:"product_cat"},{label:Object(c.__)("Variations",'woocommerce'),key:"variations",isSortable:!0},"yes"===O?{label:Object(c.__)("Status",'woocommerce'),key:"stock_status"}:null,"yes"===O?{label:Object(c.__)("Stock",'woocommerce'),key:"stock",isNumeric:!0}:null].filter(Boolean)}getRowsContent(e=[]){const{query:t}=this.props,r=Object(m.getPersistedQuery)(t),{render:a,formatDecimal:n,getCurrencyConfig:b}=this.context,p=b();return Object(i.map)(e,e=>{const{product_id:i,items_sold:b,net_revenue:y,orders_count:h}=e,f=e.extended_info||{},{category_ids:w,low_stock_amount:v,manage_stock:C,sku:S,stock_status:k,stock_quantity:E,variations:q=[]}=f,A=Object(s.decodeEntities)(f.name),N=Object(m.getNewPath)(r,"/analytics/orders",{filter:"advanced",product_includes:i}),R=Object(m.getNewPath)(r,"/analytics/products",{filter:"single_product",products:i}),{categories:P}=this.props,x=w&&P&&w.map(e=>P.get(e)).filter(Boolean)||[],F=Object(_.a)(k,E,v)?Object(o.createElement)(l.Link,{href:Object(u.e)("post.php?action=edit&post="+i),type:"wp-admin"},Object(c._x)("Low","Indication of a low quantity",'woocommerce')):j[k];return[{display:Object(o.createElement)(l.Link,{href:R,type:"wc-admin"},A),value:A},{display:S,value:S},{display:Object(d.formatValue)(p,"number",b),value:b},{display:a(y),value:n(y)},{display:Object(o.createElement)(l.Link,{href:N,type:"wc-admin"},h),value:h},{display:Object(o.createElement)("div",{className:"woocommerce-table__product-categories"},x[0]&&Object(o.createElement)(g.a,{category:x[0],categories:P}),x.length>1&&Object(o.createElement)(l.Tag,{label:Object(c.sprintf)(Object(c._x)("+%d more","categories",'woocommerce'),x.length-1),popoverContents:x.map(e=>Object(o.createElement)(g.a,{category:e,categories:P,key:e.id,query:t}))})),value:x.map(e=>e.name).join(", ")},{display:Object(d.formatValue)(p,"number",q.length),value:q.length},"yes"===O?{display:C?F:Object(c.__)("N/A",'woocommerce'),value:C?j[k]:null}:null,"yes"===O?{display:C?Object(d.formatValue)(p,"number",E):Object(c.__)("N/A",'woocommerce'),value:E}:null].filter(Boolean)})}getSummary(e){const{products_count:t=0,items_sold:r=0,net_revenue:o=0,orders_count:a=0}=e,{formatAmount:s,getCurrencyConfig:n}=this.context,i=n();return[{label:Object(c._n)("Product","Products",t,'woocommerce'),value:Object(d.formatValue)(i,"number",t)},{label:Object(c._n)("Item sold","Items sold",r,'woocommerce'),value:Object(d.formatValue)(i,"number",r)},{label:Object(c.__)("Net sales",'woocommerce'),value:s(o)},{label:Object(c._n)("Orders","Orders",a,'woocommerce'),value:Object(d.formatValue)(i,"number",a)}]}render(){const{advancedFilters:e,baseSearchQuery:t,filters:r,hideCompare:a,isRequesting:s,query:n}=this.props,i={helpText:Object(c.__)("Check at least two products below to compare",'woocommerce'),placeholder:Object(c.__)("Search by product name or SKU",'woocommerce')};return Object(o.createElement)(p.a,{compareBy:a?void 0:"products",endpoint:"products",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["products_count","items_sold","net_revenue","orders_count"],itemIdField:"product_id",isRequesting:s,labels:i,query:n,searchBy:"products",baseSearchQuery:t,tableQuery:{orderby:n.orderby||"items_sold",order:n.order||"desc",extended_info:!0,segmentby:n.segmentby},title:Object(c.__)("Products",'woocommerce'),columnPrefsKey:"products_report_columns",filters:r,advancedFilters:e})}}h.contextType=y.a,t.a=Object(a.compose)(Object(n.withSelect)((e,t)=>{const{query:r,isRequesting:o}=t,{getItems:c,getItemsError:a,isResolving:s}=e(b.ITEMS_STORE_NAME);if(o||r.search&&(!r.products||!r.products.length))return{};const n={per_page:-1};return{categories:c("categories",n),isError:Boolean(a("categories",n)),isRequesting:s("getItems",["categories",n])}}))(h)},531:function(e,t,r){}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-coupons.js b/packages/woocommerce-admin/dist/chunks/analytics-report-coupons.js new file mode 100644 index 0000000..b5b0f0d --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-coupons.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[8],{484:function(e,t,o){"use strict";o.r(t);var n=o(0),r=o(1),c=o.n(r),a=o(2),s=o(534),i=o(4),u=o(21),m=o(12),d=o(120),l=o(13),p=o(19),b=o(506),_=o(501);class y extends n.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(a.__)("Coupon code",'woocommerce'),key:"code",required:!0,isLeftAligned:!0,isSortable:!0},{label:Object(a.__)("Orders",'woocommerce'),key:"orders_count",required:!0,defaultSort:!0,isSortable:!0,isNumeric:!0},{label:Object(a.__)("Amount discounted",'woocommerce'),key:"amount",isSortable:!0,isNumeric:!0},{label:Object(a.__)("Created",'woocommerce'),key:"created"},{label:Object(a.__)("Expires",'woocommerce'),key:"expires"},{label:Object(a.__)("Type",'woocommerce'),key:"type"}]}getRowsContent(e){const{query:t}=this.props,o=Object(m.getPersistedQuery)(t),r=Object(l.f)("dateFormat",p.defaultTableDateFormat),{formatAmount:c,formatDecimal:s,getCurrencyConfig:b}=this.context;return Object(i.map)(e,e=>{const{amount:t,coupon_id:i,orders_count:l}=e,p=e.extended_info||{},{code:_,date_created:y,date_expires:f,discount_type:j}=p,O=i>0?Object(m.getNewPath)(o,"/analytics/coupons",{filter:"single_coupon",coupons:i}):null,h=null===O?_:Object(n.createElement)(u.Link,{href:O,type:"wc-admin"},_),g=i>0?Object(m.getNewPath)(o,"/analytics/orders",{filter:"advanced",coupon_includes:i}):null;return[{display:h,value:_},{display:null===g?l:Object(n.createElement)(u.Link,{href:g,type:"wc-admin"},Object(d.formatValue)(b(),"number",l)),value:l},{display:c(t),value:s(t)},{display:y?Object(n.createElement)(u.Date,{date:y,visibleFormat:r}):Object(a.__)("N/A",'woocommerce'),value:y},{display:f?Object(n.createElement)(u.Date,{date:f,visibleFormat:r}):Object(a.__)("N/A",'woocommerce'),value:f},{display:this.getCouponType(j),value:j}]})}getSummary(e){const{coupons_count:t=0,orders_count:o=0,amount:n=0}=e,{formatAmount:r,getCurrencyConfig:c}=this.context,s=c();return[{label:Object(a._n)("Coupon","Coupons",t,'woocommerce'),value:Object(d.formatValue)(s,"number",t)},{label:Object(a._n)("Order","Orders",o,'woocommerce'),value:Object(d.formatValue)(s,"number",o)},{label:Object(a.__)("Amount discounted",'woocommerce'),value:r(n)}]}getCouponType(e){return{percent:Object(a.__)("Percentage",'woocommerce'),fixed_cart:Object(a.__)("Fixed cart",'woocommerce'),fixed_product:Object(a.__)("Fixed product",'woocommerce')}[e]||Object(a.__)("N/A",'woocommerce')}render(){const{advancedFilters:e,filters:t,isRequesting:o,query:r}=this.props;return Object(n.createElement)(b.a,{compareBy:"coupons",endpoint:"coupons",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["coupons_count","orders_count","amount"],isRequesting:o,itemIdField:"coupon_id",query:r,searchBy:"coupons",tableQuery:{orderby:r.orderby||"orders_count",order:r.order||"desc",extended_info:!0},title:Object(a.__)("Coupons",'woocommerce'),columnPrefsKey:"coupons_report_columns",filters:t,advancedFilters:e})}}y.contextType=_.a;var f=y,j=o(510),O=o(508),h=o(511),g=o(505);class w extends n.Component{getChartMeta(){const{query:e}=this.props,t="compare-coupons"===e.filter&&e.coupons&&e.coupons.split(",").length>1?"item-comparison":"time-comparison";return{itemsLabel:Object(a.__)("%d coupons",'woocommerce'),mode:t}}render(){const{isRequesting:e,query:t,path:o}=this.props,{mode:r,itemsLabel:c}=this.getChartMeta(),a={...t};return"item-comparison"===r&&(a.segmentby="coupon"),Object(n.createElement)(n.Fragment,null,Object(n.createElement)(g.a,{query:t,path:o,filters:s.c,advancedFilters:s.a,report:"coupons"}),Object(n.createElement)(h.a,{charts:s.b,endpoint:"coupons",isRequesting:e,query:a,selectedChart:Object(j.a)(t.chart,s.b),filters:s.c,advancedFilters:s.a}),Object(n.createElement)(O.a,{charts:s.b,filters:s.c,advancedFilters:s.a,mode:r,endpoint:"coupons",path:o,query:a,isRequesting:e,itemsLabel:c,selectedChart:Object(j.a)(t.chart,s.b)}),Object(n.createElement)(f,{isRequesting:e,query:t,filters:s.c,advancedFilters:s.a}))}}w.propTypes={query:c.a.object.isRequired};t.default=w},502:function(e,t,o){"use strict";o.d(t,"e",(function(){return d})),o.d(t,"a",(function(){return l})),o.d(t,"b",(function(){return p})),o.d(t,"c",(function(){return b})),o.d(t,"d",(function(){return _})),o.d(t,"f",(function(){return y})),o.d(t,"h",(function(){return f})),o.d(t,"g",(function(){return j}));var n=o(15),r=o(17),c=o.n(r),a=o(4),s=o(12),i=o(11),u=o(13),m=o(503);function d(e,t=a.identity){return function(o="",r){const a="function"==typeof e?e(r):e,i=Object(s.getIdsFromQuery)(o);if(i.length<1)return Promise.resolve([]);const u={include:i.join(","),per_page:i.length};return c()({path:Object(n.addQueryArgs)(a,u)}).then(e=>e.map(t))}}d(i.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const l=d(i.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),p=d(i.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),b=d(i.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),_=d(i.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),y=d(i.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(m.a)(e)}));function f({attributes:e,name:t}){const o=Object(u.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(o)>-1)return t;const n=(e||[]).map(({option:e})=>e).join(", ");return n?t+o+n:t}const j=d(({products:e})=>e?i.NAMESPACE+`/products/${e}/variations`:i.NAMESPACE+"/variations",e=>({key:e.id,label:f(e)}))},503:function(e,t,o){"use strict";o.d(t,"a",(function(){return r}));var n=o(2);function r(e){return[e.country,e.state,e.name||Object(n.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},534:function(e,t,o){"use strict";o.d(t,"b",(function(){return u})),o.d(t,"a",(function(){return m})),o.d(t,"c",(function(){return l}));var n=o(2),r=o(30),c=o(7),a=o(502),s=o(55);const{addCesSurveyForAnalytics:i}=Object(c.dispatch)(s.c),u=Object(r.applyFilters)("woocommerce_admin_coupons_report_charts",[{key:"orders_count",label:Object(n.__)("Discounted orders",'woocommerce'),order:"desc",orderby:"orders_count",type:"number"},{key:"amount",label:Object(n.__)("Amount",'woocommerce'),order:"desc",orderby:"amount",type:"currency"}]),m=Object(r.applyFilters)("woocommerce_admin_coupon_report_advanced_filters",{filters:{},title:Object(n._x)("Coupons match {{select /}} filters","A sentence describing filters for Coupons. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')}),d=[{label:Object(n.__)("All coupons",'woocommerce'),value:"all"},{label:Object(n.__)("Single coupon",'woocommerce'),value:"select_coupon",chartMode:"item-comparison",subFilters:[{component:"Search",value:"single_coupon",chartMode:"item-comparison",path:["select_coupon"],settings:{type:"coupons",param:"coupons",getLabels:a.b,labels:{placeholder:Object(n.__)("Type to search for a coupon",'woocommerce'),button:Object(n.__)("Single Coupon",'woocommerce')}}}]},{label:Object(n.__)("Comparison",'woocommerce'),value:"compare-coupons",settings:{type:"coupons",param:"coupons",getLabels:a.b,labels:{title:Object(n.__)("Compare Coupon Codes",'woocommerce'),update:Object(n.__)("Compare",'woocommerce'),helpText:Object(n.__)("Check at least two coupon codes below to compare",'woocommerce')},onClick:i}}];Object.keys(m.filters).length&&d.push({label:Object(n.__)("Advanced filters",'woocommerce'),value:"advanced"});const l=Object(r.applyFilters)("woocommerce_admin_coupons_report_filters",[{label:Object(n.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:d}])}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-customers.js b/packages/woocommerce-admin/dist/chunks/analytics-report-customers.js new file mode 100644 index 0000000..3ddb5ff --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-customers.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[9],{480:function(e,t,r){"use strict";r.r(t),r.d(t,"default",(function(){return C}));var a=r(0),o=r(1),c=r.n(o),n=r(2),l=r(28),i=r(30),s=r(13),m=r(11),d=r(502);const{countries:u}=Object(s.f)("dataEndpoints",{countries:{}}),b=Object(i.applyFilters)("woocommerce_admin_customers_report_filters",[{label:Object(n.__)("Show",'woocommerce'),staticParams:["paged","per_page"],param:"filter",showFilters:()=>!0,filters:[{label:Object(n.__)("All Customers",'woocommerce'),value:"all"},{label:Object(n.__)("Single Customer",'woocommerce'),value:"select_customer",chartMode:"item-comparison",subFilters:[{component:"Search",value:"single_customer",chartMode:"item-comparison",path:["select_customer"],settings:{type:"customers",param:"customers",getLabels:d.c,labels:{placeholder:Object(n.__)("Type to search for a customer",'woocommerce'),button:Object(n.__)("Single Customer",'woocommerce')}}}]},{label:Object(n.__)("Advanced filters",'woocommerce'),value:"advanced"}]}]),_=Object(i.applyFilters)("woocommerce_admin_customers_report_advanced_filters",{title:Object(n._x)("Customers match {{select /}} filters","A sentence describing filters for Customers. See screen shot for context: https://cloudup.com/cCsm3GeXJbE",'woocommerce'),filters:{name:{labels:{add:Object(n.__)("Name",'woocommerce'),placeholder:Object(n.__)("Search",'woocommerce'),remove:Object(n.__)("Remove customer name filter",'woocommerce'),rule:Object(n.__)("Select a customer name filter match",'woocommerce'),title:Object(n.__)("{{title}}Name{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(n.__)("Select customer name",'woocommerce')},rules:[{value:"includes",label:Object(n._x)("Includes","customer names",'woocommerce')},{value:"excludes",label:Object(n._x)("Excludes","customer names",'woocommerce')}],input:{component:"Search",type:"customers",getLabels:Object(d.e)(m.NAMESPACE+"/customers",e=>({id:e.id,label:e.name}))}},country:{labels:{add:Object(n.__)("Country / Region",'woocommerce'),placeholder:Object(n.__)("Search",'woocommerce'),remove:Object(n.__)("Remove country / region filter",'woocommerce'),rule:Object(n.__)("Select a country / region filter match",'woocommerce'),title:Object(n.__)("{{title}}Country / Region{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(n.__)("Select country / region",'woocommerce')},rules:[{value:"includes",label:Object(n._x)("Includes","countries",'woocommerce')},{value:"excludes",label:Object(n._x)("Excludes","countries",'woocommerce')}],input:{component:"Search",type:"countries",getLabels:async e=>{const t=u.map(e=>({key:e.code,label:Object(l.decodeEntities)(e.name)})),r=e.split(",");return await t.filter(e=>r.includes(e.key))}}},username:{labels:{add:Object(n.__)("Username",'woocommerce'),placeholder:Object(n.__)("Search customer username",'woocommerce'),remove:Object(n.__)("Remove customer username filter",'woocommerce'),rule:Object(n.__)("Select a customer username filter match",'woocommerce'),title:Object(n.__)("{{title}}Username{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(n.__)("Select customer username",'woocommerce')},rules:[{value:"includes",label:Object(n._x)("Includes","customer usernames",'woocommerce')},{value:"excludes",label:Object(n._x)("Excludes","customer usernames",'woocommerce')}],input:{component:"Search",type:"usernames",getLabels:d.c}},email:{labels:{add:Object(n.__)("Email",'woocommerce'),placeholder:Object(n.__)("Search customer email",'woocommerce'),remove:Object(n.__)("Remove customer email filter",'woocommerce'),rule:Object(n.__)("Select a customer email filter match",'woocommerce'),title:Object(n.__)("{{title}}Email{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(n.__)("Select customer email",'woocommerce')},rules:[{value:"includes",label:Object(n._x)("Includes","customer emails",'woocommerce')},{value:"excludes",label:Object(n._x)("Excludes","customer emails",'woocommerce')}],input:{component:"Search",type:"emails",getLabels:Object(d.e)(m.NAMESPACE+"/customers",e=>({id:e.id,label:e.email}))}},orders_count:{labels:{add:Object(n.__)("No. of Orders",'woocommerce'),remove:Object(n.__)("Remove order filter",'woocommerce'),rule:Object(n.__)("Select an order count filter match",'woocommerce'),title:Object(n.__)("{{title}}No. of Orders{{/title}} {{rule /}} {{filter /}}",'woocommerce')},rules:[{value:"max",label:Object(n._x)("Less Than","number of orders",'woocommerce')},{value:"min",label:Object(n._x)("More Than","number of orders",'woocommerce')},{value:"between",label:Object(n._x)("Between","number of orders",'woocommerce')}],input:{component:"Number"}},total_spend:{labels:{add:Object(n.__)("Total Spend",'woocommerce'),remove:Object(n.__)("Remove total spend filter",'woocommerce'),rule:Object(n.__)("Select a total spend filter match",'woocommerce'),title:Object(n.__)("{{title}}Total Spend{{/title}} {{rule /}} {{filter /}}",'woocommerce')},rules:[{value:"max",label:Object(n._x)("Less Than","total spend by customer",'woocommerce')},{value:"min",label:Object(n._x)("More Than","total spend by customer",'woocommerce')},{value:"between",label:Object(n._x)("Between","total spend by customer",'woocommerce')}],input:{component:"Currency"}},avg_order_value:{labels:{add:Object(n.__)("AOV",'woocommerce'),remove:Object(n.__)("Remove average order value filter",'woocommerce'),rule:Object(n.__)("Select an average order value filter match",'woocommerce'),title:Object(n.__)("{{title}}AOV{{/title}} {{rule /}} {{filter /}}",'woocommerce')},rules:[{value:"max",label:Object(n._x)("Less Than","average order value of customer",'woocommerce')},{value:"min",label:Object(n._x)("More Than","average order value of customer",'woocommerce')},{value:"between",label:Object(n._x)("Between","average order value of customer",'woocommerce')}],input:{component:"Currency"}},registered:{labels:{add:Object(n.__)("Registered",'woocommerce'),remove:Object(n.__)("Remove registered filter",'woocommerce'),rule:Object(n.__)("Select a registered filter match",'woocommerce'),title:Object(n.__)("{{title}}Registered{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(n.__)("Select registered date",'woocommerce')},rules:[{value:"before",label:Object(n._x)("Before","date",'woocommerce')},{value:"after",label:Object(n._x)("After","date",'woocommerce')},{value:"between",label:Object(n._x)("Between","date",'woocommerce')}],input:{component:"Date"}},last_active:{labels:{add:Object(n.__)("Last active",'woocommerce'),remove:Object(n.__)("Remove last active filter",'woocommerce'),rule:Object(n.__)("Select a last active filter match",'woocommerce'),title:Object(n.__)("{{title}}Last active{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(n.__)("Select registered date",'woocommerce')},rules:[{value:"before",label:Object(n._x)("Before","date",'woocommerce')},{value:"after",label:Object(n._x)("After","date",'woocommerce')},{value:"between",label:Object(n._x)("Between","date",'woocommerce')}],input:{component:"Date"}}}});var p=r(3),O=r(21),j=r(120),y=r(19),g=r(506),f=r(501);const{countries:w}=Object(s.f)("dataEndpoints",{countries:{}});class h extends a.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(n.__)("Name",'woocommerce'),key:"name",required:!0,isLeftAligned:!0,isSortable:!0},{label:Object(n.__)("Username",'woocommerce'),key:"username",hiddenByDefault:!0},{label:Object(n.__)("Last active",'woocommerce'),key:"date_last_active",defaultSort:!0,isSortable:!0},{label:Object(n.__)("Date registered",'woocommerce'),key:"date_registered",isSortable:!0},{label:Object(n.__)("Email",'woocommerce'),key:"email"},{label:Object(n.__)("Orders",'woocommerce'),key:"orders_count",isSortable:!0,isNumeric:!0},{label:Object(n.__)("Total spend",'woocommerce'),key:"total_spend",isSortable:!0,isNumeric:!0},{label:Object(n.__)("AOV",'woocommerce'),screenReaderLabel:Object(n.__)("Average order value",'woocommerce'),key:"avg_order_value",isNumeric:!0},{label:Object(n.__)("Country / Region",'woocommerce'),key:"country",isSortable:!0},{label:Object(n.__)("City",'woocommerce'),key:"city",hiddenByDefault:!0,isSortable:!0},{label:Object(n.__)("Region",'woocommerce'),key:"state",hiddenByDefault:!0,isSortable:!0},{label:Object(n.__)("Postal code",'woocommerce'),key:"postcode",hiddenByDefault:!0,isSortable:!0}]}getCountryName(e){return void 0!==w[e]?w[e]:null}getRowsContent(e){const t=Object(s.f)("dateFormat",y.defaultTableDateFormat),{formatAmount:r,formatDecimal:o,getCurrencyConfig:c}=this.context;return null==e?void 0:e.map(e=>{const{avg_order_value:n,date_last_active:l,date_registered:i,email:m,name:d,user_id:u,orders_count:b,username:_,total_spend:y,postcode:g,city:f,state:w,country:h}=e,v=this.getCountryName(h),S=u?Object(a.createElement)(O.Link,{href:Object(s.e)("user-edit.php?user_id="+u),type:"wp-admin"},d):d,C=l?Object(a.createElement)(O.Date,{date:l,visibleFormat:t}):"—",E=i?Object(a.createElement)(O.Date,{date:i,visibleFormat:t}):"—",x=Object(a.createElement)(a.Fragment,null,Object(a.createElement)(p.Tooltip,{text:v},Object(a.createElement)("span",{"aria-hidden":"true"},h)),Object(a.createElement)("span",{className:"screen-reader-text"},v));return[{display:S,value:d},{display:_,value:_},{display:C,value:l},{display:E,value:i},{display:Object(a.createElement)("a",{href:"mailto:"+m},m),value:m},{display:Object(j.formatValue)(c(),"number",b),value:b},{display:r(y),value:o(y)},{display:r(n),value:o(n)},{display:x,value:h},{display:f,value:f},{display:w,value:w},{display:g,value:g}]})}getSummary(e){const{customers_count:t=0,avg_orders_count:r=0,avg_total_spend:a=0,avg_avg_order_value:o=0}=e,{formatAmount:c,getCurrencyConfig:l}=this.context,i=l();return[{label:Object(n._n)("customer","customers",t,'woocommerce'),value:Object(j.formatValue)(i,"number",t)},{label:Object(n._n)("Average order","Average orders",r,'woocommerce'),value:Object(j.formatValue)(i,"number",r)},{label:Object(n.__)("Average lifetime spend",'woocommerce'),value:c(a)},{label:Object(n.__)("Average order value",'woocommerce'),value:c(o)}]}render(){const{isRequesting:e,query:t,filters:r,advancedFilters:o}=this.props;return Object(a.createElement)(g.a,{endpoint:"customers",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["customers_count","avg_orders_count","avg_total_spend","avg_avg_order_value"],isRequesting:e,itemIdField:"id",query:t,labels:{placeholder:Object(n.__)("Search by customer name",'woocommerce')},searchBy:"customers",title:Object(n.__)("Customers",'woocommerce'),columnPrefsKey:"customers_report_columns",filters:r,advancedFilters:o})}}h.contextType=f.a;var v=h,S=r(505);class C extends a.Component{render(){const{isRequesting:e,query:t,path:r}=this.props,o={orderby:"date_last_active",order:"desc",...t};return Object(a.createElement)(a.Fragment,null,Object(a.createElement)(S.a,{query:t,path:r,filters:b,showDatePicker:!1,advancedFilters:_,report:"customers"}),Object(a.createElement)(v,{isRequesting:e,query:o,filters:b,advancedFilters:_}))}}C.propTypes={query:c.a.object.isRequired}},501:function(e,t,r){"use strict";r.d(t,"b",(function(){return s})),r.d(t,"a",(function(){return m}));var a=r(0),o=r(30),c=r(89),n=r.n(c),l=r(13);const i=n()(l.a),s=e=>{const t=i.getCurrencyConfig(),r=Object(o.applyFilters)("woocommerce_admin_report_currency",t,e);return n()(r)},m=Object(a.createContext)(i)},502:function(e,t,r){"use strict";r.d(t,"e",(function(){return d})),r.d(t,"a",(function(){return u})),r.d(t,"b",(function(){return b})),r.d(t,"c",(function(){return _})),r.d(t,"d",(function(){return p})),r.d(t,"f",(function(){return O})),r.d(t,"h",(function(){return j})),r.d(t,"g",(function(){return y}));var a=r(15),o=r(17),c=r.n(o),n=r(4),l=r(12),i=r(11),s=r(13),m=r(503);function d(e,t=n.identity){return function(r="",o){const n="function"==typeof e?e(o):e,i=Object(l.getIdsFromQuery)(r);if(i.length<1)return Promise.resolve([]);const s={include:i.join(","),per_page:i.length};return c()({path:Object(a.addQueryArgs)(n,s)}).then(e=>e.map(t))}}d(i.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const u=d(i.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),b=d(i.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),_=d(i.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),p=d(i.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),O=d(i.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(m.a)(e)}));function j({attributes:e,name:t}){const r=Object(s.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(r)>-1)return t;const a=(e||[]).map(({option:e})=>e).join(", ");return a?t+r+a:t}const y=d(({products:e})=>e?i.NAMESPACE+`/products/${e}/variations`:i.NAMESPACE+"/variations",e=>({key:e.id,label:j(e)}))},503:function(e,t,r){"use strict";r.d(t,"a",(function(){return o}));var a=r(2);function o(e){return[e.country,e.state,e.name||Object(a.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},504:function(e,t,r){"use strict";var a=r(0),o=r(2),c=r(1),n=r.n(c),l=r(21);function i({className:e}){const t=Object(o.__)("There was an error getting your stats. Please try again.",'woocommerce'),r=Object(o.__)("Reload",'woocommerce');return Object(a.createElement)(l.EmptyContent,{className:e,title:t,actionLabel:r,actionCallback:()=>{window.location.reload()}})}i.propTypes={className:n.a.string},t.a=i},505:function(e,t,r){"use strict";var a=r(0),o=r(14),c=r(1),n=r.n(c),l=r(4),i=r(7),s=r(21),m=r(13),d=r(11),u=r(19),b=r(16),_=r(501),p=r(55);class O extends a.Component{constructor(){super(),this.onDateSelect=this.onDateSelect.bind(this),this.onFilterSelect=this.onFilterSelect.bind(this),this.onAdvancedFilterAction=this.onAdvancedFilterAction.bind(this)}onDateSelect(e){const{report:t,addCesSurveyForAnalytics:r}=this.props;r(),Object(b.recordEvent)("datepicker_update",{report:t,...Object(l.omitBy)(e,l.isUndefined)})}onFilterSelect(e){const{report:t,addCesSurveyForAnalytics:r}=this.props,a=e.filter||e["filter-variations"];["single_product","single_category","single_coupon","single_variation"].includes(a)&&r();const o={report:t,filter:e.filter||"all"};"single_product"===e.filter&&(o.filter_variation=e["filter-variations"]||"all"),Object(b.recordEvent)("analytics_filter",o)}onAdvancedFilterAction(e,t){const{report:r,addCesSurveyForAnalytics:a}=this.props;switch(e){case"add":Object(b.recordEvent)("analytics_filters_add",{report:r,filter:t.key});break;case"remove":Object(b.recordEvent)("analytics_filters_remove",{report:r,filter:t.key});break;case"filter":const e=Object.keys(t).reduce((e,r)=>(e[Object(l.snakeCase)(r)]=t[r],e),{});a(),Object(b.recordEvent)("analytics_filters_filter",{report:r,...e});break;case"clear_all":Object(b.recordEvent)("analytics_filters_clear_all",{report:r});break;case"match":Object(b.recordEvent)("analytics_filters_all_any",{report:r,value:t.match})}}render(){const{advancedFilters:e,filters:t,path:r,query:o,showDatePicker:c,defaultDateRange:n}=this.props,{period:l,compare:i,before:d,after:b}=Object(u.getDateParamsFromQuery)(o,n),{primary:_,secondary:p}=Object(u.getCurrentDates)(o,n),O={period:l,compare:i,before:d,after:b,primaryDate:_,secondaryDate:p},j=this.context;return Object(a.createElement)(s.ReportFilters,{query:o,siteLocale:m.b.siteLocale,currency:j.getCurrencyConfig(),path:r,filters:t,advancedFilters:e,showDatePicker:c,onDateSelect:this.onDateSelect,onFilterSelect:this.onFilterSelect,onAdvancedFilterAction:this.onAdvancedFilterAction,dateQuery:O,isoDateFormat:u.isoDateFormat})}}O.contextType=_.a,t.a=Object(o.compose)(Object(i.withSelect)(e=>{const{woocommerce_default_date_range:t}=e(d.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");return{defaultDateRange:t}}),Object(i.withDispatch)(e=>{const{addCesSurveyForAnalytics:t}=e(p.c);return{addCesSurveyForAnalytics:t}}))(O),O.propTypes={advancedFilters:n.a.object,filters:n.a.array,path:n.a.string.isRequired,query:n.a.object,showDatePicker:n.a.bool,report:n.a.string.isRequired}},506:function(e,t,r){"use strict";var a=r(35),o=r.n(a),c=r(0),n=r(3),l=r(30),i=r(14),s=r(91),m=r(7),d=r(4),u=r(2),b=r(1),_=r.n(b),p=r(21),O=r(12),j=r(474),y=r(11),g=r(16),f=()=>Object(c.createElement)("svg",{role:"img","aria-hidden":"true",focusable:"false",version:"1.1",xmlns:"http://www.w3.org/2000/svg",x:"0px",y:"0px",viewBox:"0 0 24 24"},Object(c.createElement)("path",{d:"M18,9c-0.009,0-0.017,0.002-0.025,0.003C17.72,5.646,14.922,3,11.5,3C7.91,3,5,5.91,5,9.5c0,0.524,0.069,1.031,0.186,1.519 C5.123,11.016,5.064,11,5,11c-2.209,0-4,1.791-4,4c0,1.202,0.541,2.267,1.38,3h18.593C22.196,17.089,23,15.643,23,14 C23,11.239,20.761,9,18,9z M12,16l-4-5h3V8h2v3h3L12,16z"})),w=r(504);var h=r(55);r(515);const v=e=>{const{getHeadersContent:t,getRowsContent:r,getSummary:a,isRequesting:i,primaryData:m,tableData:b,endpoint:_,itemIdField:h,tableQuery:v,compareBy:S,compareParam:C,searchBy:E,labels:x={},...A}=e,{query:R,columnPrefsKey:F}=e,{items:k,query:D}=b,N=R[C]?Object(O.getIdsFromQuery)(R[S]):[],[q,T]=Object(c.useState)(N),P=Object(c.useRef)(null),{updateUserPreferences:B,...M}=Object(y.useUserPreferences)();if(b.isError||m.isError)return Object(c.createElement)(w.a,null);let L=[];F&&(L=M&&M[F]?M[F]:L);const Q=(e,o,c)=>{const n=a?a(o,c):null;return Object(l.applyFilters)("woocommerce_admin_report_table",{endpoint:_,headers:t(),rows:r(e),totals:o,summary:n,items:k})},I=t=>{const{ids:r}=e;T(t?r:[])},V=(t,r)=>{const{ids:a}=e;if(r)T(Object(d.uniq)([a[t],...q]));else{const e=q.indexOf(a[t]);T([...q.slice(0,e),...q.slice(e+1)])}},U=t=>{const{ids:r=[]}=e,a=-1!==q.indexOf(r[t]);return{display:Object(c.createElement)(n.CheckboxControl,{onChange:Object(d.partial)(V,t),checked:a}),value:!1}},H=()=>{const{ids:t=[]}=e,r=t.length>0,a=r&&t.length===q.length;return{cellClassName:"is-checkbox-column",key:"compare",label:Object(c.createElement)(n.CheckboxControl,{onChange:I,"aria-label":Object(u.__)("Select All"),checked:a,disabled:!r}),required:!0}},z=i||b.isRequesting||m.isRequesting,G=Object(d.get)(m,["data","totals"],{}),J=k.totalResults||0,K=J>0,X=Object(O.getSearchWords)(R).map(e=>({key:e,label:e})),{data:Y}=k,W=Q(Y,G,J);let{headers:$,rows:Z}=W;const{summary:ee}=W;S&&(Z=Z.map((e,t)=>[U(t),...e]),$=[H(),...$]);const te=((e,t)=>t?e.map(e=>({...e,visible:e.required||!t.includes(e.key)})):e.map(e=>({...e,visible:e.required||!e.hiddenByDefault})))($,L);return Object(c.createElement)(c.Fragment,null,Object(c.createElement)("div",{className:"woocommerce-report-table__scroll-point",ref:P,"aria-hidden":!0}),Object(c.createElement)(p.TableCard,o()({className:"woocommerce-report-table",hasSearch:!!E,actions:[S&&Object(c.createElement)(p.CompareButton,{key:"compare",className:"woocommerce-table__compare",count:q.length,helpText:x.helpText||Object(u.__)("Check at least two items below to compare",'woocommerce'),onClick:()=>{S&&Object(O.onQueryChange)("compare")(S,C,q.join(","))},disabled:!K},x.compareButton||Object(u.__)("Compare",'woocommerce')),E&&Object(c.createElement)(p.Search,{allowFreeTextSearch:!0,inlineTags:!0,key:"search",onChange:t=>{const{baseSearchQuery:r,addCesSurveyForCustomerSearch:a}=e,o=t.map(e=>e.label.replace(",","%2C"));o.length?(Object(O.updateQueryString)({filter:void 0,[C]:void 0,[E]:void 0,...r,search:Object(d.uniq)(o).join(",")}),a()):Object(O.updateQueryString)({search:void 0}),Object(g.recordEvent)("analytics_table_filter",{report:_})},placeholder:x.placeholder||Object(u.__)("Search by item name",'woocommerce'),selected:X,showClearButton:!0,type:E,disabled:!K}),K&&Object(c.createElement)(n.Button,{key:"download",className:"woocommerce-table__download-button",disabled:z,onClick:()=>{const{createNotice:t,startExport:r,title:a}=e,o=Object.assign({},R),{data:c,totalResults:n}=k;let l="browser";if(delete o.extended_info,o.search&&delete o[E],c&&c.length===n){const{headers:e,rows:t}=Q(c,n);Object(j.downloadCSVFile)(Object(j.generateCSVFileName)(a,o),Object(j.generateCSVDataFromTable)(e,t))}else l="email",r(_,D).then(()=>t("success",Object(u.sprintf)(Object(u.__)("Your %s Report will be emailed to you.",'woocommerce'),a))).catch(e=>t("error",e.message||Object(u.sprintf)(Object(u.__)("There was a problem exporting your %s Report. Please try again.",'woocommerce'),a)));Object(g.recordEvent)("analytics_table_download",{report:_,rows:n,download_type:l})}},Object(c.createElement)(f,null),Object(c.createElement)("span",{className:"woocommerce-table__download-button__label"},x.downloadButton||Object(u.__)("Download",'woocommerce')))],headers:te,isLoading:z,onQueryChange:O.onQueryChange,onColumnsChange:(e,t)=>{const r=$.map(e=>e.key).filter(t=>!e.includes(t));if(F){B({[F]:r})}if(t){const r={report:_,column:t,status:e.includes(t)?"on":"off"};Object(g.recordEvent)("analytics_table_header_toggle",r)}},onSort:(e,t)=>{Object(O.onQueryChange)("sort")(e,t);const r={report:_,column:e,direction:t};Object(g.recordEvent)("analytics_table_sort",r)},onPageChange:(e,t)=>{P.current.scrollIntoView();const r=P.current.nextSibling.querySelector(".woocommerce-table__table"),a=s.focus.focusable.find(r);a.length&&a[0].focus(),t&&("goto"===t?Object(g.recordEvent)("analytics_table_go_to_page",{report:_,page:e}):Object(g.recordEvent)("analytics_table_page_click",{report:_,direction:t}))},rows:Z,rowsPerPage:parseInt(D.per_page,10)||y.QUERY_DEFAULTS.pageSize,summary:ee,totalRows:J},A)))};v.propTypes={baseSearchQuery:_.a.object,compareBy:_.a.string,compareParam:_.a.string,columnPrefsKey:_.a.string,endpoint:_.a.string,extendItemsMethodNames:_.a.shape({getError:_.a.string,isRequesting:_.a.string,load:_.a.string}),extendedItemsStoreName:_.a.string,getHeadersContent:_.a.func.isRequired,getRowsContent:_.a.func.isRequired,getSummary:_.a.func,itemIdField:_.a.string,labels:_.a.shape({compareButton:_.a.string,downloadButton:_.a.string,helpText:_.a.string,placeholder:_.a.string}),primaryData:_.a.object,searchBy:_.a.string,summaryFields:_.a.arrayOf(_.a.string),tableData:_.a.object.isRequired,tableQuery:_.a.object,title:_.a.string.isRequired},v.defaultProps={primaryData:{},tableData:{items:{data:[],totalResults:0},query:{}},tableQuery:{},compareParam:"filter",downloadable:!1,onSearch:d.noop,baseSearchQuery:{}};const S=[],C={};t.a=Object(i.compose)(Object(m.withSelect)((e,t)=>{const{endpoint:r,getSummary:a,isRequesting:o,itemIdField:c,query:n,tableData:l,tableQuery:i,filters:s,advancedFilters:m,summaryFields:u,extendedItemsStoreName:b}=t,_=e(y.REPORTS_STORE_NAME),p=b?e(b):null,{woocommerce_default_date_range:O}=e(y.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");if(o)return C;const j="categories"===r?"products":r,g=a?Object(y.getReportChartData)({endpoint:j,selector:_,dataType:"primary",query:n,filters:s,advancedFilters:m,defaultDateRange:O,fields:u}):C,f=l||Object(y.getReportTableData)({endpoint:r,query:n,selector:_,tableQuery:i,filters:s,advancedFilters:m,defaultDateRange:O}),w=p?function(e,t,r){const{extendItemsMethodNames:a,itemIdField:o}=t,c=r.items.data;if(!(Array.isArray(c)&&c.length&&a&&o))return r;const{[a.getError]:n,[a.isRequesting]:l,[a.load]:i}=e,s={include:c.map(e=>e[o]).join(","),per_page:c.length},m=i(s),u=!!l&&l(s),b=!!n&&n(s),_=c.map(e=>{const t=Object(d.first)(m.filter(t=>e.id===t.id));return{...e,...t}}),p=r.isRequesting||u,O=r.isError||b;return{...r,isRequesting:p,isError:O,items:{...r.items,data:_}}}(p,t,f):f;return{primaryData:g,ids:c&&w.items.data?w.items.data.map(e=>e[c]):S,tableData:w,query:n}}),Object(m.withDispatch)(e=>{const{startExport:t}=e(y.EXPORT_STORE_NAME),{createNotice:r}=e("core/notices"),{addCesSurveyForCustomerSearch:a}=e(h.c);return{createNotice:r,startExport:t,addCesSurveyForCustomerSearch:a}}))(v)},515:function(e,t,r){}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-downloads.js b/packages/woocommerce-admin/dist/chunks/analytics-report-downloads.js new file mode 100644 index 0000000..b4578dc --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-downloads.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[10],{486:function(e,t,r){"use strict";r.r(t),r.d(t,"default",(function(){return A}));var o=r(0),c=r(1),a=r.n(c),n=r(536),d=r(2),l=r(7),m=r(4),i=r(9),s=r.n(i),u=r(21),b=r(12),_=r(120),p=r(13),w=r(11),f=r(19),O=r(506),j=r(501);class y extends o.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(d.__)("Date",'woocommerce'),key:"date",defaultSort:!0,required:!0,isLeftAligned:!0,isSortable:!0},{label:Object(d.__)("Product title",'woocommerce'),key:"product",isSortable:!0,required:!0},{label:Object(d.__)("File name",'woocommerce'),key:"file_name"},{label:Object(d.__)("Order #",'woocommerce'),screenReaderLabel:Object(d.__)("Order Number",'woocommerce'),key:"order_number"},{label:Object(d.__)("Username",'woocommerce'),key:"user_id"},{label:Object(d.__)("IP",'woocommerce'),key:"ip_address"}]}getRowsContent(e){const{query:t}=this.props,r=Object(b.getPersistedQuery)(t),c=Object(p.f)("dateFormat",f.defaultTableDateFormat);return Object(m.map)(e,e=>{const{_embedded:t,date:a,file_name:n,file_path:l,ip_address:m,order_id:i,order_number:s,product_id:_,username:w}=e,{code:f,name:O}=t.product[0];let j,y;if("woocommerce_rest_product_invalid_id"===f)j=Object(d.__)("(Deleted)",'woocommerce'),y=Object(d.__)("(Deleted)",'woocommerce');else{const e=Object(b.getNewPath)(r,"/analytics/products",{filter:"single_product",products:_});j=Object(o.createElement)(u.Link,{href:e,type:"wc-admin"},O),y=O}return[{display:Object(o.createElement)(u.Date,{date:a,visibleFormat:c}),value:a},{display:j,value:y},{display:Object(o.createElement)(u.Link,{href:l,type:"external"},n),value:n},{display:Object(o.createElement)(u.Link,{href:Object(p.e)(`post.php?post=${i}&action=edit`),type:"wp-admin"},s),value:s},{display:w,value:w},{display:m,value:m}]})}getSummary(e){const{download_count:t=0}=e,{query:r,defaultDateRange:o}=this.props,c=Object(f.getCurrentDates)(r,o),a=s()(c.primary.after),n=s()(c.primary.before).diff(a,"days")+1,l=this.context.getCurrencyConfig();return[{label:Object(d._n)("day","days",n,'woocommerce'),value:Object(_.formatValue)(l,"number",n)},{label:Object(d._n)("Download","Downloads",t,'woocommerce'),value:Object(_.formatValue)(l,"number",t)}]}render(){const{query:e,filters:t,advancedFilters:r}=this.props;return Object(o.createElement)(O.a,{endpoint:"downloads",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["download_count"],query:e,tableQuery:{_embed:!0},title:Object(d.__)("Downloads",'woocommerce'),columnPrefsKey:"downloads_report_columns",filters:t,advancedFilters:r})}}y.contextType=j.a;var h=Object(l.withSelect)(e=>{const{woocommerce_default_date_range:t}=e(w.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");return{defaultDateRange:t}})(y),v=r(510),g=r(508),S=r(511),E=r(505);class A extends o.Component{render(){const{query:e,path:t}=this.props;return Object(o.createElement)(o.Fragment,null,Object(o.createElement)(E.a,{query:e,path:t,filters:n.c,advancedFilters:n.a,report:"downloads"}),Object(o.createElement)(S.a,{charts:n.b,endpoint:"downloads",query:e,selectedChart:Object(v.a)(e.chart,n.b),filters:n.c,advancedFilters:n.a}),Object(o.createElement)(g.a,{charts:n.b,endpoint:"downloads",path:t,query:e,selectedChart:Object(v.a)(e.chart,n.b),filters:n.c,advancedFilters:n.a}),Object(o.createElement)(h,{query:e,filters:n.c,advancedFilters:n.a}))}}A.propTypes={query:a.a.object.isRequired}},502:function(e,t,r){"use strict";r.d(t,"e",(function(){return s})),r.d(t,"a",(function(){return u})),r.d(t,"b",(function(){return b})),r.d(t,"c",(function(){return _})),r.d(t,"d",(function(){return p})),r.d(t,"f",(function(){return w})),r.d(t,"h",(function(){return f})),r.d(t,"g",(function(){return O}));var o=r(15),c=r(17),a=r.n(c),n=r(4),d=r(12),l=r(11),m=r(13),i=r(503);function s(e,t=n.identity){return function(r="",c){const n="function"==typeof e?e(c):e,l=Object(d.getIdsFromQuery)(r);if(l.length<1)return Promise.resolve([]);const m={include:l.join(","),per_page:l.length};return a()({path:Object(o.addQueryArgs)(n,m)}).then(e=>e.map(t))}}s(l.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const u=s(l.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),b=s(l.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),_=s(l.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),p=s(l.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),w=s(l.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(i.a)(e)}));function f({attributes:e,name:t}){const r=Object(m.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(r)>-1)return t;const o=(e||[]).map(({option:e})=>e).join(", ");return o?t+r+o:t}const O=s(({products:e})=>e?l.NAMESPACE+`/products/${e}/variations`:l.NAMESPACE+"/variations",e=>({key:e.id,label:f(e)}))},503:function(e,t,r){"use strict";r.d(t,"a",(function(){return c}));var o=r(2);function c(e){return[e.country,e.state,e.name||Object(o.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},536:function(e,t,r){"use strict";r.d(t,"b",(function(){return n})),r.d(t,"c",(function(){return d})),r.d(t,"a",(function(){return l}));var o=r(2),c=r(30),a=r(502);const n=Object(c.applyFilters)("woocommerce_admin_downloads_report_charts",[{key:"download_count",label:Object(o.__)("Downloads",'woocommerce'),type:"number"}]),d=Object(c.applyFilters)("woocommerce_admin_downloads_report_filters",[{label:Object(o.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:[{label:Object(o.__)("All downloads",'woocommerce'),value:"all"},{label:Object(o.__)("Advanced filters",'woocommerce'),value:"advanced"}]}]),l=Object(c.applyFilters)("woocommerce_admin_downloads_report_advanced_filters",{title:Object(o._x)("Downloads match {{select /}} filters","A sentence describing filters for Downloads. See screen shot for context: https://cloudup.com/ccxhyH2mEDg",'woocommerce'),filters:{product:{labels:{add:Object(o.__)("Product",'woocommerce'),placeholder:Object(o.__)("Search",'woocommerce'),remove:Object(o.__)("Remove product filter",'woocommerce'),rule:Object(o.__)("Select a product filter match",'woocommerce'),title:Object(o.__)("{{title}}Product{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select product",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","products",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","products",'woocommerce')}],input:{component:"Search",type:"products",getLabels:a.d}},customer:{labels:{add:Object(o.__)("Username",'woocommerce'),placeholder:Object(o.__)("Search customer username",'woocommerce'),remove:Object(o.__)("Remove customer username filter",'woocommerce'),rule:Object(o.__)("Select a customer username filter match",'woocommerce'),title:Object(o.__)("{{title}}Username{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select customer username",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","customer usernames",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","customer usernames",'woocommerce')}],input:{component:"Search",type:"usernames",getLabels:a.c}},order:{labels:{add:Object(o.__)("Order #",'woocommerce'),placeholder:Object(o.__)("Search order number",'woocommerce'),remove:Object(o.__)("Remove order number filter",'woocommerce'),rule:Object(o.__)("Select a order number filter match",'woocommerce'),title:Object(o.__)("{{title}}Order #{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select order number",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","order numbers",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","order numbers",'woocommerce')}],input:{component:"Search",type:"orders",getLabels:async e=>{const t=e.split(",");return await t.map(e=>({id:e,label:"#"+e}))}}},ip_address:{labels:{add:Object(o.__)("IP Address",'woocommerce'),placeholder:Object(o.__)("Search IP address",'woocommerce'),remove:Object(o.__)("Remove IP address filter",'woocommerce'),rule:Object(o.__)("Select an IP address filter match",'woocommerce'),title:Object(o.__)("{{title}}IP Address{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select IP address",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","IP addresses",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","IP addresses",'woocommerce')}],input:{component:"Search",type:"downloadIps",getLabels:async e=>{const t=e.split(",");return await t.map(e=>({id:e,label:e}))}}}}})}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-orders.js b/packages/woocommerce-admin/dist/chunks/analytics-report-orders.js new file mode 100644 index 0000000..4e127fb --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-orders.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[11],{483:function(e,t,o){"use strict";o.r(t),o.d(t,"default",(function(){return h}));var r=o(0),c=o(1),a=o.n(c),n=o(533),m=o(510),l=o(2),i=o(4),s=o(21),d=o(120),u=o(13),b=o(12),_=o(19),p=o(506),O=o(501);o(581);class j extends r.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(l.__)("Date",'woocommerce'),key:"date",required:!0,defaultSort:!0,isLeftAligned:!0,isSortable:!0},{label:Object(l.__)("Order #",'woocommerce'),screenReaderLabel:Object(l.__)("Order Number",'woocommerce'),key:"order_number",required:!0},{label:Object(l.__)("Status",'woocommerce'),key:"status",required:!1,isSortable:!1},{label:Object(l.__)("Customer",'woocommerce'),key:"customer_id",required:!1,isSortable:!1},{label:Object(l.__)("Customer type",'woocommerce'),key:"customer_type",required:!1,isSortable:!1},{label:Object(l.__)("Product(s)",'woocommerce'),screenReaderLabel:Object(l.__)("Products",'woocommerce'),key:"products",required:!1,isSortable:!1},{label:Object(l.__)("Items sold",'woocommerce'),key:"num_items_sold",required:!1,isSortable:!0,isNumeric:!0},{label:Object(l.__)("Coupon(s)",'woocommerce'),screenReaderLabel:Object(l.__)("Coupons",'woocommerce'),key:"coupons",required:!1,isSortable:!1},{label:Object(l.__)("Net sales",'woocommerce'),screenReaderLabel:Object(l.__)("Net sales",'woocommerce'),key:"net_total",required:!0,isSortable:!0,isNumeric:!0}]}getCustomerName(e){const{first_name:t,last_name:o}=e||{};return t||o?[t,o].join(" "):""}getRowsContent(e){const{query:t}=this.props,o=Object(b.getPersistedQuery)(t),c=Object(u.f)("dateFormat",_.defaultTableDateFormat),{render:a,getCurrencyConfig:n}=this.context;return Object(i.map)(e,e=>{const{currency:t,date_created:m,net_total:i,num_items_sold:_,order_id:p,order_number:O,parent_id:j,status:w,customer_type:f}=e,y=e.extended_info||{},{coupons:v,customer:h,products:S}=y,g=S.sort((e,t)=>t.quantity-e.quantity).map(e=>({label:e.name,quantity:e.quantity,href:Object(b.getNewPath)(o,"/analytics/products",{filter:"single_product",products:e.id})})),C=v.map(e=>({label:e.code,href:Object(b.getNewPath)(o,"/analytics/coupons",{filter:"single_coupon",coupons:e.id})}));return[{display:Object(r.createElement)(s.Date,{date:m,visibleFormat:c}),value:m},{display:Object(r.createElement)(s.Link,{href:"post.php?post="+(j||p)+"&action=edit"+(j?"#order_refunds":""),type:"wp-admin"},O),value:O},{display:Object(r.createElement)(s.OrderStatus,{className:"woocommerce-orders-table__status",order:{status:w},orderStatusMap:Object(u.f)("orderStatuses",{})}),value:w},{display:this.getCustomerName(h),value:this.getCustomerName(h)},{display:(x=f,x.charAt(0).toUpperCase()+x.slice(1)),value:f},{display:this.renderList(g.length?[g[0]]:[],g.map(e=>({label:Object(l.sprintf)(Object(l.__)("%s× %s",'woocommerce'),e.quantity,e.label),href:e.href}))),value:g.map(({quantity:e,label:t})=>Object(l.sprintf)(Object(l.__)("%s× %s",'woocommerce'),e,t)).join(", ")},{display:Object(d.formatValue)(n(),"number",_),value:_},{display:this.renderList(C.length?[C[0]]:[],C),value:C.map(e=>e.label).join(", ")},{display:a(i,t),value:i}];var x})}getSummary(e){const{orders_count:t=0,total_customers:o=0,products:r=0,num_items_sold:c=0,coupons_count:a=0,net_revenue:n=0}=e,{formatAmount:m,getCurrencyConfig:i}=this.context,s=i();return[{label:Object(l._n)("Order","Orders",t,'woocommerce'),value:Object(d.formatValue)(s,"number",t)},{label:Object(l._n)(" Customer"," Customers",o,'woocommerce'),value:Object(d.formatValue)(s,"number",o)},{label:Object(l._n)("Product","Products",r,'woocommerce'),value:Object(d.formatValue)(s,"number",r)},{label:Object(l._n)("Item sold","Items sold",c,'woocommerce'),value:Object(d.formatValue)(s,"number",c)},{label:Object(l._n)("Coupon","Coupons",a,'woocommerce'),value:Object(d.formatValue)(s,"number",a)},{label:Object(l.__)("net sales",'woocommerce'),value:m(n)}]}renderLinks(e=[]){return e.map((e,t)=>Object(r.createElement)(s.Link,{href:e.href,key:t,type:"wc-admin"},e.label))}renderList(e,t){return Object(r.createElement)(r.Fragment,null,this.renderLinks(e),t.length>1&&Object(r.createElement)(s.ViewMoreList,{items:this.renderLinks(t)}))}render(){const{query:e,filters:t,advancedFilters:o}=this.props;return Object(r.createElement)(p.a,{endpoint:"orders",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["orders_count","total_customers","products","num_items_sold","coupons_count","net_revenue"],query:e,tableQuery:{extended_info:!0},title:Object(l.__)("Orders",'woocommerce'),columnPrefsKey:"orders_report_columns",filters:t,advancedFilters:o})}}j.contextType=O.a;var w=j,f=o(508),y=o(511),v=o(505);class h extends r.Component{render(){const{path:e,query:t}=this.props;return Object(r.createElement)(r.Fragment,null,Object(r.createElement)(v.a,{query:t,path:e,filters:n.c,advancedFilters:n.a,report:"orders"}),Object(r.createElement)(y.a,{charts:n.b,endpoint:"orders",query:t,selectedChart:Object(m.a)(t.chart,n.b),filters:n.c,advancedFilters:n.a}),Object(r.createElement)(f.a,{charts:n.b,endpoint:"orders",path:e,query:t,selectedChart:Object(m.a)(t.chart,n.b),filters:n.c,advancedFilters:n.a}),Object(r.createElement)(w,{query:t,filters:n.c,advancedFilters:n.a}))}}h.propTypes={path:a.a.string.isRequired,query:a.a.object.isRequired}},502:function(e,t,o){"use strict";o.d(t,"e",(function(){return d})),o.d(t,"a",(function(){return u})),o.d(t,"b",(function(){return b})),o.d(t,"c",(function(){return _})),o.d(t,"d",(function(){return p})),o.d(t,"f",(function(){return O})),o.d(t,"h",(function(){return j})),o.d(t,"g",(function(){return w}));var r=o(15),c=o(17),a=o.n(c),n=o(4),m=o(12),l=o(11),i=o(13),s=o(503);function d(e,t=n.identity){return function(o="",c){const n="function"==typeof e?e(c):e,l=Object(m.getIdsFromQuery)(o);if(l.length<1)return Promise.resolve([]);const i={include:l.join(","),per_page:l.length};return a()({path:Object(r.addQueryArgs)(n,i)}).then(e=>e.map(t))}}d(l.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const u=d(l.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),b=d(l.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),_=d(l.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),p=d(l.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),O=d(l.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(s.a)(e)}));function j({attributes:e,name:t}){const o=Object(i.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(o)>-1)return t;const r=(e||[]).map(({option:e})=>e).join(", ");return r?t+o+r:t}const w=d(({products:e})=>e?l.NAMESPACE+`/products/${e}/variations`:l.NAMESPACE+"/variations",e=>({key:e.id,label:j(e)}))},503:function(e,t,o){"use strict";o.d(t,"a",(function(){return c}));var r=o(2);function c(e){return[e.country,e.state,e.name||Object(r.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},533:function(e,t,o){"use strict";o.d(t,"b",(function(){return m})),o.d(t,"c",(function(){return l})),o.d(t,"a",(function(){return i}));var r=o(2),c=o(30),a=o(13),n=o(502);const m=Object(c.applyFilters)("woocommerce_admin_orders_report_charts",[{key:"orders_count",label:Object(r.__)("Orders",'woocommerce'),type:"number"},{key:"net_revenue",label:Object(r.__)("Net sales",'woocommerce'),order:"desc",orderby:"net_total",type:"currency"},{key:"avg_order_value",label:Object(r.__)("Average order value",'woocommerce'),type:"currency"},{key:"avg_items_per_order",label:Object(r.__)("Average items per order",'woocommerce'),order:"desc",orderby:"num_items_sold",type:"average"}]),l=Object(c.applyFilters)("woocommerce_admin_orders_report_filters",[{label:Object(r.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:[{label:Object(r.__)("All orders",'woocommerce'),value:"all"},{label:Object(r.__)("Advanced filters",'woocommerce'),value:"advanced"}]}]),i=Object(c.applyFilters)("woocommerce_admin_orders_report_advanced_filters",{title:Object(r._x)("Orders match {{select /}} filters","A sentence describing filters for Orders. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce'),filters:{status:{labels:{add:Object(r.__)("Order Status",'woocommerce'),remove:Object(r.__)("Remove order status filter",'woocommerce'),rule:Object(r.__)("Select an order status filter match",'woocommerce'),title:Object(r.__)("{{title}}Order Status{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(r.__)("Select an order status",'woocommerce')},rules:[{value:"is",label:Object(r._x)("Is","order status",'woocommerce')},{value:"is_not",label:Object(r._x)("Is Not","order status",'woocommerce')}],input:{component:"SelectControl",options:Object.keys(a.c).map(e=>({value:e,label:a.c[e]}))}},product:{labels:{add:Object(r.__)("Products",'woocommerce'),placeholder:Object(r.__)("Search products",'woocommerce'),remove:Object(r.__)("Remove products filter",'woocommerce'),rule:Object(r.__)("Select a product filter match",'woocommerce'),title:Object(r.__)("{{title}}Product{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(r.__)("Select products",'woocommerce')},rules:[{value:"includes",label:Object(r._x)("Includes","products",'woocommerce')},{value:"excludes",label:Object(r._x)("Excludes","products",'woocommerce')}],input:{component:"Search",type:"products",getLabels:n.d}},variation:{labels:{add:Object(r.__)("Variations",'woocommerce'),placeholder:Object(r.__)("Search variations",'woocommerce'),remove:Object(r.__)("Remove variations filter",'woocommerce'),rule:Object(r.__)("Select a variation filter match",'woocommerce'),title:Object(r.__)("{{title}}Variation{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(r.__)("Select variation",'woocommerce')},rules:[{value:"includes",label:Object(r._x)("Includes","variations",'woocommerce')},{value:"excludes",label:Object(r._x)("Excludes","variations",'woocommerce')}],input:{component:"Search",type:"variations",getLabels:n.g}},coupon:{labels:{add:Object(r.__)("Coupon Codes",'woocommerce'),placeholder:Object(r.__)("Search coupons",'woocommerce'),remove:Object(r.__)("Remove coupon filter",'woocommerce'),rule:Object(r.__)("Select a coupon filter match",'woocommerce'),title:Object(r.__)("{{title}}Coupon code{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(r.__)("Select coupon codes",'woocommerce')},rules:[{value:"includes",label:Object(r._x)("Includes","coupon code",'woocommerce')},{value:"excludes",label:Object(r._x)("Excludes","coupon code",'woocommerce')}],input:{component:"Search",type:"coupons",getLabels:n.b}},customer_type:{labels:{add:Object(r.__)("Customer type",'woocommerce'),remove:Object(r.__)("Remove customer filter",'woocommerce'),rule:Object(r.__)("Select a customer filter match",'woocommerce'),title:Object(r.__)("{{title}}Customer is{{/title}} {{filter /}}",'woocommerce'),filter:Object(r.__)("Select a customer type",'woocommerce')},input:{component:"SelectControl",options:[{value:"new",label:Object(r.__)("New",'woocommerce')},{value:"returning",label:Object(r.__)("Returning",'woocommerce')}],defaultOption:"new"}},refunds:{labels:{add:Object(r.__)("Refunds",'woocommerce'),remove:Object(r.__)("Remove refunds filter",'woocommerce'),rule:Object(r.__)("Select a refund filter match",'woocommerce'),title:Object(r.__)("{{title}}Refunds{{/title}} {{filter /}}",'woocommerce'),filter:Object(r.__)("Select a refund type",'woocommerce')},input:{component:"SelectControl",options:[{value:"all",label:Object(r.__)("All",'woocommerce')},{value:"partial",label:Object(r.__)("Partially refunded",'woocommerce')},{value:"full",label:Object(r.__)("Fully refunded",'woocommerce')},{value:"none",label:Object(r.__)("None",'woocommerce')}],defaultOption:"all"}},tax_rate:{labels:{add:Object(r.__)("Tax Rates",'woocommerce'),placeholder:Object(r.__)("Search tax rates",'woocommerce'),remove:Object(r.__)("Remove tax rate filter",'woocommerce'),rule:Object(r.__)("Select a tax rate filter match",'woocommerce'),title:Object(r.__)("{{title}}Tax Rate{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(r.__)("Select tax rates",'woocommerce')},rules:[{value:"includes",label:Object(r._x)("Includes","tax rate",'woocommerce')},{value:"excludes",label:Object(r._x)("Excludes","tax rate",'woocommerce')}],input:{component:"Search",type:"taxes",getLabels:n.f}},attribute:{allowMultiple:!0,labels:{add:Object(r.__)("Attribute",'woocommerce'),placeholder:Object(r.__)("Search attributes",'woocommerce'),remove:Object(r.__)("Remove attribute filter",'woocommerce'),rule:Object(r.__)("Select a product attribute filter match",'woocommerce'),title:Object(r.__)("{{title}}Attribute{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(r.__)("Select attributes",'woocommerce')},rules:[{value:"is",label:Object(r._x)("Is","product attribute",'woocommerce')},{value:"is_not",label:Object(r._x)("Is Not","product attribute",'woocommerce')}],input:{component:"ProductAttribute"}}}})},581:function(e,t,o){}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-products.js b/packages/woocommerce-admin/dist/chunks/analytics-report-products.js new file mode 100644 index 0000000..9af64ee --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-products.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[12],{477:function(e,t,r){"use strict";r.r(t);var o=r(0),a=r(2),c=r(14),n=r(1),s=r.n(n),i=r(11),l=r(7),m=r(529),d=r(510),u=r(530),b=r(508),p=r(504),_=r(511),y=r(532),g=r(505);class O extends o.Component{getChartMeta(){const{query:e,isSingleProductView:t,isSingleProductVariable:r}=this.props,o="compare-products"===e.filter&&e.products&&e.products.split(",").length>1||t&&r?"item-comparison":"time-comparison";return{compareObject:t&&r?"variations":"products",itemsLabel:t&&r?Object(a.__)("%d variations",'woocommerce'):Object(a.__)("%d products",'woocommerce'),mode:o}}render(){const{compareObject:e,itemsLabel:t,mode:r}=this.getChartMeta(),{path:a,query:c,isError:n,isRequesting:s,isSingleProductVariable:i}=this.props;if(n)return Object(o.createElement)(p.a,null);const l={...c};return"item-comparison"===r&&(l.segmentby="products"===e?"product":"variation"),Object(o.createElement)(o.Fragment,null,Object(o.createElement)(g.a,{query:c,path:a,filters:m.c,advancedFilters:m.a,report:"products"}),Object(o.createElement)(_.a,{mode:r,charts:m.b,endpoint:"products",isRequesting:s,query:l,selectedChart:Object(d.a)(c.chart,m.b),filters:m.c,advancedFilters:m.a}),Object(o.createElement)(b.a,{charts:m.b,mode:r,filters:m.c,advancedFilters:m.a,endpoint:"products",isRequesting:s,itemsLabel:t,path:a,query:l,selectedChart:Object(d.a)(l.chart,m.b)}),i?Object(o.createElement)(y.a,{baseSearchQuery:{filter:"single_product"},isRequesting:s,query:c,filters:m.c,advancedFilters:m.a}):Object(o.createElement)(u.a,{isRequesting:s,query:c,filters:m.c,advancedFilters:m.a}))}}O.propTypes={path:s.a.string.isRequired,query:s.a.object.isRequired},t.default=Object(c.compose)(Object(l.withSelect)((e,t)=>{const{query:r,isRequesting:o}=t,a=!r.search&&r.products&&1===r.products.split(",").length,{getItems:c,isResolving:n,getItemsError:s}=e(i.ITEMS_STORE_NAME);if(o)return{query:{...r},isSingleProductView:a,isRequesting:o};if(a){const e=parseInt(r.products,10),t={include:e},o=c("products",t),i=o&&o.get(e)&&"variable"===o.get(e).type,l=n("getItems",["products",t]),m=Boolean(s("products",t));return{query:{...r,"is-variable":i},isSingleProductView:a,isRequesting:l,isSingleProductVariable:i,isError:m}}return{query:r,isSingleProductView:a}}))(O)},502:function(e,t,r){"use strict";r.d(t,"e",(function(){return d})),r.d(t,"a",(function(){return u})),r.d(t,"b",(function(){return b})),r.d(t,"c",(function(){return p})),r.d(t,"d",(function(){return _})),r.d(t,"f",(function(){return y})),r.d(t,"h",(function(){return g})),r.d(t,"g",(function(){return O}));var o=r(15),a=r(17),c=r.n(a),n=r(4),s=r(12),i=r(11),l=r(13),m=r(503);function d(e,t=n.identity){return function(r="",a){const n="function"==typeof e?e(a):e,i=Object(s.getIdsFromQuery)(r);if(i.length<1)return Promise.resolve([]);const l={include:i.join(","),per_page:i.length};return c()({path:Object(o.addQueryArgs)(n,l)}).then(e=>e.map(t))}}d(i.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const u=d(i.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),b=d(i.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),p=d(i.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),_=d(i.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),y=d(i.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(m.a)(e)}));function g({attributes:e,name:t}){const r=Object(l.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(r)>-1)return t;const o=(e||[]).map(({option:e})=>e).join(", ");return o?t+r+o:t}const O=d(({products:e})=>e?i.NAMESPACE+`/products/${e}/variations`:i.NAMESPACE+"/variations",e=>({key:e.id,label:g(e)}))},503:function(e,t,r){"use strict";r.d(t,"a",(function(){return a}));var o=r(2);function a(e){return[e.country,e.state,e.name||Object(o.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},513:function(e,t,r){"use strict";function o(e,t,r){return!!t&&(e&&t<=r==="instock")}r.d(t,"a",(function(){return o}))},518:function(e,t,r){"use strict";r.d(t,"a",(function(){return i}));var o=r(0),a=r(4),c=r(3),n=r(21),s=r(12);class i extends o.Component{getCategoryAncestorIds(e,t){const r=[];let o=e.parent;for(;o;)r.unshift(o),o=t.get(o).parent;return r}getCategoryAncestors(e,t){const r=this.getCategoryAncestorIds(e,t);if(r.length)return 1===r.length?t.get(Object(a.first)(r)).name+" › ":2===r.length?t.get(Object(a.first)(r)).name+" › "+t.get(Object(a.last)(r)).name+" › ":t.get(Object(a.first)(r)).name+" … "+t.get(Object(a.last)(r)).name+" › "}render(){const{categories:e,category:t,query:r}=this.props,a=Object(s.getPersistedQuery)(r);return t?Object(o.createElement)("div",{className:"woocommerce-table__breadcrumbs"},this.getCategoryAncestors(t,e),Object(o.createElement)(n.Link,{href:Object(s.getNewPath)(a,"/analytics/categories",{filter:"single_category",categories:t.id}),type:"wc-admin"},t.name)):Object(o.createElement)(c.Spinner,null)}}},529:function(e,t,r){"use strict";r.d(t,"b",(function(){return l})),r.d(t,"a",(function(){return u})),r.d(t,"c",(function(){return b}));var o=r(2),a=r(30),c=r(7),n=r(502),s=r(55);const{addCesSurveyForAnalytics:i}=Object(c.dispatch)(s.c),l=Object(a.applyFilters)("woocommerce_admin_products_report_charts",[{key:"items_sold",label:Object(o.__)("Items sold",'woocommerce'),order:"desc",orderby:"items_sold",type:"number"},{key:"net_revenue",label:Object(o.__)("Net sales",'woocommerce'),order:"desc",orderby:"net_revenue",type:"currency"},{key:"orders_count",label:Object(o.__)("Orders",'woocommerce'),order:"desc",orderby:"orders_count",type:"number"}]),m={label:Object(o.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:[{label:Object(o.__)("All products",'woocommerce'),value:"all"},{label:Object(o.__)("Single product",'woocommerce'),value:"select_product",chartMode:"item-comparison",subFilters:[{component:"Search",value:"single_product",chartMode:"item-comparison",path:["select_product"],settings:{type:"products",param:"products",getLabels:n.d,labels:{placeholder:Object(o.__)("Type to search for a product",'woocommerce'),button:Object(o.__)("Single product",'woocommerce')}}}]},{label:Object(o.__)("Comparison",'woocommerce'),value:"compare-products",chartMode:"item-comparison",settings:{type:"products",param:"products",getLabels:n.d,labels:{helpText:Object(o.__)("Check at least two products below to compare",'woocommerce'),placeholder:Object(o.__)("Search for products to compare",'woocommerce'),title:Object(o.__)("Compare Products",'woocommerce'),update:Object(o.__)("Compare",'woocommerce')},onClick:i}}]},d={showFilters:e=>"single_product"===e.filter&&!!e.products&&e["is-variable"],staticParams:["filter","products","chartType","paged","per_page"],param:"filter-variations",filters:[{label:Object(o.__)("All variations",'woocommerce'),chartMode:"item-comparison",value:"all"},{label:Object(o.__)("Single variation",'woocommerce'),value:"select_variation",subFilters:[{component:"Search",value:"single_variation",path:["select_variation"],settings:{type:"variations",param:"variations",getLabels:n.g,labels:{placeholder:Object(o.__)("Type to search for a variation",'woocommerce'),button:Object(o.__)("Single variation",'woocommerce')}}}]},{label:Object(o.__)("Comparison",'woocommerce'),chartMode:"item-comparison",value:"compare-variations",settings:{type:"variations",param:"variations",getLabels:n.g,labels:{helpText:Object(o.__)("Check at least two variations below to compare",'woocommerce'),placeholder:Object(o.__)("Search for variations to compare",'woocommerce'),title:Object(o.__)("Compare Variations",'woocommerce'),update:Object(o.__)("Compare",'woocommerce')}}}]},u=Object(a.applyFilters)("woocommerce_admin_products_report_advanced_filters",{filters:{},title:Object(o._x)("Products Match {{select /}} Filters","A sentence describing filters for Products. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')});Object.keys(u.filters).length&&(m.filters.push({label:Object(o.__)("Advanced Filters",'woocommerce'),value:"advanced"}),d.filters.push({label:Object(o.__)("Advanced Filters",'woocommerce'),value:"advanced"}));const b=Object(a.applyFilters)("woocommerce_admin_products_report_filters",[m,d])},530:function(e,t,r){"use strict";var o=r(0),a=r(2),c=r(14),n=r(28),s=r(7),i=r(4),l=r(12),m=r(21),d=r(120),u=r(13),b=r(11),p=r(518),_=r(513),y=r(506),g=r(501);r(531);const O=Object(u.f)("manageStock","no"),j=Object(u.f)("stockStatuses",{});class h extends o.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(a.__)("Product title",'woocommerce'),key:"product_name",required:!0,isLeftAligned:!0,isSortable:!0},{label:Object(a.__)("SKU",'woocommerce'),key:"sku",hiddenByDefault:!0,isSortable:!0},{label:Object(a.__)("Items sold",'woocommerce'),key:"items_sold",required:!0,defaultSort:!0,isSortable:!0,isNumeric:!0},{label:Object(a.__)("Net sales",'woocommerce'),screenReaderLabel:Object(a.__)("Net sales",'woocommerce'),key:"net_revenue",required:!0,isSortable:!0,isNumeric:!0},{label:Object(a.__)("Orders",'woocommerce'),key:"orders_count",isSortable:!0,isNumeric:!0},{label:Object(a.__)("Category",'woocommerce'),key:"product_cat"},{label:Object(a.__)("Variations",'woocommerce'),key:"variations",isSortable:!0},"yes"===O?{label:Object(a.__)("Status",'woocommerce'),key:"stock_status"}:null,"yes"===O?{label:Object(a.__)("Stock",'woocommerce'),key:"stock",isNumeric:!0}:null].filter(Boolean)}getRowsContent(e=[]){const{query:t}=this.props,r=Object(l.getPersistedQuery)(t),{render:c,formatDecimal:s,getCurrencyConfig:b}=this.context,y=b();return Object(i.map)(e,e=>{const{product_id:i,items_sold:b,net_revenue:g,orders_count:h}=e,f=e.extended_info||{},{category_ids:w,low_stock_amount:v,manage_stock:S,sku:C,stock_status:k,stock_quantity:E,variations:q=[]}=f,A=Object(n.decodeEntities)(f.name),P=Object(l.getNewPath)(r,"/analytics/orders",{filter:"advanced",product_includes:i}),N=Object(l.getNewPath)(r,"/analytics/products",{filter:"single_product",products:i}),{categories:R}=this.props,x=w&&R&&w.map(e=>R.get(e)).filter(Boolean)||[],F=Object(_.a)(k,E,v)?Object(o.createElement)(m.Link,{href:Object(u.e)("post.php?action=edit&post="+i),type:"wp-admin"},Object(a._x)("Low","Indication of a low quantity",'woocommerce')):j[k];return[{display:Object(o.createElement)(m.Link,{href:N,type:"wc-admin"},A),value:A},{display:C,value:C},{display:Object(d.formatValue)(y,"number",b),value:b},{display:c(g),value:s(g)},{display:Object(o.createElement)(m.Link,{href:P,type:"wc-admin"},h),value:h},{display:Object(o.createElement)("div",{className:"woocommerce-table__product-categories"},x[0]&&Object(o.createElement)(p.a,{category:x[0],categories:R}),x.length>1&&Object(o.createElement)(m.Tag,{label:Object(a.sprintf)(Object(a._x)("+%d more","categories",'woocommerce'),x.length-1),popoverContents:x.map(e=>Object(o.createElement)(p.a,{category:e,categories:R,key:e.id,query:t}))})),value:x.map(e=>e.name).join(", ")},{display:Object(d.formatValue)(y,"number",q.length),value:q.length},"yes"===O?{display:S?F:Object(a.__)("N/A",'woocommerce'),value:S?j[k]:null}:null,"yes"===O?{display:S?Object(d.formatValue)(y,"number",E):Object(a.__)("N/A",'woocommerce'),value:E}:null].filter(Boolean)})}getSummary(e){const{products_count:t=0,items_sold:r=0,net_revenue:o=0,orders_count:c=0}=e,{formatAmount:n,getCurrencyConfig:s}=this.context,i=s();return[{label:Object(a._n)("Product","Products",t,'woocommerce'),value:Object(d.formatValue)(i,"number",t)},{label:Object(a._n)("Item sold","Items sold",r,'woocommerce'),value:Object(d.formatValue)(i,"number",r)},{label:Object(a.__)("Net sales",'woocommerce'),value:n(o)},{label:Object(a._n)("Orders","Orders",c,'woocommerce'),value:Object(d.formatValue)(i,"number",c)}]}render(){const{advancedFilters:e,baseSearchQuery:t,filters:r,hideCompare:c,isRequesting:n,query:s}=this.props,i={helpText:Object(a.__)("Check at least two products below to compare",'woocommerce'),placeholder:Object(a.__)("Search by product name or SKU",'woocommerce')};return Object(o.createElement)(y.a,{compareBy:c?void 0:"products",endpoint:"products",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["products_count","items_sold","net_revenue","orders_count"],itemIdField:"product_id",isRequesting:n,labels:i,query:s,searchBy:"products",baseSearchQuery:t,tableQuery:{orderby:s.orderby||"items_sold",order:s.order||"desc",extended_info:!0,segmentby:s.segmentby},title:Object(a.__)("Products",'woocommerce'),columnPrefsKey:"products_report_columns",filters:r,advancedFilters:e})}}h.contextType=g.a,t.a=Object(c.compose)(Object(s.withSelect)((e,t)=>{const{query:r,isRequesting:o}=t,{getItems:a,getItemsError:c,isResolving:n}=e(b.ITEMS_STORE_NAME);if(o||r.search&&(!r.products||!r.products.length))return{};const s={per_page:-1};return{categories:a("categories",s),isError:Boolean(c("categories",s)),isRequesting:n("getItems",["categories",s])}}))(h)},531:function(e,t,r){},532:function(e,t,r){"use strict";var o=r(0),a=r(2),c=r(4),n=r(21),s=r(12),i=r(120),l=r(13),m=r(506),d=r(513),u=r(501),b=r(502);const p=Object(l.f)("manageStock","no"),_=Object(l.f)("stockStatuses",{});class y extends o.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(a.__)("Product / Variation title",'woocommerce'),key:"name",required:!0,isLeftAligned:!0},{label:Object(a.__)("SKU",'woocommerce'),key:"sku",hiddenByDefault:!0,isSortable:!0},{label:Object(a.__)("Items sold",'woocommerce'),key:"items_sold",required:!0,defaultSort:!0,isSortable:!0,isNumeric:!0},{label:Object(a.__)("Net sales",'woocommerce'),screenReaderLabel:Object(a.__)("Net sales",'woocommerce'),key:"net_revenue",required:!0,isSortable:!0,isNumeric:!0},{label:Object(a.__)("Orders",'woocommerce'),key:"orders_count",isSortable:!0,isNumeric:!0},"yes"===p?{label:Object(a.__)("Status",'woocommerce'),key:"stock_status"}:null,"yes"===p?{label:Object(a.__)("Stock",'woocommerce'),key:"stock",isNumeric:!0}:null].filter(Boolean)}getRowsContent(e=[]){const{query:t}=this.props,r=Object(s.getPersistedQuery)(t),{formatAmount:m,formatDecimal:u,getCurrencyConfig:y}=this.context;return Object(c.map)(e,e=>{const{items_sold:t,net_revenue:c,orders_count:g,product_id:O,variation_id:j}=e,h=e.extended_info||{},{stock_status:f,stock_quantity:w,low_stock_amount:v,sku:S}=h,C=(k=e,Object(b.h)(k.extended_info||{}));var k;const E=Object(s.getNewPath)(r,"/analytics/orders",{filter:"advanced",variation_includes:j}),q=Object(l.e)(`post.php?post=${O}&action=edit`);return[{display:Object(o.createElement)(n.Link,{href:q,type:"wp-admin"},C),value:C},{display:S,value:S},{display:Object(i.formatValue)(y(),"number",t),value:t},{display:m(c),value:u(c)},{display:Object(o.createElement)(n.Link,{href:E,type:"wc-admin"},g),value:g},"yes"===p?{display:Object(d.a)(f,w,v)?Object(o.createElement)(n.Link,{href:q,type:"wp-admin"},Object(a._x)("Low","Indication of a low quantity",'woocommerce')):_[f],value:_[f]}:null,"yes"===p?{display:w,value:w}:null].filter(Boolean)})}getSummary(e){const{variations_count:t=0,items_sold:r=0,net_revenue:o=0,orders_count:c=0}=e,{formatAmount:n,getCurrencyConfig:s}=this.context,l=s();return[{label:Object(a._n)("variation sold","variations sold",t,'woocommerce'),value:Object(i.formatValue)(l,"number",t)},{label:Object(a._n)("item sold","items sold",r,'woocommerce'),value:Object(i.formatValue)(l,"number",r)},{label:Object(a.__)("net sales",'woocommerce'),value:n(o)},{label:Object(a._n)("orders","orders",c,'woocommerce'),value:Object(i.formatValue)(l,"number",c)}]}render(){const{advancedFilters:e,baseSearchQuery:t,filters:r,isRequesting:c,query:n}=this.props,s={helpText:Object(a.__)("Check at least two variations below to compare",'woocommerce'),placeholder:Object(a.__)("Search by variation name or SKU",'woocommerce')};return Object(o.createElement)(m.a,{baseSearchQuery:t,compareBy:"variations",compareParam:"filter-variations",endpoint:"variations",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,isRequesting:c,itemIdField:"variation_id",labels:s,query:n,getSummary:this.getSummary,summaryFields:["variations_count","items_sold","net_revenue","orders_count"],tableQuery:{orderby:n.orderby||"items_sold",order:n.order||"desc",extended_info:!0,product_includes:n.products,variations:n.variations},title:Object(a.__)("Variations",'woocommerce'),columnPrefsKey:"variations_report_columns",filters:r,advancedFilters:e})}}y.contextType=u.a,t.a=y}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-revenue.js b/packages/woocommerce-admin/dist/chunks/analytics-report-revenue.js new file mode 100644 index 0000000..aec1b4e --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-revenue.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[13],{481:function(e,t,r){"use strict";r.r(t),r.d(t,"default",(function(){return F}));var a=r(0),s=r(1),o=r.n(s),n=r(528),c=r(510),l=r(508),i=r(511),d=r(2),m=r(59),u=r(7),b=r(14),p=r(4),_=r(21),y=r(120),v=r(13),O=r(11),j=r(19),g=r(32),f=r(506),w=r(501);const h=[],R=["orders_count","gross_sales","total_sales","refunds","coupons","taxes","shipping","net_revenue"];class S extends a.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(d.__)("Date",'woocommerce'),key:"date",required:!0,defaultSort:!0,isLeftAligned:!0,isSortable:!0},{label:Object(d.__)("Orders",'woocommerce'),key:"orders_count",required:!1,isSortable:!0,isNumeric:!0},{label:Object(d.__)("Gross sales",'woocommerce'),key:"gross_sales",required:!1,isSortable:!0,isNumeric:!0},{label:Object(d.__)("Returns",'woocommerce'),key:"refunds",required:!1,isSortable:!0,isNumeric:!0},{label:Object(d.__)("Coupons",'woocommerce'),key:"coupons",required:!1,isSortable:!0,isNumeric:!0},{label:Object(d.__)("Net sales",'woocommerce'),key:"net_revenue",required:!1,isSortable:!0,isNumeric:!0},{label:Object(d.__)("Taxes",'woocommerce'),key:"taxes",required:!1,isSortable:!0,isNumeric:!0},{label:Object(d.__)("Shipping",'woocommerce'),key:"shipping",required:!1,isSortable:!0,isNumeric:!0},{label:Object(d.__)("Total sales",'woocommerce'),key:"total_sales",required:!1,isSortable:!0,isNumeric:!0}]}getRowsContent(e=[]){const t=Object(v.f)("dateFormat",j.defaultTableDateFormat),{formatAmount:r,render:s,formatDecimal:o,getCurrencyConfig:n}=this.context;return e.map(e=>{const{coupons:c,gross_sales:l,total_sales:i,net_revenue:d,orders_count:u,refunds:b,shipping:p,taxes:v}=e.subtotals,O=Object(a.createElement)(_.Link,{href:"edit.php?post_type=shop_order&m="+Object(m.format)("Ymd",e.date_start),type:"wp-admin"},Object(y.formatValue)(n(),"number",u));return[{display:Object(a.createElement)(_.Date,{date:e.date_start,visibleFormat:t}),value:e.date_start},{display:O,value:Number(u)},{display:s(l),value:o(l)},{display:r(b),value:o(b)},{display:r(c),value:o(c)},{display:s(d),value:o(d)},{display:s(v),value:o(v)},{display:s(p),value:o(p)},{display:s(i),value:o(i)}]})}getSummary(e,t=0){const{orders_count:r=0,gross_sales:a=0,total_sales:s=0,refunds:o=0,coupons:n=0,taxes:c=0,shipping:l=0,net_revenue:i=0}=e,{formatAmount:m,getCurrencyConfig:u}=this.context,b=u();return[{label:Object(d._n)("day","days",t,'woocommerce'),value:Object(y.formatValue)(b,"number",t)},{label:Object(d._n)("order","orders",r,'woocommerce'),value:Object(y.formatValue)(b,"number",r)},{label:Object(d.__)("Gross sales",'woocommerce'),value:m(a)},{label:Object(d.__)("Returns",'woocommerce'),value:m(o)},{label:Object(d.__)("Coupons",'woocommerce'),value:m(n)},{label:Object(d.__)("Net sales",'woocommerce'),value:m(i)},{label:Object(d.__)("Taxes",'woocommerce'),value:m(c)},{label:Object(d.__)("Shipping",'woocommerce'),value:m(l)},{label:Object(d.__)("Total sales",'woocommerce'),value:m(s)}]}render(){const{advancedFilters:e,filters:t,tableData:r,query:s}=this.props;return Object(a.createElement)(f.a,{endpoint:"revenue",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:R,query:s,tableData:r,title:Object(d.__)("Revenue",'woocommerce'),columnPrefsKey:"revenue_report_columns",filters:t,advancedFilters:e})}}S.contextType=w.a;const T=Object(p.memoize)((e,t,r,a)=>({tableData:{items:{data:Object(p.get)(a,["data","intervals"],h),totalResults:Object(p.get)(a,["totalResults"],0)},isError:e,isRequesting:t,query:r}}),(e,t,r,a)=>[e,t,Object(g.stringify)(r),Object(p.get)(a,["totalResults"],0),Object(p.get)(a,["data","intervals"],h).length].join(":")),q=Object(p.memoize)((e,t,r,a,s)=>({interval:"day",orderby:t,order:e,page:r,per_page:a,after:Object(j.appendTimestamp)(s.primary.after,"start"),before:Object(j.appendTimestamp)(s.primary.before,"end")}),(e,t,r,a,s)=>[e,t,r,a,s.primary.after,s.primary.before].join(":"));var C=Object(b.compose)(Object(u.withSelect)((e,t)=>{const{query:r,filters:a,advancedFilters:s}=t,{woocommerce_default_date_range:o}=e(O.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings"),n=Object(j.getCurrentDates)(r,o),{getReportStats:c,getReportStatsError:l,isResolving:i}=e(O.REPORTS_STORE_NAME),d=q(r.order||"desc",r.orderby||"date",r.paged||1,r.per_page||O.QUERY_DEFAULTS.pageSize,n),m=Object(O.getReportTableQuery)({endpoint:"revenue",query:r,select:e,tableQuery:d,filters:a,advancedFilters:s}),u=c("revenue",m),b=Boolean(l("revenue",m)),p=i("getReportStats",["revenue",m]);return T(b,p,d,u)}))(S),k=r(505);class F extends a.Component{render(){const{path:e,query:t}=this.props;return Object(a.createElement)(a.Fragment,null,Object(a.createElement)(k.a,{query:t,path:e,report:"revenue",filters:n.c,advancedFilters:n.a}),Object(a.createElement)(i.a,{charts:n.b,endpoint:"revenue",query:t,selectedChart:Object(c.a)(t.chart,n.b),filters:n.c,advancedFilters:n.a}),Object(a.createElement)(l.a,{charts:n.b,endpoint:"revenue",path:e,query:t,selectedChart:Object(c.a)(t.chart,n.b),filters:n.c,advancedFilters:n.a}),Object(a.createElement)(C,{query:t,filters:n.c,advancedFilters:n.a}))}}F.propTypes={path:o.a.string.isRequired,query:o.a.object.isRequired}},528:function(e,t,r){"use strict";r.d(t,"b",(function(){return o})),r.d(t,"a",(function(){return n})),r.d(t,"c",(function(){return l}));var a=r(2),s=r(30);const o=Object(s.applyFilters)("woocommerce_admin_revenue_report_charts",[{key:"gross_sales",label:Object(a.__)("Gross sales",'woocommerce'),order:"desc",orderby:"gross_sales",type:"currency",isReverseTrend:!1},{key:"refunds",label:Object(a.__)("Returns",'woocommerce'),order:"desc",orderby:"refunds",type:"currency",isReverseTrend:!0},{key:"coupons",label:Object(a.__)("Coupons",'woocommerce'),order:"desc",orderby:"coupons",type:"currency",isReverseTrend:!1},{key:"net_revenue",label:Object(a.__)("Net sales",'woocommerce'),orderby:"net_revenue",type:"currency",isReverseTrend:!1},{key:"taxes",label:Object(a.__)("Taxes",'woocommerce'),order:"desc",orderby:"taxes",type:"currency",isReverseTrend:!1},{key:"shipping",label:Object(a.__)("Shipping",'woocommerce'),orderby:"shipping",type:"currency",isReverseTrend:!1},{key:"total_sales",label:Object(a.__)("Total sales",'woocommerce'),order:"desc",orderby:"total_sales",type:"currency",isReverseTrend:!1}]),n=Object(s.applyFilters)("woocommerce_admin_revenue_report_advanced_filters",{filters:{},title:Object(a._x)("Revenue Matches {{select /}} Filters","A sentence describing filters for Revenue. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')}),c=[];Object.keys(n.filters).length&&(c.push({label:Object(a.__)("All Revenue",'woocommerce'),value:"all"}),c.push({label:Object(a.__)("Advanced Filters",'woocommerce'),value:"advanced"}));const l=Object(s.applyFilters)("woocommerce_admin_revenue_report_filters",[{label:Object(a.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>c.length>0,filters:c}])}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-stock.js b/packages/woocommerce-admin/dist/chunks/analytics-report-stock.js new file mode 100644 index 0000000..a0ea92f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-stock.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[14],{478:function(e,t,r){"use strict";r.r(t),r.d(t,"default",(function(){return f}));var a=r(0),o=r(1),c=r.n(o),n=r(2),s=r(30);const i=Object(s.applyFilters)("woocommerce_admin_stock_report_advanced_filters",{filters:{},title:Object(n._x)("Products Match {{select /}} Filters","A sentence describing filters for Products. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')}),l=Object(s.applyFilters)("woocommerce_admin_stock_report_filters",[{label:Object(n.__)("Show",'woocommerce'),staticParams:["paged","per_page"],param:"type",showFilters:()=>!0,filters:[{label:Object(n.__)("All products",'woocommerce'),value:"all"},{label:Object(n.__)("Out of stock",'woocommerce'),value:"outofstock"},{label:Object(n.__)("Low stock",'woocommerce'),value:"lowstock"},{label:Object(n.__)("In stock",'woocommerce'),value:"instock"},{label:Object(n.__)("On backorder",'woocommerce'),value:"onbackorder"}]},{label:Object(n.__)("Filter by",'woocommerce'),staticParams:["paged","per_page"],param:"filter",showFilters:()=>Object.keys(i.filters).length,filters:[{label:Object(n.__)("All Products",'woocommerce'),value:"all"},{label:Object(n.__)("Advanced Filters",'woocommerce'),value:"advanced"}]}]);var d=r(28),m=r(21),u=r(12),b=r(120),p=r(13),_=r(506);var y=r(501);const g=Object(p.f)("stockStatuses",{});class h extends a.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(n.__)("Product / Variation",'woocommerce'),key:"title",required:!0,isLeftAligned:!0,isSortable:!0},{label:Object(n.__)("SKU",'woocommerce'),key:"sku",isSortable:!0},{label:Object(n.__)("Status",'woocommerce'),key:"stock_status",isSortable:!0,defaultSort:!0},{label:Object(n.__)("Stock",'woocommerce'),key:"stock_quantity",isSortable:!0}]}getRowsContent(e=[]){const{query:t}=this.props,r=Object(u.getPersistedQuery)(t);return e.map(e=>{const{id:t,manage_stock:o,parent_id:c,sku:s,stock_quantity:i,stock_status:l,low_stock_amount:_}=e,y=Object(d.decodeEntities)(e.name),h=Object(u.getNewPath)(r,"/analytics/products",{filter:"single_product",products:c||t}),O=Object(a.createElement)(m.Link,{href:h,type:"wc-admin"},y),j=Object(p.e)("post.php?action=edit&post="+(c||t));var f,w,v;return[{display:O,value:y},{display:s,value:s},{display:(f=l,v=_,(w=i)&&f&&w<=v==="instock"?Object(a.createElement)(m.Link,{href:j,type:"wp-admin"},Object(n._x)("Low","Indication of a low quantity",'woocommerce')):Object(a.createElement)(m.Link,{href:j,type:"wp-admin"},g[l])),value:g[l]},{display:o?Object(b.formatValue)(this.context.getCurrencyConfig(),"number",i):Object(n.__)("N/A",'woocommerce'),value:i}]})}getSummary(e){const{products:t=0,outofstock:r=0,lowstock:a=0,instock:o=0,onbackorder:c=0}=e,s=this.context.getCurrencyConfig();return[{label:Object(n._n)("Product","Products",t,'woocommerce'),value:Object(b.formatValue)(s,"number",t)},{label:Object(n.__)("Out of stock",'woocommerce'),value:Object(b.formatValue)(s,"number",r)},{label:Object(n.__)("Low stock",'woocommerce'),value:Object(b.formatValue)(s,"number",a)},{label:Object(n.__)("On backorder",'woocommerce'),value:Object(b.formatValue)(s,"number",c)},{label:Object(n.__)("In stock",'woocommerce'),value:Object(b.formatValue)(s,"number",o)}]}render(){const{advancedFilters:e,filters:t,query:r}=this.props;return Object(a.createElement)(_.a,{endpoint:"stock",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["products","outofstock","lowstock","instock","onbackorder"],query:r,tableQuery:{orderby:r.orderby||"stock_status",order:r.order||"asc",type:r.type||"all"},title:Object(n.__)("Stock",'woocommerce'),filters:t,advancedFilters:e})}}h.contextType=y.a;var O=h,j=r(505);class f extends a.Component{render(){const{query:e,path:t}=this.props;return Object(a.createElement)(a.Fragment,null,Object(a.createElement)(j.a,{query:e,path:t,showDatePicker:!1,filters:l,advancedFilters:i,report:"stock"}),Object(a.createElement)(O,{query:e,filters:l,advancedFilters:i}))}}f.propTypes={query:c.a.object.isRequired}},501:function(e,t,r){"use strict";r.d(t,"b",(function(){return l})),r.d(t,"a",(function(){return d}));var a=r(0),o=r(30),c=r(89),n=r.n(c),s=r(13);const i=n()(s.a),l=e=>{const t=i.getCurrencyConfig(),r=Object(o.applyFilters)("woocommerce_admin_report_currency",t,e);return n()(r)},d=Object(a.createContext)(i)},504:function(e,t,r){"use strict";var a=r(0),o=r(2),c=r(1),n=r.n(c),s=r(21);function i({className:e}){const t=Object(o.__)("There was an error getting your stats. Please try again.",'woocommerce'),r=Object(o.__)("Reload",'woocommerce');return Object(a.createElement)(s.EmptyContent,{className:e,title:t,actionLabel:r,actionCallback:()=>{window.location.reload()}})}i.propTypes={className:n.a.string},t.a=i},505:function(e,t,r){"use strict";var a=r(0),o=r(14),c=r(1),n=r.n(c),s=r(4),i=r(7),l=r(21),d=r(13),m=r(11),u=r(19),b=r(16),p=r(501),_=r(55);class y extends a.Component{constructor(){super(),this.onDateSelect=this.onDateSelect.bind(this),this.onFilterSelect=this.onFilterSelect.bind(this),this.onAdvancedFilterAction=this.onAdvancedFilterAction.bind(this)}onDateSelect(e){const{report:t,addCesSurveyForAnalytics:r}=this.props;r(),Object(b.recordEvent)("datepicker_update",{report:t,...Object(s.omitBy)(e,s.isUndefined)})}onFilterSelect(e){const{report:t,addCesSurveyForAnalytics:r}=this.props,a=e.filter||e["filter-variations"];["single_product","single_category","single_coupon","single_variation"].includes(a)&&r();const o={report:t,filter:e.filter||"all"};"single_product"===e.filter&&(o.filter_variation=e["filter-variations"]||"all"),Object(b.recordEvent)("analytics_filter",o)}onAdvancedFilterAction(e,t){const{report:r,addCesSurveyForAnalytics:a}=this.props;switch(e){case"add":Object(b.recordEvent)("analytics_filters_add",{report:r,filter:t.key});break;case"remove":Object(b.recordEvent)("analytics_filters_remove",{report:r,filter:t.key});break;case"filter":const e=Object.keys(t).reduce((e,r)=>(e[Object(s.snakeCase)(r)]=t[r],e),{});a(),Object(b.recordEvent)("analytics_filters_filter",{report:r,...e});break;case"clear_all":Object(b.recordEvent)("analytics_filters_clear_all",{report:r});break;case"match":Object(b.recordEvent)("analytics_filters_all_any",{report:r,value:t.match})}}render(){const{advancedFilters:e,filters:t,path:r,query:o,showDatePicker:c,defaultDateRange:n}=this.props,{period:s,compare:i,before:m,after:b}=Object(u.getDateParamsFromQuery)(o,n),{primary:p,secondary:_}=Object(u.getCurrentDates)(o,n),y={period:s,compare:i,before:m,after:b,primaryDate:p,secondaryDate:_},g=this.context;return Object(a.createElement)(l.ReportFilters,{query:o,siteLocale:d.b.siteLocale,currency:g.getCurrencyConfig(),path:r,filters:t,advancedFilters:e,showDatePicker:c,onDateSelect:this.onDateSelect,onFilterSelect:this.onFilterSelect,onAdvancedFilterAction:this.onAdvancedFilterAction,dateQuery:y,isoDateFormat:u.isoDateFormat})}}y.contextType=p.a,t.a=Object(o.compose)(Object(i.withSelect)(e=>{const{woocommerce_default_date_range:t}=e(m.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");return{defaultDateRange:t}}),Object(i.withDispatch)(e=>{const{addCesSurveyForAnalytics:t}=e(_.c);return{addCesSurveyForAnalytics:t}}))(y),y.propTypes={advancedFilters:n.a.object,filters:n.a.array,path:n.a.string.isRequired,query:n.a.object,showDatePicker:n.a.bool,report:n.a.string.isRequired}},506:function(e,t,r){"use strict";var a=r(35),o=r.n(a),c=r(0),n=r(3),s=r(30),i=r(14),l=r(91),d=r(7),m=r(4),u=r(2),b=r(1),p=r.n(b),_=r(21),y=r(12),g=r(474),h=r(11),O=r(16),j=()=>Object(c.createElement)("svg",{role:"img","aria-hidden":"true",focusable:"false",version:"1.1",xmlns:"http://www.w3.org/2000/svg",x:"0px",y:"0px",viewBox:"0 0 24 24"},Object(c.createElement)("path",{d:"M18,9c-0.009,0-0.017,0.002-0.025,0.003C17.72,5.646,14.922,3,11.5,3C7.91,3,5,5.91,5,9.5c0,0.524,0.069,1.031,0.186,1.519 C5.123,11.016,5.064,11,5,11c-2.209,0-4,1.791-4,4c0,1.202,0.541,2.267,1.38,3h18.593C22.196,17.089,23,15.643,23,14 C23,11.239,20.761,9,18,9z M12,16l-4-5h3V8h2v3h3L12,16z"})),f=r(504);var w=r(55);r(515);const v=e=>{const{getHeadersContent:t,getRowsContent:r,getSummary:a,isRequesting:i,primaryData:d,tableData:b,endpoint:p,itemIdField:w,tableQuery:v,compareBy:S,compareParam:k,searchBy:C,labels:E={},...F}=e,{query:R,columnPrefsKey:q}=e,{items:x,query:D}=b,A=R[k]?Object(y.getIdsFromQuery)(R[S]):[],[P,T]=Object(c.useState)(A),N=Object(c.useRef)(null),{updateUserPreferences:Q,...I}=Object(h.useUserPreferences)();if(b.isError||d.isError)return Object(c.createElement)(f.a,null);let B=[];q&&(B=I&&I[q]?I[q]:B);const V=(e,o,c)=>{const n=a?a(o,c):null;return Object(s.applyFilters)("woocommerce_admin_report_table",{endpoint:p,headers:t(),rows:r(e),totals:o,summary:n,items:x})},L=t=>{const{ids:r}=e;T(t?r:[])},M=(t,r)=>{const{ids:a}=e;if(r)T(Object(m.uniq)([a[t],...P]));else{const e=P.indexOf(a[t]);T([...P.slice(0,e),...P.slice(e+1)])}},H=t=>{const{ids:r=[]}=e,a=-1!==P.indexOf(r[t]);return{display:Object(c.createElement)(n.CheckboxControl,{onChange:Object(m.partial)(M,t),checked:a}),value:!1}},U=()=>{const{ids:t=[]}=e,r=t.length>0,a=r&&t.length===P.length;return{cellClassName:"is-checkbox-column",key:"compare",label:Object(c.createElement)(n.CheckboxControl,{onChange:L,"aria-label":Object(u.__)("Select All"),checked:a,disabled:!r}),required:!0}},z=i||b.isRequesting||d.isRequesting,J=Object(m.get)(d,["data","totals"],{}),K=x.totalResults||0,Y=K>0,G=Object(y.getSearchWords)(R).map(e=>({key:e,label:e})),{data:W}=x,X=V(W,J,K);let{headers:Z,rows:$}=X;const{summary:ee}=X;S&&($=$.map((e,t)=>[H(t),...e]),Z=[U(),...Z]);const te=((e,t)=>t?e.map(e=>({...e,visible:e.required||!t.includes(e.key)})):e.map(e=>({...e,visible:e.required||!e.hiddenByDefault})))(Z,B);return Object(c.createElement)(c.Fragment,null,Object(c.createElement)("div",{className:"woocommerce-report-table__scroll-point",ref:N,"aria-hidden":!0}),Object(c.createElement)(_.TableCard,o()({className:"woocommerce-report-table",hasSearch:!!C,actions:[S&&Object(c.createElement)(_.CompareButton,{key:"compare",className:"woocommerce-table__compare",count:P.length,helpText:E.helpText||Object(u.__)("Check at least two items below to compare",'woocommerce'),onClick:()=>{S&&Object(y.onQueryChange)("compare")(S,k,P.join(","))},disabled:!Y},E.compareButton||Object(u.__)("Compare",'woocommerce')),C&&Object(c.createElement)(_.Search,{allowFreeTextSearch:!0,inlineTags:!0,key:"search",onChange:t=>{const{baseSearchQuery:r,addCesSurveyForCustomerSearch:a}=e,o=t.map(e=>e.label.replace(",","%2C"));o.length?(Object(y.updateQueryString)({filter:void 0,[k]:void 0,[C]:void 0,...r,search:Object(m.uniq)(o).join(",")}),a()):Object(y.updateQueryString)({search:void 0}),Object(O.recordEvent)("analytics_table_filter",{report:p})},placeholder:E.placeholder||Object(u.__)("Search by item name",'woocommerce'),selected:G,showClearButton:!0,type:C,disabled:!Y}),Y&&Object(c.createElement)(n.Button,{key:"download",className:"woocommerce-table__download-button",disabled:z,onClick:()=>{const{createNotice:t,startExport:r,title:a}=e,o=Object.assign({},R),{data:c,totalResults:n}=x;let s="browser";if(delete o.extended_info,o.search&&delete o[C],c&&c.length===n){const{headers:e,rows:t}=V(c,n);Object(g.downloadCSVFile)(Object(g.generateCSVFileName)(a,o),Object(g.generateCSVDataFromTable)(e,t))}else s="email",r(p,D).then(()=>t("success",Object(u.sprintf)(Object(u.__)("Your %s Report will be emailed to you.",'woocommerce'),a))).catch(e=>t("error",e.message||Object(u.sprintf)(Object(u.__)("There was a problem exporting your %s Report. Please try again.",'woocommerce'),a)));Object(O.recordEvent)("analytics_table_download",{report:p,rows:n,download_type:s})}},Object(c.createElement)(j,null),Object(c.createElement)("span",{className:"woocommerce-table__download-button__label"},E.downloadButton||Object(u.__)("Download",'woocommerce')))],headers:te,isLoading:z,onQueryChange:y.onQueryChange,onColumnsChange:(e,t)=>{const r=Z.map(e=>e.key).filter(t=>!e.includes(t));if(q){Q({[q]:r})}if(t){const r={report:p,column:t,status:e.includes(t)?"on":"off"};Object(O.recordEvent)("analytics_table_header_toggle",r)}},onSort:(e,t)=>{Object(y.onQueryChange)("sort")(e,t);const r={report:p,column:e,direction:t};Object(O.recordEvent)("analytics_table_sort",r)},onPageChange:(e,t)=>{N.current.scrollIntoView();const r=N.current.nextSibling.querySelector(".woocommerce-table__table"),a=l.focus.focusable.find(r);a.length&&a[0].focus(),t&&("goto"===t?Object(O.recordEvent)("analytics_table_go_to_page",{report:p,page:e}):Object(O.recordEvent)("analytics_table_page_click",{report:p,direction:t}))},rows:$,rowsPerPage:parseInt(D.per_page,10)||h.QUERY_DEFAULTS.pageSize,summary:ee,totalRows:K},F)))};v.propTypes={baseSearchQuery:p.a.object,compareBy:p.a.string,compareParam:p.a.string,columnPrefsKey:p.a.string,endpoint:p.a.string,extendItemsMethodNames:p.a.shape({getError:p.a.string,isRequesting:p.a.string,load:p.a.string}),extendedItemsStoreName:p.a.string,getHeadersContent:p.a.func.isRequired,getRowsContent:p.a.func.isRequired,getSummary:p.a.func,itemIdField:p.a.string,labels:p.a.shape({compareButton:p.a.string,downloadButton:p.a.string,helpText:p.a.string,placeholder:p.a.string}),primaryData:p.a.object,searchBy:p.a.string,summaryFields:p.a.arrayOf(p.a.string),tableData:p.a.object.isRequired,tableQuery:p.a.object,title:p.a.string.isRequired},v.defaultProps={primaryData:{},tableData:{items:{data:[],totalResults:0},query:{}},tableQuery:{},compareParam:"filter",downloadable:!1,onSearch:m.noop,baseSearchQuery:{}};const S=[],k={};t.a=Object(i.compose)(Object(d.withSelect)((e,t)=>{const{endpoint:r,getSummary:a,isRequesting:o,itemIdField:c,query:n,tableData:s,tableQuery:i,filters:l,advancedFilters:d,summaryFields:u,extendedItemsStoreName:b}=t,p=e(h.REPORTS_STORE_NAME),_=b?e(b):null,{woocommerce_default_date_range:y}=e(h.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");if(o)return k;const g="categories"===r?"products":r,O=a?Object(h.getReportChartData)({endpoint:g,selector:p,dataType:"primary",query:n,filters:l,advancedFilters:d,defaultDateRange:y,fields:u}):k,j=s||Object(h.getReportTableData)({endpoint:r,query:n,selector:p,tableQuery:i,filters:l,advancedFilters:d,defaultDateRange:y}),f=_?function(e,t,r){const{extendItemsMethodNames:a,itemIdField:o}=t,c=r.items.data;if(!(Array.isArray(c)&&c.length&&a&&o))return r;const{[a.getError]:n,[a.isRequesting]:s,[a.load]:i}=e,l={include:c.map(e=>e[o]).join(","),per_page:c.length},d=i(l),u=!!s&&s(l),b=!!n&&n(l),p=c.map(e=>{const t=Object(m.first)(d.filter(t=>e.id===t.id));return{...e,...t}}),_=r.isRequesting||u,y=r.isError||b;return{...r,isRequesting:_,isError:y,items:{...r.items,data:p}}}(_,t,j):j;return{primaryData:O,ids:c&&f.items.data?f.items.data.map(e=>e[c]):S,tableData:f,query:n}}),Object(d.withDispatch)(e=>{const{startExport:t}=e(h.EXPORT_STORE_NAME),{createNotice:r}=e("core/notices"),{addCesSurveyForCustomerSearch:a}=e(w.c);return{createNotice:r,startExport:t,addCesSurveyForCustomerSearch:a}}))(v)},515:function(e,t,r){}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-taxes.js b/packages/woocommerce-admin/dist/chunks/analytics-report-taxes.js new file mode 100644 index 0000000..f4671dd --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-taxes.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[15],{485:function(e,t,r){"use strict";r.r(t);var a=r(0),o=r(1),c=r.n(o),n=r(2),i=r(535),s=r(510),d=r(508),l=r(511),m=r(4),u=r(21),b=r(12),_=r(120),p=r(503),y=r(506),x=r(501);class h extends a.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(n.__)("Tax code",'woocommerce'),key:"tax_code",required:!0,isLeftAligned:!0,isSortable:!0},{label:Object(n.__)("Rate",'woocommerce'),key:"rate",isSortable:!0,isNumeric:!0},{label:Object(n.__)("Total tax",'woocommerce'),key:"total_tax",isSortable:!0},{label:Object(n.__)("Order tax",'woocommerce'),key:"order_tax",isSortable:!0},{label:Object(n.__)("Shipping tax",'woocommerce'),key:"shipping_tax",isSortable:!0},{label:Object(n.__)("Orders",'woocommerce'),key:"orders_count",required:!0,defaultSort:!0,isSortable:!0,isNumeric:!0}]}getRowsContent(e){const{render:t,formatDecimal:r,getCurrencyConfig:o}=this.context;return Object(m.map)(e,e=>{const{query:c}=this.props,{order_tax:n,orders_count:i,tax_rate:s,tax_rate_id:d,total_tax:l,shipping_tax:m}=e,y=Object(p.a)(e),x=Object(b.getPersistedQuery)(c),h=Object(b.getNewPath)(x,"/analytics/orders",{filter:"advanced",tax_rate_includes:d});return[{display:Object(a.createElement)(u.Link,{href:h,type:"wc-admin"},y),value:y},{display:s.toFixed(2)+"%",value:s},{display:t(l),value:r(l)},{display:t(n),value:r(n)},{display:t(m),value:r(m)},{display:Object(_.formatValue)(o(),"number",i),value:i}]})}getSummary(e){const{tax_codes:t=0,total_tax:r=0,order_tax:a=0,shipping_tax:o=0,orders_count:c=0}=e,{formatAmount:i,getCurrencyConfig:s}=this.context,d=s();return[{label:Object(n._n)("tax code","tax codes",t,'woocommerce'),value:Object(_.formatValue)(d,"number",t)},{label:Object(n.__)("total tax",'woocommerce'),value:i(r)},{label:Object(n.__)("order tax",'woocommerce'),value:i(a)},{label:Object(n.__)("shipping tax",'woocommerce'),value:i(o)},{label:Object(n._n)("order","orders",c,'woocommerce'),value:Object(_.formatValue)(d,"number",c)}]}render(){const{advancedFilters:e,filters:t,isRequesting:r,query:o}=this.props;return Object(a.createElement)(y.a,{compareBy:"taxes",endpoint:"taxes",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,getSummary:this.getSummary,summaryFields:["tax_codes","total_tax","order_tax","shipping_tax","orders_count"],isRequesting:r,itemIdField:"tax_rate_id",query:o,searchBy:"taxes",tableQuery:{orderby:o.orderby||"tax_rate_id"},title:Object(n.__)("Taxes",'woocommerce'),columnPrefsKey:"taxes_report_columns",filters:t,advancedFilters:e})}}h.contextType=x.a;var f=h,O=r(505);class j extends a.Component{getChartMeta(){const{query:e}=this.props,t="compare-taxes"===e.filter?"item-comparison":"time-comparison";return{itemsLabel:Object(n.__)("%d taxes",'woocommerce'),mode:t}}render(){const{isRequesting:e,query:t,path:r}=this.props,{mode:o,itemsLabel:c}=this.getChartMeta(),n={...t};return"item-comparison"===o&&(n.segmentby="tax_rate_id"),Object(a.createElement)(a.Fragment,null,Object(a.createElement)(O.a,{query:t,path:r,filters:i.c,advancedFilters:i.a,report:"taxes"}),Object(a.createElement)(l.a,{charts:i.b,endpoint:"taxes",isRequesting:e,query:n,selectedChart:Object(s.a)(t.chart,i.b),filters:i.c,advancedFilters:i.a}),Object(a.createElement)(d.a,{charts:i.b,filters:i.c,advancedFilters:i.a,mode:o,endpoint:"taxes",query:n,path:r,isRequesting:e,itemsLabel:c,selectedChart:Object(s.a)(t.chart,i.b)}),Object(a.createElement)(f,{isRequesting:e,query:t,filters:i.c,advancedFilters:i.a}))}}j.propTypes={query:c.a.object.isRequired};t.default=j},502:function(e,t,r){"use strict";r.d(t,"e",(function(){return m})),r.d(t,"a",(function(){return u})),r.d(t,"b",(function(){return b})),r.d(t,"c",(function(){return _})),r.d(t,"d",(function(){return p})),r.d(t,"f",(function(){return y})),r.d(t,"h",(function(){return x})),r.d(t,"g",(function(){return h}));var a=r(15),o=r(17),c=r.n(o),n=r(4),i=r(12),s=r(11),d=r(13),l=r(503);function m(e,t=n.identity){return function(r="",o){const n="function"==typeof e?e(o):e,s=Object(i.getIdsFromQuery)(r);if(s.length<1)return Promise.resolve([]);const d={include:s.join(","),per_page:s.length};return c()({path:Object(a.addQueryArgs)(n,d)}).then(e=>e.map(t))}}m(s.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const u=m(s.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),b=m(s.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),_=m(s.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),p=m(s.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),y=m(s.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(l.a)(e)}));function x({attributes:e,name:t}){const r=Object(d.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(r)>-1)return t;const a=(e||[]).map(({option:e})=>e).join(", ");return a?t+r+a:t}const h=m(({products:e})=>e?s.NAMESPACE+`/products/${e}/variations`:s.NAMESPACE+"/variations",e=>({key:e.id,label:x(e)}))},503:function(e,t,r){"use strict";r.d(t,"a",(function(){return o}));var a=r(2);function o(e){return[e.country,e.state,e.name||Object(a.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},535:function(e,t,r){"use strict";r.d(t,"b",(function(){return m})),r.d(t,"a",(function(){return u})),r.d(t,"c",(function(){return _}));var a=r(2),o=r(30),c=r(11),n=r(7),i=r(502),s=r(503),d=r(55);const{addCesSurveyForAnalytics:l}=Object(n.dispatch)(d.c),m=Object(o.applyFilters)("woocommerce_admin_taxes_report_charts",[{key:"total_tax",label:Object(a.__)("Total tax",'woocommerce'),order:"desc",orderby:"total_tax",type:"currency"},{key:"order_tax",label:Object(a.__)("Order tax",'woocommerce'),order:"desc",orderby:"order_tax",type:"currency"},{key:"shipping_tax",label:Object(a.__)("Shipping tax",'woocommerce'),order:"desc",orderby:"shipping_tax",type:"currency"},{key:"orders_count",label:Object(a.__)("Orders",'woocommerce'),order:"desc",orderby:"orders_count",type:"number"}]),u=Object(o.applyFilters)("woocommerce_admin_taxes_report_advanced_filters",{filters:{},title:Object(a._x)("Taxes match {{select /}} filters","A sentence describing filters for Taxes. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')}),b=[{label:Object(a.__)("All taxes",'woocommerce'),value:"all"},{label:Object(a.__)("Comparison",'woocommerce'),value:"compare-taxes",chartMode:"item-comparison",settings:{type:"taxes",param:"taxes",getLabels:Object(i.e)(c.NAMESPACE+"/taxes",e=>({id:e.id,key:e.id,label:Object(s.a)(e)})),labels:{helpText:Object(a.__)("Check at least two tax codes below to compare",'woocommerce'),placeholder:Object(a.__)("Search for tax codes to compare",'woocommerce'),title:Object(a.__)("Compare Tax Codes",'woocommerce'),update:Object(a.__)("Compare",'woocommerce')},onClick:l}}];Object.keys(u.filters).length&&b.push({label:Object(a.__)("Advanced filters",'woocommerce'),value:"advanced"});const _=Object(o.applyFilters)("woocommerce_admin_taxes_report_filters",[{label:Object(a.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:b}])}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report-variations.js b/packages/woocommerce-admin/dist/chunks/analytics-report-variations.js new file mode 100644 index 0000000..e6d41cd --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report-variations.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[16],{482:function(e,t,o){"use strict";o.r(t);var r=o(0),a=o(2),c=o(1),i=o.n(c),n=o(30),s=o(7),m=o(502),l=o(55);const{addCesSurveyForAnalytics:d}=Object(s.dispatch)(l.c),u=Object(n.applyFilters)("woocommerce_admin_variations_report_charts",[{key:"items_sold",label:Object(a.__)("Items sold",'woocommerce'),order:"desc",orderby:"items_sold",type:"number"},{key:"net_revenue",label:Object(a.__)("Net sales",'woocommerce'),order:"desc",orderby:"net_revenue",type:"currency"},{key:"orders_count",label:Object(a.__)("Orders",'woocommerce'),order:"desc",orderby:"orders_count",type:"number"}]),b=Object(n.applyFilters)("woocommerce_admin_variations_report_filters",[{label:Object(a.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter-variations",showFilters:()=>!0,filters:[{label:Object(a.__)("All variations",'woocommerce'),chartMode:"item-comparison",value:"all"},{label:Object(a.__)("Single variation",'woocommerce'),value:"select_variation",subFilters:[{component:"Search",value:"single_variation",path:["select_variation"],settings:{type:"variations",param:"variations",getLabels:m.g,labels:{placeholder:Object(a.__)("Type to search for a variation",'woocommerce'),button:Object(a.__)("Single variation",'woocommerce')}}}]},{label:Object(a.__)("Comparison",'woocommerce'),chartMode:"item-comparison",value:"compare-variations",settings:{type:"variations",param:"variations",getLabels:m.g,labels:{helpText:Object(a.__)("Check at least two variations below to compare",'woocommerce'),placeholder:Object(a.__)("Search for variations to compare",'woocommerce'),title:Object(a.__)("Compare Variations",'woocommerce'),update:Object(a.__)("Compare",'woocommerce')},onClick:d}},{label:Object(a.__)("Advanced filters",'woocommerce'),value:"advanced"}]}]),_=Object(n.applyFilters)("woocommerce_admin_variations_report_advanced_filters",{title:Object(a._x)("Variations match {{select /}} filters","A sentence describing filters for Variations. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce'),filters:{attribute:{allowMultiple:!0,labels:{add:Object(a.__)("Attribute",'woocommerce'),placeholder:Object(a.__)("Search attributes",'woocommerce'),remove:Object(a.__)("Remove attribute filter",'woocommerce'),rule:Object(a.__)("Select a product attribute filter match",'woocommerce'),title:Object(a.__)("{{title}}Attribute{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(a.__)("Select attributes",'woocommerce')},rules:[{value:"is",label:Object(a._x)("Is","product attribute",'woocommerce')},{value:"is_not",label:Object(a._x)("Is Not","product attribute",'woocommerce')}],input:{component:"ProductAttribute"}},category:{labels:{add:Object(a.__)("Categories",'woocommerce'),placeholder:Object(a.__)("Search categories",'woocommerce'),remove:Object(a.__)("Remove categories filter",'woocommerce'),rule:Object(a.__)("Select a category filter match",'woocommerce'),title:Object(a.__)("{{title}}Category{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(a.__)("Select categories",'woocommerce')},rules:[{value:"includes",label:Object(a._x)("Includes","categories",'woocommerce')},{value:"excludes",label:Object(a._x)("Excludes","categories",'woocommerce')}],input:{component:"Search",type:"categories",getLabels:m.a}},product:{labels:{add:Object(a.__)("Products",'woocommerce'),placeholder:Object(a.__)("Search products",'woocommerce'),remove:Object(a.__)("Remove products filter",'woocommerce'),rule:Object(a.__)("Select a product filter match",'woocommerce'),title:Object(a.__)("{{title}}Product{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(a.__)("Select products",'woocommerce')},rules:[{value:"includes",label:Object(a._x)("Includes","products",'woocommerce')},{value:"excludes",label:Object(a._x)("Excludes","products",'woocommerce')}],input:{component:"Search",type:"variableProducts",getLabels:m.d}}}});var p=o(510),v=o(508),j=o(504),O=o(511),y=o(532),f=o(505);const w=e=>{const{itemsLabel:t,mode:o}=(({query:e})=>{const t="compare-variations"===e["filter-variations"]&&e.variations&&e.variations.split(",").length>1;return{compareObject:"variations",itemsLabel:Object(a.__)("%d variations",'woocommerce'),mode:t?"item-comparison":"time-comparison"}})(e),{path:c,query:i,isError:n,isRequesting:s}=e;if(n)return Object(r.createElement)(j.a,null);const m={...i};return"item-comparison"===o&&(m.segmentby="variation"),Object(r.createElement)(r.Fragment,null,Object(r.createElement)(f.a,{query:i,path:c,filters:b,advancedFilters:_,report:"variations"}),Object(r.createElement)(O.a,{mode:o,charts:u,endpoint:"variations",isRequesting:s,query:m,selectedChart:Object(p.a)(i.chart,u),filters:b,advancedFilters:_}),Object(r.createElement)(v.a,{charts:u,mode:o,filters:b,advancedFilters:_,endpoint:"variations",isRequesting:s,itemsLabel:t,path:c,query:m,selectedChart:Object(p.a)(m.chart,u)}),Object(r.createElement)(y.a,{isRequesting:s,query:i,filters:b,advancedFilters:_}))};w.propTypes={path:i.a.string.isRequired,query:i.a.object.isRequired};t.default=w},502:function(e,t,o){"use strict";o.d(t,"e",(function(){return d})),o.d(t,"a",(function(){return u})),o.d(t,"b",(function(){return b})),o.d(t,"c",(function(){return _})),o.d(t,"d",(function(){return p})),o.d(t,"f",(function(){return v})),o.d(t,"h",(function(){return j})),o.d(t,"g",(function(){return O}));var r=o(15),a=o(17),c=o.n(a),i=o(4),n=o(12),s=o(11),m=o(13),l=o(503);function d(e,t=i.identity){return function(o="",a){const i="function"==typeof e?e(a):e,s=Object(n.getIdsFromQuery)(o);if(s.length<1)return Promise.resolve([]);const m={include:s.join(","),per_page:s.length};return c()({path:Object(r.addQueryArgs)(i,m)}).then(e=>e.map(t))}}d(s.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const u=d(s.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),b=d(s.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),_=d(s.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),p=d(s.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),v=d(s.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(l.a)(e)}));function j({attributes:e,name:t}){const o=Object(m.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(o)>-1)return t;const r=(e||[]).map(({option:e})=>e).join(", ");return r?t+o+r:t}const O=d(({products:e})=>e?s.NAMESPACE+`/products/${e}/variations`:s.NAMESPACE+"/variations",e=>({key:e.id,label:j(e)}))},503:function(e,t,o){"use strict";o.d(t,"a",(function(){return a}));var r=o(2);function a(e){return[e.country,e.state,e.name||Object(r.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},513:function(e,t,o){"use strict";function r(e,t,o){return!!t&&(e&&t<=o==="instock")}o.d(t,"a",(function(){return r}))},532:function(e,t,o){"use strict";var r=o(0),a=o(2),c=o(4),i=o(21),n=o(12),s=o(120),m=o(13),l=o(506),d=o(513),u=o(501),b=o(502);const _=Object(m.f)("manageStock","no"),p=Object(m.f)("stockStatuses",{});class v extends r.Component{constructor(){super(),this.getHeadersContent=this.getHeadersContent.bind(this),this.getRowsContent=this.getRowsContent.bind(this),this.getSummary=this.getSummary.bind(this)}getHeadersContent(){return[{label:Object(a.__)("Product / Variation title",'woocommerce'),key:"name",required:!0,isLeftAligned:!0},{label:Object(a.__)("SKU",'woocommerce'),key:"sku",hiddenByDefault:!0,isSortable:!0},{label:Object(a.__)("Items sold",'woocommerce'),key:"items_sold",required:!0,defaultSort:!0,isSortable:!0,isNumeric:!0},{label:Object(a.__)("Net sales",'woocommerce'),screenReaderLabel:Object(a.__)("Net sales",'woocommerce'),key:"net_revenue",required:!0,isSortable:!0,isNumeric:!0},{label:Object(a.__)("Orders",'woocommerce'),key:"orders_count",isSortable:!0,isNumeric:!0},"yes"===_?{label:Object(a.__)("Status",'woocommerce'),key:"stock_status"}:null,"yes"===_?{label:Object(a.__)("Stock",'woocommerce'),key:"stock",isNumeric:!0}:null].filter(Boolean)}getRowsContent(e=[]){const{query:t}=this.props,o=Object(n.getPersistedQuery)(t),{formatAmount:l,formatDecimal:u,getCurrencyConfig:v}=this.context;return Object(c.map)(e,e=>{const{items_sold:t,net_revenue:c,orders_count:j,product_id:O,variation_id:y}=e,f=e.extended_info||{},{stock_status:w,stock_quantity:h,low_stock_amount:g,sku:S}=f,k=(C=e,Object(b.h)(C.extended_info||{}));var C;const A=Object(n.getNewPath)(o,"/analytics/orders",{filter:"advanced",variation_includes:y}),E=Object(m.e)(`post.php?post=${O}&action=edit`);return[{display:Object(r.createElement)(i.Link,{href:E,type:"wp-admin"},k),value:k},{display:S,value:S},{display:Object(s.formatValue)(v(),"number",t),value:t},{display:l(c),value:u(c)},{display:Object(r.createElement)(i.Link,{href:A,type:"wc-admin"},j),value:j},"yes"===_?{display:Object(d.a)(w,h,g)?Object(r.createElement)(i.Link,{href:E,type:"wp-admin"},Object(a._x)("Low","Indication of a low quantity",'woocommerce')):p[w],value:p[w]}:null,"yes"===_?{display:h,value:h}:null].filter(Boolean)})}getSummary(e){const{variations_count:t=0,items_sold:o=0,net_revenue:r=0,orders_count:c=0}=e,{formatAmount:i,getCurrencyConfig:n}=this.context,m=n();return[{label:Object(a._n)("variation sold","variations sold",t,'woocommerce'),value:Object(s.formatValue)(m,"number",t)},{label:Object(a._n)("item sold","items sold",o,'woocommerce'),value:Object(s.formatValue)(m,"number",o)},{label:Object(a.__)("net sales",'woocommerce'),value:i(r)},{label:Object(a._n)("orders","orders",c,'woocommerce'),value:Object(s.formatValue)(m,"number",c)}]}render(){const{advancedFilters:e,baseSearchQuery:t,filters:o,isRequesting:c,query:i}=this.props,n={helpText:Object(a.__)("Check at least two variations below to compare",'woocommerce'),placeholder:Object(a.__)("Search by variation name or SKU",'woocommerce')};return Object(r.createElement)(l.a,{baseSearchQuery:t,compareBy:"variations",compareParam:"filter-variations",endpoint:"variations",getHeadersContent:this.getHeadersContent,getRowsContent:this.getRowsContent,isRequesting:c,itemIdField:"variation_id",labels:n,query:i,getSummary:this.getSummary,summaryFields:["variations_count","items_sold","net_revenue","orders_count"],tableQuery:{orderby:i.orderby||"items_sold",order:i.order||"desc",extended_info:!0,product_includes:i.products,variations:i.variations},title:Object(a.__)("Variations",'woocommerce'),columnPrefsKey:"variations_report_columns",filters:o,advancedFilters:e})}}v.contextType=u.a,t.a=v}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-report.js b/packages/woocommerce-admin/dist/chunks/analytics-report.js new file mode 100644 index 0000000..3084a7a --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-report.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[6],{501:function(e,t,r){"use strict";r.d(t,"b",(function(){return u})),r.d(t,"a",(function(){return p}));var n=r(0),c=r(30),s=r(89),o=r.n(s),a=r(13);const i=o()(a.a),u=e=>{const t=i.getCurrencyConfig(),r=Object(c.applyFilters)("woocommerce_admin_report_currency",t,e);return o()(r)},p=Object(n.createContext)(i)},504:function(e,t,r){"use strict";var n=r(0),c=r(2),s=r(1),o=r.n(s),a=r(21);function i({className:e}){const t=Object(c.__)("There was an error getting your stats. Please try again.",'woocommerce'),r=Object(c.__)("Reload",'woocommerce');return Object(n.createElement)(a.EmptyContent,{className:e,title:t,actionLabel:r,actionCallback:()=>{window.location.reload()}})}i.propTypes={className:o.a.string},t.a=i},545:function(e,t,r){},605:function(e,t,r){"use strict";r.r(t);var n=r(0),c=r(14),s=r(7),o=r(1),a=r.n(o),i=r(4),u=r(12),p=r(11),l=(r(545),r(504)),m=r(501),b=r(117);const d=({params:e,path:t})=>e.report||t.replace(/^\/+/,"");class j extends n.Component{constructor(){super(...arguments),this.state={hasError:!1}}componentDidCatch(e){this.setState({hasError:!0}),console.warn(e)}render(){if(this.state.hasError)return null;const{isError:e}=this.props;if(e)return Object(n.createElement)(l.a,null);const t=d(this.props),r=Object(i.find)(Object(b.a)(),{report:t});if(!r)return null;const c=r.component;return Object(n.createElement)(m.a.Provider,{value:Object(m.b)(Object(u.getQuery)())},Object(n.createElement)(c,this.props))}}j.propTypes={params:a.a.object.isRequired},t.default=Object(c.compose)(Object(s.withSelect)((e,t)=>{const r=Object(u.getQuery)(),{search:n}=r,c=e(p.ITEMS_STORE_NAME);if(!n)return{};const s=d(t),o=Object(u.getSearchWords)(r),a="categories"===s&&"single_category"===r.filter?"products":s,i=Object(p.searchItemsByString)(c,a,o,{per_page:100}),{isError:l,isRequesting:m,items:b}=i,j=Object.keys(b);return j.length?{isError:l,isRequesting:m,query:{...t.query,[a]:j.join(",")}}:{isError:l,isRequesting:m}}))(j)}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/analytics-settings.js b/packages/woocommerce-admin/dist/chunks/analytics-settings.js new file mode 100644 index 0000000..c9d851d --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/analytics-settings.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[17],{38:function(e,t){e.exports=function(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e},e.exports.default=e.exports,e.exports.__esModule=!0},546:function(e,t,a){},547:function(e,t,a){},548:function(e,t,a){},614:function(e,t,a){"use strict";a.r(t);var o=a(35),r=a.n(o),s=a(0),c=a(2),i=a(3),n=a(14),m=a(7),l=a(21),d=a(11),p=a(16),u=(a(546),a(255)),b=a(38),h=a.n(b),_=a(1),g=a.n(_),O=a(4);a(547);class j extends s.Component{constructor(e){super(e),h()(this,"renderInput",()=>{const{handleChange:e,name:t,inputText:a,inputType:o,options:c,value:n,component:m}=this.props,{disabled:l}=this.state;switch(o){case"checkboxGroup":return c.map(e=>e.options.length>0&&Object(s.createElement)("div",{className:"woocommerce-setting__options-group",key:e.key,"aria-labelledby":t+"-label"},e.label&&Object(s.createElement)("span",{className:"woocommerce-setting__options-group-label"},e.label),this.renderCheckboxOptions(e.options)));case"checkbox":return this.renderCheckboxOptions(c);case"button":return Object(s.createElement)(i.Button,{isSecondary:!0,onClick:this.handleInputCallback,disabled:l},a);case"component":const o=m;return Object(s.createElement)(o,r()({value:n,onChange:e},this.props));case"text":default:const d=Object(O.uniqueId)(t);return Object(s.createElement)("input",{id:d,type:"text",name:t,onChange:e,value:n,placeholder:a,disabled:l})}}),h()(this,"handleInputCallback",()=>{const{createNotice:e,callback:t}=this.props;if("function"==typeof t)return new Promise((a,o)=>{this.setState({disabled:!0}),t(a,o,e)}).then(()=>{this.setState({disabled:!1})}).catch(()=>{this.setState({disabled:!1})})}),this.state={disabled:!1}}renderCheckboxOptions(e){const{handleChange:t,name:a,value:o}=this.props,{disabled:r}=this.state;return e.map(e=>Object(s.createElement)(i.CheckboxControl,{key:a+"-"+e.value,label:e.label,name:a,checked:o&&o.includes(e.value),onChange:o=>t({target:{checked:o,name:a,type:"checkbox",value:e.value}}),disabled:r}))}render(){const{helpText:e,label:t,name:a}=this.props;return Object(s.createElement)("div",{className:"woocommerce-setting"},Object(s.createElement)("div",{className:"woocommerce-setting__label",id:a+"-label"},t),Object(s.createElement)("div",{className:"woocommerce-setting__input"},this.renderInput(),e&&Object(s.createElement)("span",{className:"woocommerce-setting__help"},e)))}}j.propTypes={callback:g.a.func,handleChange:g.a.func.isRequired,helpText:g.a.oneOfType([g.a.string,g.a.array]),inputText:g.a.string,inputType:g.a.oneOf(["button","checkbox","checkboxGroup","text","component"]),label:g.a.string.isRequired,name:g.a.string.isRequired,options:g.a.arrayOf(g.a.shape({value:g.a.string,label:g.a.string,description:g.a.string,key:g.a.string,options:g.a.array})),value:g.a.oneOfType([g.a.string,g.a.array])};var w=Object(n.compose)(Object(m.withDispatch)(e=>{const{createNotice:t}=e("core/notices");return{createNotice:t}}))(j),v=a(9),I=a.n(v);const S=(e,t,a)=>{const o={};if(a&&(o.skip_existing=!0),"all"!==t.label)if("custom"===t.label){const a=I()().diff(I()(t.date,e),"days",!0);o.days=Math.floor(a)}else o.days=parseInt(t.label,10);return o};var E=a(15);var k=Object(n.compose)([Object(m.withSelect)(e=>{const{getFormSettings:t}=e(d.IMPORT_STORE_NAME),{period:a,skipPrevious:o}=t();return{selectedPeriod:a,skipChecked:o}}),Object(m.withDispatch)(e=>{const{updateImportation:t,setImportStarted:a}=e(d.IMPORT_STORE_NAME),{createNotice:o}=e("core/notices");return{createNotice:o,setImportStarted:a,updateImportation:t}})])((function({clearStatusAndTotalsCache:e,createNotice:t,dateFormat:a,importDate:o,onImportStarted:r,selectedPeriod:n,stopImport:m,skipChecked:l,status:d,setImportStarted:u,updateImportation:b}){const h=()=>{const e=Object(E.addQueryArgs)("/wc-analytics/reports/import",S(a,n,l)),t=Object(c.__)("There was a problem rebuilding your report data.",'woocommerce');g(e,t,!0),r()},_=()=>{m();const e=Object(c.__)("There was a problem stopping your current import.",'woocommerce');g("/wc-analytics/reports/import/cancel",e)},g=(e,a,o=!1)=>{b(e,o).then(e=>{"success"===e.status?t("success",e.message):(t("error",a),u(!1),m())}).catch(e=>{e&&e.message&&(t("error",e.message),u(!1),m())})},O=()=>{const e=Object(c.__)("There was a problem deleting your previous data.",'woocommerce');g("/wc-analytics/reports/import/delete",e),Object(p.recordEvent)("analytics_import_delete_previous"),u(!1)},j=()=>{u(!1),e()};return Object(s.createElement)("div",{className:"woocommerce-settings__actions woocommerce-settings-historical-data__actions"},(()=>{const e="ready"!==d;return["initializing","customers","orders","finalizing"].includes(d)?Object(s.createElement)(s.Fragment,null,Object(s.createElement)(i.Button,{className:"woocommerce-settings-historical-data__action-button",isPrimary:!0,onClick:_},Object(c.__)("Stop Import",'woocommerce')),Object(s.createElement)("div",{className:"woocommerce-setting__help woocommerce-settings-historical-data__action-help"},Object(c.__)("Imported data will not be lost if the import is stopped.",'woocommerce'),Object(s.createElement)("br",null),Object(c.__)("Navigating away from this page will not affect the import.",'woocommerce'))):["ready","nothing"].includes(d)?o?Object(s.createElement)(s.Fragment,null,Object(s.createElement)(i.Button,{isPrimary:!0,onClick:h,disabled:e},Object(c.__)("Start",'woocommerce')),Object(s.createElement)(i.Button,{isSecondary:!0,onClick:O},Object(c.__)("Delete Previously Imported Data",'woocommerce'))):Object(s.createElement)(s.Fragment,null,Object(s.createElement)(i.Button,{isPrimary:!0,onClick:h,disabled:e},Object(c.__)("Start",'woocommerce'))):("error"===d&&t("error",Object(c.__)("Something went wrong with the importation process.",'woocommerce')),Object(s.createElement)(s.Fragment,null,Object(s.createElement)(i.Button,{isSecondary:!0,onClick:j},Object(c.__)("Re-import Data",'woocommerce')),Object(s.createElement)(i.Button,{isSecondary:!0,onClick:O},Object(c.__)("Delete Previously Imported Data",'woocommerce'))))})())})),y=a(19);var f=Object(m.withDispatch)(e=>{const{setImportPeriod:t}=e(d.IMPORT_STORE_NAME);return{setImportPeriod:t}})((function({dateFormat:e,disabled:t,setImportPeriod:a,value:o}){const r=t=>{t.date&&t.date.isValid?a(t.date.format(e),!0):a(t.text,!0)},n=t=>t.isValid()&&o.date.length===e.length?t.isAfter(new Date,"day")?y.dateValidationMessages.future:null:y.dateValidationMessages.invalid;return Object(s.createElement)("div",{className:"woocommerce-settings-historical-data__columns"},Object(s.createElement)("div",{className:"woocommerce-settings-historical-data__column"},Object(s.createElement)(i.SelectControl,{label:Object(c.__)("Import historical data",'woocommerce'),value:o.label,disabled:t,onChange:e=>{a(e)},options:[{label:"All",value:"all"},{label:"Last 365 days",value:"365"},{label:"Last 90 days",value:"90"},{label:"Last 30 days",value:"30"},{label:"Last 7 days",value:"7"},{label:"Last 24 hours",value:"1"},{label:"Custom",value:"custom"}]})),"custom"===o.label&&(()=>{const a=I()(o.date,e);return Object(s.createElement)("div",{className:"woocommerce-settings-historical-data__column"},Object(s.createElement)("div",{className:"woocommerce-settings-historical-data__column-label"},Object(c.__)("Beginning on",'woocommerce')),Object(s.createElement)(l.DatePicker,{date:a.isValid()?a.toDate():null,dateFormat:e,disabled:t,error:n(a),isInvalidDate:e=>I()(e).isAfter(new Date,"day"),onUpdate:r,text:o.date}))})())}));var C=function({label:e,progress:t,total:a}){const o=Object(c.sprintf)(Object(c.__)("Imported %(label)s",'woocommerce'),{label:e}),r=Object(O.isNil)(a)?null:Object(c.sprintf)(Object(c.__)("%(progress)s of %(total)s",'woocommerce'),{progress:t||0,total:a});return Object(s.createElement)("div",{className:"woocommerce-settings-historical-data__progress"},Object(s.createElement)("span",{className:"woocommerce-settings-historical-data__progress-label"},o),r&&Object(s.createElement)("span",{className:"woocommerce-settings-historical-data__progress-label"},r),Object(s.createElement)("progress",{className:"woocommerce-settings-historical-data__progress-bar",max:a,value:t||0}))},N=a(30);var T=function({importDate:e,status:t}){const a=Object(N.applyFilters)("woocommerce_admin_import_status",{nothing:Object(c.__)("Nothing To Import",'woocommerce'),ready:Object(c.__)("Ready To Import",'woocommerce'),initializing:[Object(c.__)("Initializing",'woocommerce'),Object(s.createElement)(i.Spinner,{key:"spinner"})],customers:[Object(c.__)("Importing Customers",'woocommerce'),Object(s.createElement)(i.Spinner,{key:"spinner"})],orders:[Object(c.__)("Importing Orders",'woocommerce'),Object(s.createElement)(i.Spinner,{key:"spinner"})],finalizing:[Object(c.__)("Finalizing",'woocommerce'),Object(s.createElement)(i.Spinner,{key:"spinner"})],finished:-1===e?Object(c.__)("All historical data imported",'woocommerce'):Object(c.sprintf)(Object(c.__)("Historical data from %s onward imported",'woocommerce'),I()(e).format("YYYY-MM-DD"))});return Object(s.createElement)("span",{className:"woocommerce-settings-historical-data__status"},Object(c.__)("Status:",'woocommerce')+" ",a[t])};var P=Object(m.withDispatch)(e=>{const{setSkipPrevious:t}=e(d.IMPORT_STORE_NAME);return{setSkipPrevious:t}})((function({checked:e,disabled:t,setSkipPrevious:a}){return Object(s.createElement)(i.CheckboxControl,{className:"woocommerce-settings-historical-data__skip-checkbox",checked:e,disabled:t,label:Object(c.__)("Skip previously imported customers and orders",'woocommerce'),onChange:e=>{a(e)}})}));a(548);class A extends s.Component{render(){const{customersProgress:e,customersTotal:t,dateFormat:a,importDate:o,inProgress:r,lastImportStartTimestamp:i,clearStatusAndTotalsCache:n,ordersProgress:m,ordersTotal:d,onImportStarted:p,period:u,stopImport:b,skipChecked:h,status:_}=this.props;return Object(s.createElement)(s.Fragment,null,Object(s.createElement)(l.SectionHeader,{title:Object(c.__)("Import historical data",'woocommerce')}),Object(s.createElement)("div",{className:"woocommerce-settings__wrapper"},Object(s.createElement)("div",{className:"woocommerce-setting"},Object(s.createElement)("div",{className:"woocommerce-setting__input"},Object(s.createElement)("span",{className:"woocommerce-setting__help"},Object(c.__)("This tool populates historical analytics data by processing customers and orders created prior to activating WooCommerce Admin.",'woocommerce')),"finished"!==_&&Object(s.createElement)(s.Fragment,null,Object(s.createElement)(f,{dateFormat:a,disabled:r,value:u}),Object(s.createElement)(P,{disabled:r,checked:h}),Object(s.createElement)(C,{label:Object(c.__)("Registered Customers",'woocommerce'),progress:e,total:t}),Object(s.createElement)(C,{label:Object(c.__)("Orders and Refunds",'woocommerce'),progress:m,total:d})),Object(s.createElement)(T,{importDate:o,status:_})))),Object(s.createElement)(k,{clearStatusAndTotalsCache:n,dateFormat:a,importDate:o,lastImportStartTimestamp:i,onImportStarted:p,stopImport:b,status:_}))}}var x=Object(m.withSelect)((e,t)=>{const{getImportError:a,getImportStatus:o,getImportTotals:r}=e(d.IMPORT_STORE_NAME),{activeImport:s,cacheNeedsClearing:c,dateFormat:i,inProgress:n,onImportStarted:m,onImportFinished:l,period:p,startStatusCheckInterval:u,skipChecked:b}=t,h=S(i,p,b),{customers:_,orders:g,lastImportStartTimestamp:j}=r(h),{customers:w,imported_from:v,is_importing:I,orders:E}=o(j),{imported:k,total:y}=w||{},{imported:f,total:C}=E||{},N=Boolean(a(j)||a(h));Boolean(!j&&!n&&!0===I)&&m();const T=Boolean(n&&!c&&!1===I&&(y>0||C>0)&&k===y&&f===C);let P={customersTotal:_,isError:N,ordersTotal:g};s&&(P={cacheNeedsClearing:c,customersProgress:k,customersTotal:Object(O.isNil)(y)?_:y,inProgress:n,isError:N,ordersProgress:f,ordersTotal:Object(O.isNil)(C)?g:C});const A=(({cacheNeedsClearing:e,customersProgress:t,customersTotal:a,isError:o,inProgress:r,ordersProgress:s,ordersTotal:c})=>o?"error":r?Object(O.isNil)(t)||Object(O.isNil)(s)||Object(O.isNil)(a)||Object(O.isNil)(c)||e?"initializing":t0||c>0?t===a&&s===c?"finished":"ready":"nothing")(P);return"initializing"===A&&u(),T&&l(),{...P,importDate:v,status:A}})(A);class D extends s.Component{constructor(){super(...arguments),this.dateFormat=Object(c.__)("MM/DD/YYYY",'woocommerce'),this.intervalId=-1,this.lastImportStopTimestamp=0,this.cacheNeedsClearing=!0,this.onImportFinished=this.onImportFinished.bind(this),this.onImportStarted=this.onImportStarted.bind(this),this.clearStatusAndTotalsCache=this.clearStatusAndTotalsCache.bind(this),this.stopImport=this.stopImport.bind(this),this.startStatusCheckInterval=this.startStatusCheckInterval.bind(this),this.cancelStatusCheckInterval=this.cancelStatusCheckInterval.bind(this)}startStatusCheckInterval(){this.intervalId<0&&(this.cacheNeedsClearing=!0,this.intervalId=setInterval(()=>{this.clearCache("getImportStatus")},3*d.SECOND))}cancelStatusCheckInterval(){clearInterval(this.intervalId),this.intervalId=-1}clearCache(e,t){const{invalidateResolution:a,lastImportStartTimestamp:o}=this.props;a(e,["getImportStatus"===e?o:t]).then(()=>{this.cacheNeedsClearing=!1})}stopImport(){this.cancelStatusCheckInterval(),this.lastImportStopTimestamp=Date.now()}onImportFinished(){const{debouncedSpeak:e}=this.props;this.cacheNeedsClearing||(e("Import complete"),this.stopImport())}onImportStarted(){const{notes:e,setImportStarted:t,updateNote:a}=this.props,o=e.find(e=>"wc-admin-historical-data"===e.name);o&&a(o.id,{status:"actioned"}),t(!0)}clearStatusAndTotalsCache(){const{selectedPeriod:e,skipChecked:t}=this.props,a=S(this.dateFormat,e,t);this.clearCache("getImportTotals",a),this.clearCache("getImportStatus")}isImportationInProgress(){const{lastImportStartTimestamp:e}=this.props;return void 0!==e&&void 0===this.lastImportStopTimestamp||e>this.lastImportStopTimestamp}render(){const{activeImport:e,createNotice:t,lastImportStartTimestamp:a,selectedPeriod:o,skipChecked:r}=this.props;return Object(s.createElement)(x,{activeImport:e,cacheNeedsClearing:this.cacheNeedsClearing,createNotice:t,dateFormat:this.dateFormat,inProgress:this.isImportationInProgress(),onImportFinished:this.onImportFinished,onImportStarted:this.onImportStarted,lastImportStartTimestamp:a,clearStatusAndTotalsCache:this.clearStatusAndTotalsCache,period:o,skipChecked:r,startStatusCheckInterval:this.startStatusCheckInterval,stopImport:this.stopImport})}}var R=Object(n.compose)([Object(m.withSelect)(e=>{const{getNotes:t}=e(d.NOTES_STORE_NAME),{getImportStarted:a,getFormSettings:o}=e(d.IMPORT_STORE_NAME),r=t({page:1,per_page:d.QUERY_DEFAULTS.pageSize,type:"update",status:"unactioned"}),{activeImport:s,lastImportStartTimestamp:c}=a(),{period:i,skipPrevious:n}=o();return{activeImport:s,lastImportStartTimestamp:c,notes:r,selectedPeriod:i,skipChecked:n}}),Object(m.withDispatch)(e=>{const{updateNote:t}=e(d.NOTES_STORE_NAME),{invalidateResolution:a,setImportStarted:o}=e(d.IMPORT_STORE_NAME);return{invalidateResolution:a,setImportStarted:o,updateNote:t}}),i.withSpokenMessages])(D);t.default=Object(n.compose)(Object(m.withDispatch)(e=>{const{createNotice:t}=e("core/notices");return{createNotice:t}}))(({createNotice:e,query:t})=>{const{settingsError:a,isRequesting:o,isDirty:n,persistSettings:m,updateAndPersistSettings:b,updateSettings:h,wcAdminSettings:_}=Object(d.useSettings)("wc_admin",["wcAdminSettings"]),g=Object(s.useRef)(!1);Object(s.useEffect)(()=>{function e(e){if(n)return e.returnValue=Object(c.__)("You have unsaved changes. If you proceed, they will be lost.",'woocommerce'),e.returnValue}return window.addEventListener("beforeunload",e),()=>window.removeEventListener("beforeunload",e)},[n]),Object(s.useEffect)(()=>{o?g.current=!0:!o&&g.current&&(a?e("error",Object(c.__)("There was an error saving your settings. Please try again.",'woocommerce')):e("success",Object(c.__)("Your settings have been successfully saved.",'woocommerce')),g.current=!1)},[o,a,e]);const O=e=>{const{checked:t,name:a,type:o,value:r}=e.target,s={..._};s[a]="checkbox"===o?t?[...s[a],r]:s[a].filter(e=>e!==r):r,h("wcAdminSettings",s)};return Object(s.createElement)(s.Fragment,null,Object(s.createElement)(l.SectionHeader,{title:Object(c.__)("Analytics settings",'woocommerce')}),Object(s.createElement)("div",{className:"woocommerce-settings__wrapper"},Object.keys(u.b).map(e=>Object(s.createElement)(w,r()({handleChange:O,value:_[e],key:e,name:e},u.b[e]))),Object(s.createElement)("div",{className:"woocommerce-settings__actions"},Object(s.createElement)(i.Button,{isSecondary:!0,onClick:()=>{if(window.confirm(Object(c.__)("Are you sure you want to reset all settings to default values?",'woocommerce'))){const e=Object.keys(u.b).reduce((e,t)=>(e[t]=u.b[t].defaultValue,e),{});b("wcAdminSettings",e),Object(p.recordEvent)("analytics_settings_reset_defaults")}}},Object(c.__)("Reset defaults",'woocommerce')),Object(s.createElement)(i.Button,{isPrimary:!0,isBusy:o,onClick:()=>{m(),Object(p.recordEvent)("analytics_settings_save",_),t.period=void 0,t.compare=void 0,t.before=void 0,t.after=void 0,t.interval=void 0,t.type=void 0,window.wpNavMenuUrlUpdate(t)}},Object(c.__)("Save settings",'woocommerce')))),"true"===t.import?Object(s.createElement)(l.ScrollTo,{offset:"-56"},Object(s.createElement)(R,{createNotice:e})):Object(s.createElement)(R,{createNotice:e}))})}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/customizable-dashboard.js b/packages/woocommerce-admin/dist/chunks/customizable-dashboard.js new file mode 100644 index 0000000..6926a0e --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/customizable-dashboard.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[24],{168:function(e,t,n){"use strict";var o=Object.assign||function(e){for(var t,n=1;n{const t=s.getCurrencyConfig(),n=Object(r.applyFilters)("woocommerce_admin_report_currency",t,e);return a()(n)},d=Object(o.createContext)(s)},505:function(e,t,n){"use strict";var o=n(0),r=n(14),c=n(1),a=n.n(c),i=n(4),s=n(7),l=n(21),d=n(13),m=n(11),u=n(19),p=n(16),b=n(501),h=n(55);class v extends o.Component{constructor(){super(),this.onDateSelect=this.onDateSelect.bind(this),this.onFilterSelect=this.onFilterSelect.bind(this),this.onAdvancedFilterAction=this.onAdvancedFilterAction.bind(this)}onDateSelect(e){const{report:t,addCesSurveyForAnalytics:n}=this.props;n(),Object(p.recordEvent)("datepicker_update",{report:t,...Object(i.omitBy)(e,i.isUndefined)})}onFilterSelect(e){const{report:t,addCesSurveyForAnalytics:n}=this.props,o=e.filter||e["filter-variations"];["single_product","single_category","single_coupon","single_variation"].includes(o)&&n();const r={report:t,filter:e.filter||"all"};"single_product"===e.filter&&(r.filter_variation=e["filter-variations"]||"all"),Object(p.recordEvent)("analytics_filter",r)}onAdvancedFilterAction(e,t){const{report:n,addCesSurveyForAnalytics:o}=this.props;switch(e){case"add":Object(p.recordEvent)("analytics_filters_add",{report:n,filter:t.key});break;case"remove":Object(p.recordEvent)("analytics_filters_remove",{report:n,filter:t.key});break;case"filter":const e=Object.keys(t).reduce((e,n)=>(e[Object(i.snakeCase)(n)]=t[n],e),{});o(),Object(p.recordEvent)("analytics_filters_filter",{report:n,...e});break;case"clear_all":Object(p.recordEvent)("analytics_filters_clear_all",{report:n});break;case"match":Object(p.recordEvent)("analytics_filters_all_any",{report:n,value:t.match})}}render(){const{advancedFilters:e,filters:t,path:n,query:r,showDatePicker:c,defaultDateRange:a}=this.props,{period:i,compare:s,before:m,after:p}=Object(u.getDateParamsFromQuery)(r,a),{primary:b,secondary:h}=Object(u.getCurrentDates)(r,a),v={period:i,compare:s,before:m,after:p,primaryDate:b,secondaryDate:h},_=this.context;return Object(o.createElement)(l.ReportFilters,{query:r,siteLocale:d.b.siteLocale,currency:_.getCurrencyConfig(),path:n,filters:t,advancedFilters:e,showDatePicker:c,onDateSelect:this.onDateSelect,onFilterSelect:this.onFilterSelect,onAdvancedFilterAction:this.onAdvancedFilterAction,dateQuery:v,isoDateFormat:u.isoDateFormat})}}v.contextType=b.a,t.a=Object(r.compose)(Object(s.withSelect)(e=>{const{woocommerce_default_date_range:t}=e(m.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");return{defaultDateRange:t}}),Object(s.withDispatch)(e=>{const{addCesSurveyForAnalytics:t}=e(h.c);return{addCesSurveyForAnalytics:t}}))(v),v.propTypes={advancedFilters:a.a.object,filters:a.a.array,path:a.a.string.isRequired,query:a.a.object,showDatePicker:a.a.bool,report:a.a.string.isRequired}},591:function(e,t,n){"use strict";var o=Object.assign||function(e){for(var t,n=1;nn.e(26).then(n.bind(null,617))),y=Object(o.lazy)(()=>Promise.all([n.e(1),n.e(32)]).then(n.bind(null,620))),k=Object(o.lazy)(()=>n.e(50).then(n.bind(null,610)));var E=Object(s.applyFilters)("woocommerce_dashboard_default_sections",[{key:"store-performance",component:e=>Object(o.createElement)(o.Suspense,{fallback:Object(o.createElement)(p.Spinner,null)},Object(o.createElement)(k,e)),title:Object(r.__)("Performance",'woocommerce'),isVisible:!0,icon:O,hiddenBlocks:["coupons/amount","coupons/orders_count","downloads/download_count","taxes/order_tax","taxes/total_tax","taxes/shipping_tax","revenue/shipping","orders/avg_order_value","revenue/refunds","revenue/gross_sales"]},{key:"charts",component:e=>Object(o.createElement)(o.Suspense,{fallback:Object(o.createElement)(p.Spinner,null)},Object(o.createElement)(w,e)),title:Object(r.__)("Charts",'woocommerce'),isVisible:!0,icon:f,hiddenBlocks:["orders_avg_order_value","avg_items_per_order","products_items_sold","revenue_total_sales","revenue_refunds","coupons_amount","coupons_orders_count","revenue_shipping","taxes_total_tax","taxes_order_tax","taxes_shipping_tax","downloads_download_count"]},{key:"leaderboards",component:e=>Object(o.createElement)(o.Suspense,{fallback:Object(o.createElement)(p.Spinner,null)},Object(o.createElement)(y,e)),title:Object(r.__)("Leaderboards",'woocommerce'),isVisible:!0,icon:Object(o.createElement)(g.a,null),hiddenBlocks:["coupons","customers"]}]),C=n(35),S=n.n(C),x=Object(o.createElement)(d.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 24 24"},Object(o.createElement)(d.Path,{d:"M12 4h3c.6 0 1 .4 1 1v1H3V5c0-.6.5-1 1-1h3c.2-1.1 1.3-2 2.5-2s2.3.9 2.5 2zM8 4h3c-.2-.6-.9-1-1.5-1S8.2 3.4 8 4zM4 7h11l-.9 10.1c0 .5-.5.9-1 .9H5.9c-.5 0-.9-.4-1-.9L4 7z"})),M=n(592),B=n.n(M),F=n(168),T=n.n(F);class D extends o.Component{constructor(e){super(e),this.onMoveUp=this.onMoveUp.bind(this),this.onMoveDown=this.onMoveDown.bind(this)}onMoveUp(){const{onMove:e,onToggle:t}=this.props;e(-1),t()}onMoveDown(){const{onMove:e,onToggle:t}=this.props;e(1),t()}render(){const{onRemove:e,isFirst:t,isLast:n,onTitleBlur:c,onTitleChange:a,titleInput:s}=this.props;return Object(o.createElement)(o.Fragment,null,Object(o.createElement)("div",{className:"woocommerce-ellipsis-menu__item"},Object(o.createElement)(i.TextControl,{label:Object(r.__)("Section title",'woocommerce'),onBlur:c,onChange:a,required:!0,value:s})),Object(o.createElement)("div",{className:"woocommerce-dashboard-section-controls"},!t&&Object(o.createElement)(p.MenuItem,{isClickable:!0,onInvoke:this.onMoveUp},Object(o.createElement)(l.a,{icon:Object(o.createElement)(B.a,null),label:Object(r.__)("Move up"),size:20,className:"icon-control"}),Object(r.__)("Move up",'woocommerce')),!n&&Object(o.createElement)(p.MenuItem,{isClickable:!0,onInvoke:this.onMoveDown},Object(o.createElement)(l.a,{icon:Object(o.createElement)(T.a,null),size:20,label:Object(r.__)("Move down"),className:"icon-control"}),Object(r.__)("Move down",'woocommerce')),Object(o.createElement)(p.MenuItem,{isClickable:!0,onInvoke:e},Object(o.createElement)(l.a,{icon:x,size:20,label:Object(r.__)("Remove block"),className:"icon-control"}),Object(r.__)("Remove section",'woocommerce'))))}}var V=D;class z extends o.Component{constructor(e){super(e);const{title:t}=e;this.state={titleInput:t},this.onToggleHiddenBlock=this.onToggleHiddenBlock.bind(this),this.onTitleChange=this.onTitleChange.bind(this),this.onTitleBlur=this.onTitleBlur.bind(this)}onTitleChange(e){this.setState({titleInput:e})}onTitleBlur(){const{onTitleUpdate:e,title:t}=this.props,{titleInput:n}=this.state;""===n?this.setState({titleInput:t}):e&&e(n)}onToggleHiddenBlock(e){return()=>{const t=Object(a.xor)(this.props.hiddenBlocks,[e]);this.props.onChangeHiddenBlocks(t)}}render(){const{component:e,...t}=this.props,{titleInput:n}=this.state;return Object(o.createElement)("div",{className:"woocommerce-dashboard-section"},Object(o.createElement)(e,S()({onTitleChange:this.onTitleChange,onTitleBlur:this.onTitleBlur,onToggleHiddenBlock:this.onToggleHiddenBlock,titleInput:n,controls:V},t)))}}var H=n(505),N=n(501);const A=Object(s.applyFilters)("woocommerce_admin_dashboard_filters",[]);t.default=Object(c.compose)(Object(u.withSelect)(e=>{const{woocommerce_default_date_range:t}=e(b.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings");return{defaultDateRange:t}}))(({defaultDateRange:e,path:t,query:n})=>{const{updateUserPreferences:c,...s}=Object(b.useUserPreferences)(),d=Object(o.useMemo)(()=>(e=>{if(!e||0===e.length)return E.reduce((e,t)=>[...e,{...t}],[]);const t=E.map(e=>e.key),n=e.map(e=>e.key),o=new Set([...n,...t]),r=[];return o.forEach(t=>{const n=E.find(e=>e.key===t);if(!n)return;const o=e.find(e=>e.key===t);o&&delete o.icon,r.push({...n,...o})}),r})(s.dashboard_sections),[s.dashboard_sections]),u=e=>{c({dashboard_sections:e})},O=(e,t)=>{const n=d.map(n=>(delete n.icon,n.key===e?{...n,...t}:n));u(n)},f=e=>t=>{Object(_.recordEvent)("dash_section_rename",{key:e}),O(e,{title:t})},j=(e,t)=>()=>{t&&t();const n=d.findIndex(t=>e===t.key),o=d.splice(n,1).shift();o.isVisible=!o.isVisible,d.push(o),o.isVisible?Object(_.recordEvent)("dash_section_add",{key:o.key}):Object(_.recordEvent)("dash_section_remove",{key:o.key}),u(d)},g=(e,t)=>{const n=d.splice(e,1).shift(),o=e+t;if(d[t<0?o:o-1].isVisible||0===e||e===d.length-1){d.splice(o,0,n),u(d);const e={key:n.key,direction:t>0?"down":"up"};Object(_.recordEvent)("dash_section_order_change",e)}else g(e,t+t)};return Object(o.createElement)(N.a.Provider,{value:Object(N.b)(Object(h.getQuery)())},(()=>{const{period:c,compare:s,before:u,after:b}=Object(v.getDateParamsFromQuery)(n,e),{primary:h,secondary:_}=Object(v.getCurrentDates)(n,e),w={period:c,compare:s,before:u,after:b,primaryDate:h,secondaryDate:_},y=d.filter(e=>e.isVisible).map(e=>e.key);return Object(o.createElement)(o.Fragment,null,Object(o.createElement)(H.a,{report:"dashboard",query:n,path:t,dateQuery:w,isoDateFormat:v.isoDateFormat,filters:A}),d.map((e,r)=>{return e.isVisible?Object(o.createElement)(z,{component:e.component,hiddenBlocks:e.hiddenBlocks,key:e.key,onChangeHiddenBlocks:(c=e.key,e=>{O(c,{hiddenBlocks:e})}),onTitleUpdate:f(e.key),path:t,query:n,title:e.title,onMove:Object(a.partial)(g,r),onRemove:j(e.key),isFirst:e.key===y[0],isLast:e.key===y[y.length-1],filters:A}):null;var c}),(()=>{const e=d.filter(e=>!1===e.isVisible);return 0===e.length?null:Object(o.createElement)(i.Dropdown,{position:"top center",className:"woocommerce-dashboard-section__add-more",renderToggle:({onToggle:e,isOpen:t})=>Object(o.createElement)(i.Button,{onClick:e,title:Object(r.__)("Add more sections",'woocommerce'),"aria-expanded":t},Object(o.createElement)(l.a,{icon:m})),renderContent:({onToggle:t})=>Object(o.createElement)(o.Fragment,null,Object(o.createElement)(p.H,null,Object(r.__)("Dashboard Sections",'woocommerce')),Object(o.createElement)("div",{className:"woocommerce-dashboard-section__add-more-choices"},e.map(e=>Object(o.createElement)(i.Button,{key:e.key,onClick:j(e.key,t),className:"woocommerce-dashboard-section__add-more-btn",title:Object(r.sprintf)(Object(r.__)("Add %s section",'woocommerce'),e.title)},Object(o.createElement)(l.a,{className:e.key+"__icon",icon:e.icon,size:30}),Object(o.createElement)("span",{className:"woocommerce-dashboard-section__add-more-btn-title"},e.title)))))})})())})())})}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/dashboard-charts.js b/packages/woocommerce-admin/dist/chunks/dashboard-charts.js new file mode 100644 index 0000000..c948bfb --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/dashboard-charts.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[26],{165:function(e,t,r){"use strict";var o=Object.assign||function(e){for(var t,r=1;re.map(t))}}d(i.NAMESPACE+"/products/attributes",e=>({key:e.id,label:e.name}));const u=d(i.NAMESPACE+"/products/categories",e=>({key:e.id,label:e.name})),_=d(i.NAMESPACE+"/coupons",e=>({key:e.id,label:e.code})),p=d(i.NAMESPACE+"/customers",e=>({key:e.id,label:e.name})),b=d(i.NAMESPACE+"/products",e=>({key:e.id,label:e.name})),h=d(i.NAMESPACE+"/taxes",e=>({key:e.id,label:Object(m.a)(e)}));function O({attributes:e,name:t}){const r=Object(s.f)("variationTitleAttributesSeparator"," - ");if(t&&t.indexOf(r)>-1)return t;const o=(e||[]).map(({option:e})=>e).join(", ");return o?t+r+o:t}const j=d(({products:e})=>e?i.NAMESPACE+`/products/${e}/variations`:i.NAMESPACE+"/variations",e=>({key:e.id,label:O(e)}))},503:function(e,t,r){"use strict";r.d(t,"a",(function(){return a}));var o=r(2);function a(e){return[e.country,e.state,e.name||Object(o.__)("TAX",'woocommerce'),e.priority].map(e=>e.toString().toUpperCase().trim()).filter(Boolean).join("-")}},504:function(e,t,r){"use strict";var o=r(0),a=r(2),c=r(1),n=r.n(c),l=r(21);function i({className:e}){const t=Object(a.__)("There was an error getting your stats. Please try again.",'woocommerce'),r=Object(a.__)("Reload",'woocommerce');return Object(o.createElement)(l.EmptyContent,{className:e,title:t,actionLabel:r,actionCallback:()=>{window.location.reload()}})}i.propTypes={className:n.a.string},t.a=i},508:function(e,t,r){"use strict";var o=r(0),a=r(2),c=r(14),n=r(59),l=r(7),i=r(4),s=r(1),m=r.n(s),d=r(21),u=r(11),_=r(19),p=r(501),b=r(504),h=r(12);class O extends o.Component{shouldComponentUpdate(e){return e.isRequesting!==this.props.isRequesting||e.primaryData.isRequesting!==this.props.primaryData.isRequesting||e.secondaryData.isRequesting!==this.props.secondaryData.isRequesting||!Object(i.isEqual)(e.query,this.props.query)}getItemChartData(){const{primaryData:e,selectedChart:t}=this.props;return e.data.intervals.map((function(e){const r={};return e.subtotals.segments.forEach((function(e){if(e.segment_label){const o=r[e.segment_label]?e.segment_label+" (#"+e.segment_id+")":e.segment_label;r[e.segment_id]={label:o,value:e.subtotals[t.key]||0}}})),{date:Object(n.format)("Y-m-d\\TH:i:s",e.date_start),...r}}))}getTimeChartData(){const{query:e,primaryData:t,secondaryData:r,selectedChart:o,defaultDateRange:a}=this.props,c=Object(_.getIntervalForQuery)(e),{primary:l,secondary:i}=Object(_.getCurrentDates)(e,a);return t.data.intervals.map((function(t,a){const s=Object(_.getPreviousDate)(t.date_start,l.after,i.after,e.compare,c),m=r.data.intervals[a];return{date:Object(n.format)("Y-m-d\\TH:i:s",t.date_start),primary:{label:`${l.label} (${l.range})`,labelDate:t.date_start,value:t.subtotals[o.key]||0},secondary:{label:`${i.label} (${i.range})`,labelDate:s.format("YYYY-MM-DD HH:mm:ss"),value:m&&m.subtotals[o.key]||0}}}))}getTimeChartTotals(){const{primaryData:e,secondaryData:t,selectedChart:r}=this.props;return{primary:Object(i.get)(e,["data","totals",r.key],null),secondary:Object(i.get)(t,["data","totals",r.key],null)}}renderChart(e,t,r,c){const{emptySearchResults:n,filterParam:l,interactiveLegend:i,itemsLabel:s,legendPosition:m,path:p,query:b,selectedChart:h,showHeaderControls:O,primaryData:j}=this.props,w=Object(_.getIntervalForQuery)(b),y=Object(_.getAllowedIntervalsForQuery)(b),f=Object(_.getDateFormatsForInterval)(w,j.data.intervals.length),v=n?Object(a.__)("No data for the current search",'woocommerce'):Object(a.__)("No data for the selected date range",'woocommerce'),{formatAmount:g,getCurrencyConfig:x}=this.context;return Object(o.createElement)(d.Chart,{allowedIntervals:y,data:r,dateParser:"%Y-%m-%dT%H:%M:%S",emptyMessage:v,filterParam:l,interactiveLegend:i,interval:w,isRequesting:t,itemsLabel:s,legendPosition:m,legendTotals:c,mode:e,path:p,query:b,screenReaderFormat:f.screenReaderFormat,showHeaderControls:O,title:h.label,tooltipLabelFormat:f.tooltipLabelFormat,tooltipTitle:"time-comparison"===e&&h.label||null,tooltipValueFormat:Object(u.getTooltipValueFormat)(h.type,g),chartType:Object(_.getChartTypeForQuery)(b),valueType:h.type,xFormat:f.xFormat,x2Format:f.x2Format,currency:x()})}renderItemComparison(){const{isRequesting:e,primaryData:t}=this.props;if(t.isError)return Object(o.createElement)(b.a,null);const r=e||t.isRequesting,a=this.getItemChartData();return this.renderChart("item-comparison",r,a)}renderTimeComparison(){const{isRequesting:e,primaryData:t,secondaryData:r}=this.props;if(!t||t.isError||r.isError)return Object(o.createElement)(b.a,null);const a=e||t.isRequesting||r.isRequesting,c=this.getTimeChartData(),n=this.getTimeChartTotals();return this.renderChart("time-comparison",a,c,n)}render(){const{mode:e}=this.props;return"item-comparison"===e?this.renderItemComparison():this.renderTimeComparison()}}O.contextType=p.a,O.propTypes={filters:m.a.array,isRequesting:m.a.bool,itemsLabel:m.a.string,limitProperties:m.a.array,mode:m.a.string,path:m.a.string.isRequired,primaryData:m.a.object,query:m.a.object.isRequired,secondaryData:m.a.object,selectedChart:m.a.shape({key:m.a.string.isRequired,label:m.a.string.isRequired,order:m.a.oneOf(["asc","desc"]),orderby:m.a.string,type:m.a.oneOf(["average","number","currency"]).isRequired}).isRequired},O.defaultProps={isRequesting:!1,primaryData:{data:{intervals:[]},isError:!1,isRequesting:!1},secondaryData:{data:{intervals:[]},isError:!1,isRequesting:!1}};t.a=Object(c.compose)(Object(l.withSelect)((e,t)=>{const{charts:r,endpoint:o,filters:a,isRequesting:c,limitProperties:n,query:l,advancedFilters:s}=t,m=n||[o],d=function e(t,r,o={}){if(!t||0===t.length)return null;const a=t.slice(0),c=a.pop();if(c.showFilters(r,o)){const e=Object(h.flattenFilters)(c.filters),t=r[c.param]||c.defaultValue||"all";return Object(i.find)(e,{value:t})}return e(a,r,o)}(a,l),_=Object(i.get)(d,["settings","param"]),p=t.mode||function(e,t){if(e&&t){const r=Object(i.get)(e,["settings","param"]);if(!r||Object.keys(t).includes(r))return Object(i.get)(e,["chartMode"])}return null}(d,l)||"time-comparison",{woocommerce_default_date_range:b}=e(u.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings"),O=e(u.REPORTS_STORE_NAME),j={mode:p,filterParam:_,defaultDateRange:b};if(c)return j;const w=m.some(e=>l[e]&&l[e].length);if(l.search&&!w)return{...j,emptySearchResults:!0};const y=r&&r.map(e=>e.key),f=Object(u.getReportChartData)({endpoint:o,dataType:"primary",query:l,selector:O,limitBy:m,filters:a,advancedFilters:s,defaultDateRange:b,fields:y});if("item-comparison"===p)return{...j,primaryData:f};const v=Object(u.getReportChartData)({endpoint:o,dataType:"secondary",query:l,selector:O,limitBy:m,filters:a,advancedFilters:s,defaultDateRange:b,fields:y});return{...j,primaryData:f,secondaryData:v}}))(O)},528:function(e,t,r){"use strict";r.d(t,"b",(function(){return c})),r.d(t,"a",(function(){return n})),r.d(t,"c",(function(){return i}));var o=r(2),a=r(30);const c=Object(a.applyFilters)("woocommerce_admin_revenue_report_charts",[{key:"gross_sales",label:Object(o.__)("Gross sales",'woocommerce'),order:"desc",orderby:"gross_sales",type:"currency",isReverseTrend:!1},{key:"refunds",label:Object(o.__)("Returns",'woocommerce'),order:"desc",orderby:"refunds",type:"currency",isReverseTrend:!0},{key:"coupons",label:Object(o.__)("Coupons",'woocommerce'),order:"desc",orderby:"coupons",type:"currency",isReverseTrend:!1},{key:"net_revenue",label:Object(o.__)("Net sales",'woocommerce'),orderby:"net_revenue",type:"currency",isReverseTrend:!1},{key:"taxes",label:Object(o.__)("Taxes",'woocommerce'),order:"desc",orderby:"taxes",type:"currency",isReverseTrend:!1},{key:"shipping",label:Object(o.__)("Shipping",'woocommerce'),orderby:"shipping",type:"currency",isReverseTrend:!1},{key:"total_sales",label:Object(o.__)("Total sales",'woocommerce'),order:"desc",orderby:"total_sales",type:"currency",isReverseTrend:!1}]),n=Object(a.applyFilters)("woocommerce_admin_revenue_report_advanced_filters",{filters:{},title:Object(o._x)("Revenue Matches {{select /}} Filters","A sentence describing filters for Revenue. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')}),l=[];Object.keys(n.filters).length&&(l.push({label:Object(o.__)("All Revenue",'woocommerce'),value:"all"}),l.push({label:Object(o.__)("Advanced Filters",'woocommerce'),value:"advanced"}));const i=Object(a.applyFilters)("woocommerce_admin_revenue_report_filters",[{label:Object(o.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>l.length>0,filters:l}])},529:function(e,t,r){"use strict";r.d(t,"b",(function(){return s})),r.d(t,"a",(function(){return u})),r.d(t,"c",(function(){return _}));var o=r(2),a=r(30),c=r(7),n=r(502),l=r(55);const{addCesSurveyForAnalytics:i}=Object(c.dispatch)(l.c),s=Object(a.applyFilters)("woocommerce_admin_products_report_charts",[{key:"items_sold",label:Object(o.__)("Items sold",'woocommerce'),order:"desc",orderby:"items_sold",type:"number"},{key:"net_revenue",label:Object(o.__)("Net sales",'woocommerce'),order:"desc",orderby:"net_revenue",type:"currency"},{key:"orders_count",label:Object(o.__)("Orders",'woocommerce'),order:"desc",orderby:"orders_count",type:"number"}]),m={label:Object(o.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:[{label:Object(o.__)("All products",'woocommerce'),value:"all"},{label:Object(o.__)("Single product",'woocommerce'),value:"select_product",chartMode:"item-comparison",subFilters:[{component:"Search",value:"single_product",chartMode:"item-comparison",path:["select_product"],settings:{type:"products",param:"products",getLabels:n.d,labels:{placeholder:Object(o.__)("Type to search for a product",'woocommerce'),button:Object(o.__)("Single product",'woocommerce')}}}]},{label:Object(o.__)("Comparison",'woocommerce'),value:"compare-products",chartMode:"item-comparison",settings:{type:"products",param:"products",getLabels:n.d,labels:{helpText:Object(o.__)("Check at least two products below to compare",'woocommerce'),placeholder:Object(o.__)("Search for products to compare",'woocommerce'),title:Object(o.__)("Compare Products",'woocommerce'),update:Object(o.__)("Compare",'woocommerce')},onClick:i}}]},d={showFilters:e=>"single_product"===e.filter&&!!e.products&&e["is-variable"],staticParams:["filter","products","chartType","paged","per_page"],param:"filter-variations",filters:[{label:Object(o.__)("All variations",'woocommerce'),chartMode:"item-comparison",value:"all"},{label:Object(o.__)("Single variation",'woocommerce'),value:"select_variation",subFilters:[{component:"Search",value:"single_variation",path:["select_variation"],settings:{type:"variations",param:"variations",getLabels:n.g,labels:{placeholder:Object(o.__)("Type to search for a variation",'woocommerce'),button:Object(o.__)("Single variation",'woocommerce')}}}]},{label:Object(o.__)("Comparison",'woocommerce'),chartMode:"item-comparison",value:"compare-variations",settings:{type:"variations",param:"variations",getLabels:n.g,labels:{helpText:Object(o.__)("Check at least two variations below to compare",'woocommerce'),placeholder:Object(o.__)("Search for variations to compare",'woocommerce'),title:Object(o.__)("Compare Variations",'woocommerce'),update:Object(o.__)("Compare",'woocommerce')}}}]},u=Object(a.applyFilters)("woocommerce_admin_products_report_advanced_filters",{filters:{},title:Object(o._x)("Products Match {{select /}} Filters","A sentence describing filters for Products. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')});Object.keys(u.filters).length&&(m.filters.push({label:Object(o.__)("Advanced Filters",'woocommerce'),value:"advanced"}),d.filters.push({label:Object(o.__)("Advanced Filters",'woocommerce'),value:"advanced"}));const _=Object(a.applyFilters)("woocommerce_admin_products_report_filters",[m,d])},533:function(e,t,r){"use strict";r.d(t,"b",(function(){return l})),r.d(t,"c",(function(){return i})),r.d(t,"a",(function(){return s}));var o=r(2),a=r(30),c=r(13),n=r(502);const l=Object(a.applyFilters)("woocommerce_admin_orders_report_charts",[{key:"orders_count",label:Object(o.__)("Orders",'woocommerce'),type:"number"},{key:"net_revenue",label:Object(o.__)("Net sales",'woocommerce'),order:"desc",orderby:"net_total",type:"currency"},{key:"avg_order_value",label:Object(o.__)("Average order value",'woocommerce'),type:"currency"},{key:"avg_items_per_order",label:Object(o.__)("Average items per order",'woocommerce'),order:"desc",orderby:"num_items_sold",type:"average"}]),i=Object(a.applyFilters)("woocommerce_admin_orders_report_filters",[{label:Object(o.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:[{label:Object(o.__)("All orders",'woocommerce'),value:"all"},{label:Object(o.__)("Advanced filters",'woocommerce'),value:"advanced"}]}]),s=Object(a.applyFilters)("woocommerce_admin_orders_report_advanced_filters",{title:Object(o._x)("Orders match {{select /}} filters","A sentence describing filters for Orders. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce'),filters:{status:{labels:{add:Object(o.__)("Order Status",'woocommerce'),remove:Object(o.__)("Remove order status filter",'woocommerce'),rule:Object(o.__)("Select an order status filter match",'woocommerce'),title:Object(o.__)("{{title}}Order Status{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select an order status",'woocommerce')},rules:[{value:"is",label:Object(o._x)("Is","order status",'woocommerce')},{value:"is_not",label:Object(o._x)("Is Not","order status",'woocommerce')}],input:{component:"SelectControl",options:Object.keys(c.c).map(e=>({value:e,label:c.c[e]}))}},product:{labels:{add:Object(o.__)("Products",'woocommerce'),placeholder:Object(o.__)("Search products",'woocommerce'),remove:Object(o.__)("Remove products filter",'woocommerce'),rule:Object(o.__)("Select a product filter match",'woocommerce'),title:Object(o.__)("{{title}}Product{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select products",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","products",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","products",'woocommerce')}],input:{component:"Search",type:"products",getLabels:n.d}},variation:{labels:{add:Object(o.__)("Variations",'woocommerce'),placeholder:Object(o.__)("Search variations",'woocommerce'),remove:Object(o.__)("Remove variations filter",'woocommerce'),rule:Object(o.__)("Select a variation filter match",'woocommerce'),title:Object(o.__)("{{title}}Variation{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select variation",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","variations",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","variations",'woocommerce')}],input:{component:"Search",type:"variations",getLabels:n.g}},coupon:{labels:{add:Object(o.__)("Coupon Codes",'woocommerce'),placeholder:Object(o.__)("Search coupons",'woocommerce'),remove:Object(o.__)("Remove coupon filter",'woocommerce'),rule:Object(o.__)("Select a coupon filter match",'woocommerce'),title:Object(o.__)("{{title}}Coupon code{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select coupon codes",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","coupon code",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","coupon code",'woocommerce')}],input:{component:"Search",type:"coupons",getLabels:n.b}},customer_type:{labels:{add:Object(o.__)("Customer type",'woocommerce'),remove:Object(o.__)("Remove customer filter",'woocommerce'),rule:Object(o.__)("Select a customer filter match",'woocommerce'),title:Object(o.__)("{{title}}Customer is{{/title}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select a customer type",'woocommerce')},input:{component:"SelectControl",options:[{value:"new",label:Object(o.__)("New",'woocommerce')},{value:"returning",label:Object(o.__)("Returning",'woocommerce')}],defaultOption:"new"}},refunds:{labels:{add:Object(o.__)("Refunds",'woocommerce'),remove:Object(o.__)("Remove refunds filter",'woocommerce'),rule:Object(o.__)("Select a refund filter match",'woocommerce'),title:Object(o.__)("{{title}}Refunds{{/title}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select a refund type",'woocommerce')},input:{component:"SelectControl",options:[{value:"all",label:Object(o.__)("All",'woocommerce')},{value:"partial",label:Object(o.__)("Partially refunded",'woocommerce')},{value:"full",label:Object(o.__)("Fully refunded",'woocommerce')},{value:"none",label:Object(o.__)("None",'woocommerce')}],defaultOption:"all"}},tax_rate:{labels:{add:Object(o.__)("Tax Rates",'woocommerce'),placeholder:Object(o.__)("Search tax rates",'woocommerce'),remove:Object(o.__)("Remove tax rate filter",'woocommerce'),rule:Object(o.__)("Select a tax rate filter match",'woocommerce'),title:Object(o.__)("{{title}}Tax Rate{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select tax rates",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","tax rate",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","tax rate",'woocommerce')}],input:{component:"Search",type:"taxes",getLabels:n.f}},attribute:{allowMultiple:!0,labels:{add:Object(o.__)("Attribute",'woocommerce'),placeholder:Object(o.__)("Search attributes",'woocommerce'),remove:Object(o.__)("Remove attribute filter",'woocommerce'),rule:Object(o.__)("Select a product attribute filter match",'woocommerce'),title:Object(o.__)("{{title}}Attribute{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select attributes",'woocommerce')},rules:[{value:"is",label:Object(o._x)("Is","product attribute",'woocommerce')},{value:"is_not",label:Object(o._x)("Is Not","product attribute",'woocommerce')}],input:{component:"ProductAttribute"}}}})},534:function(e,t,r){"use strict";r.d(t,"b",(function(){return s})),r.d(t,"a",(function(){return m})),r.d(t,"c",(function(){return u}));var o=r(2),a=r(30),c=r(7),n=r(502),l=r(55);const{addCesSurveyForAnalytics:i}=Object(c.dispatch)(l.c),s=Object(a.applyFilters)("woocommerce_admin_coupons_report_charts",[{key:"orders_count",label:Object(o.__)("Discounted orders",'woocommerce'),order:"desc",orderby:"orders_count",type:"number"},{key:"amount",label:Object(o.__)("Amount",'woocommerce'),order:"desc",orderby:"amount",type:"currency"}]),m=Object(a.applyFilters)("woocommerce_admin_coupon_report_advanced_filters",{filters:{},title:Object(o._x)("Coupons match {{select /}} filters","A sentence describing filters for Coupons. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')}),d=[{label:Object(o.__)("All coupons",'woocommerce'),value:"all"},{label:Object(o.__)("Single coupon",'woocommerce'),value:"select_coupon",chartMode:"item-comparison",subFilters:[{component:"Search",value:"single_coupon",chartMode:"item-comparison",path:["select_coupon"],settings:{type:"coupons",param:"coupons",getLabels:n.b,labels:{placeholder:Object(o.__)("Type to search for a coupon",'woocommerce'),button:Object(o.__)("Single Coupon",'woocommerce')}}}]},{label:Object(o.__)("Comparison",'woocommerce'),value:"compare-coupons",settings:{type:"coupons",param:"coupons",getLabels:n.b,labels:{title:Object(o.__)("Compare Coupon Codes",'woocommerce'),update:Object(o.__)("Compare",'woocommerce'),helpText:Object(o.__)("Check at least two coupon codes below to compare",'woocommerce')},onClick:i}}];Object.keys(m.filters).length&&d.push({label:Object(o.__)("Advanced filters",'woocommerce'),value:"advanced"});const u=Object(a.applyFilters)("woocommerce_admin_coupons_report_filters",[{label:Object(o.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:d}])},535:function(e,t,r){"use strict";r.d(t,"b",(function(){return d})),r.d(t,"a",(function(){return u})),r.d(t,"c",(function(){return p}));var o=r(2),a=r(30),c=r(11),n=r(7),l=r(502),i=r(503),s=r(55);const{addCesSurveyForAnalytics:m}=Object(n.dispatch)(s.c),d=Object(a.applyFilters)("woocommerce_admin_taxes_report_charts",[{key:"total_tax",label:Object(o.__)("Total tax",'woocommerce'),order:"desc",orderby:"total_tax",type:"currency"},{key:"order_tax",label:Object(o.__)("Order tax",'woocommerce'),order:"desc",orderby:"order_tax",type:"currency"},{key:"shipping_tax",label:Object(o.__)("Shipping tax",'woocommerce'),order:"desc",orderby:"shipping_tax",type:"currency"},{key:"orders_count",label:Object(o.__)("Orders",'woocommerce'),order:"desc",orderby:"orders_count",type:"number"}]),u=Object(a.applyFilters)("woocommerce_admin_taxes_report_advanced_filters",{filters:{},title:Object(o._x)("Taxes match {{select /}} filters","A sentence describing filters for Taxes. See screen shot for context: https://cloudup.com/cSsUY9VeCVJ",'woocommerce')}),_=[{label:Object(o.__)("All taxes",'woocommerce'),value:"all"},{label:Object(o.__)("Comparison",'woocommerce'),value:"compare-taxes",chartMode:"item-comparison",settings:{type:"taxes",param:"taxes",getLabels:Object(l.e)(c.NAMESPACE+"/taxes",e=>({id:e.id,key:e.id,label:Object(i.a)(e)})),labels:{helpText:Object(o.__)("Check at least two tax codes below to compare",'woocommerce'),placeholder:Object(o.__)("Search for tax codes to compare",'woocommerce'),title:Object(o.__)("Compare Tax Codes",'woocommerce'),update:Object(o.__)("Compare",'woocommerce')},onClick:m}}];Object.keys(u.filters).length&&_.push({label:Object(o.__)("Advanced filters",'woocommerce'),value:"advanced"});const p=Object(a.applyFilters)("woocommerce_admin_taxes_report_filters",[{label:Object(o.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:_}])},536:function(e,t,r){"use strict";r.d(t,"b",(function(){return n})),r.d(t,"c",(function(){return l})),r.d(t,"a",(function(){return i}));var o=r(2),a=r(30),c=r(502);const n=Object(a.applyFilters)("woocommerce_admin_downloads_report_charts",[{key:"download_count",label:Object(o.__)("Downloads",'woocommerce'),type:"number"}]),l=Object(a.applyFilters)("woocommerce_admin_downloads_report_filters",[{label:Object(o.__)("Show",'woocommerce'),staticParams:["chartType","paged","per_page"],param:"filter",showFilters:()=>!0,filters:[{label:Object(o.__)("All downloads",'woocommerce'),value:"all"},{label:Object(o.__)("Advanced filters",'woocommerce'),value:"advanced"}]}]),i=Object(a.applyFilters)("woocommerce_admin_downloads_report_advanced_filters",{title:Object(o._x)("Downloads match {{select /}} filters","A sentence describing filters for Downloads. See screen shot for context: https://cloudup.com/ccxhyH2mEDg",'woocommerce'),filters:{product:{labels:{add:Object(o.__)("Product",'woocommerce'),placeholder:Object(o.__)("Search",'woocommerce'),remove:Object(o.__)("Remove product filter",'woocommerce'),rule:Object(o.__)("Select a product filter match",'woocommerce'),title:Object(o.__)("{{title}}Product{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select product",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","products",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","products",'woocommerce')}],input:{component:"Search",type:"products",getLabels:c.d}},customer:{labels:{add:Object(o.__)("Username",'woocommerce'),placeholder:Object(o.__)("Search customer username",'woocommerce'),remove:Object(o.__)("Remove customer username filter",'woocommerce'),rule:Object(o.__)("Select a customer username filter match",'woocommerce'),title:Object(o.__)("{{title}}Username{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select customer username",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","customer usernames",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","customer usernames",'woocommerce')}],input:{component:"Search",type:"usernames",getLabels:c.c}},order:{labels:{add:Object(o.__)("Order #",'woocommerce'),placeholder:Object(o.__)("Search order number",'woocommerce'),remove:Object(o.__)("Remove order number filter",'woocommerce'),rule:Object(o.__)("Select a order number filter match",'woocommerce'),title:Object(o.__)("{{title}}Order #{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select order number",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","order numbers",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","order numbers",'woocommerce')}],input:{component:"Search",type:"orders",getLabels:async e=>{const t=e.split(",");return await t.map(e=>({id:e,label:"#"+e}))}}},ip_address:{labels:{add:Object(o.__)("IP Address",'woocommerce'),placeholder:Object(o.__)("Search IP address",'woocommerce'),remove:Object(o.__)("Remove IP address filter",'woocommerce'),rule:Object(o.__)("Select an IP address filter match",'woocommerce'),title:Object(o.__)("{{title}}IP Address{{/title}} {{rule /}} {{filter /}}",'woocommerce'),filter:Object(o.__)("Select IP address",'woocommerce')},rules:[{value:"includes",label:Object(o._x)("Includes","IP addresses",'woocommerce')},{value:"excludes",label:Object(o._x)("Excludes","IP addresses",'woocommerce')}],input:{component:"Search",type:"downloadIps",getLabels:async e=>{const t=e.split(",");return await t.map(e=>({id:e,label:e}))}}}}})},599:function(e,t,r){},600:function(e,t,r){},617:function(e,t,r){"use strict";r.r(t);var o=r(0),a=r(2),c=r(6),n=r.n(c),l=r(165),i=r.n(l),s=r(166),m=r.n(s),d=r(1),u=r.n(d),_=r(3),p=r(21),b=r(11),h=r(19),O=r(16),j=r(38),w=r.n(j),y=r(12),f=r(13),v=r(20),g=r(508);r(599);class x extends o.Component{constructor(...e){super(...e),w()(this,"handleChartClick",()=>{const{selectedChart:e}=this.props;Object(y.getHistory)().push(this.getChartPath(e))})}getChartPath(e){return Object(y.getNewPath)({chart:e.key},"/analytics/"+e.endpoint,Object(y.getPersistedQuery)())}render(){const{charts:e,endpoint:t,path:r,query:c,selectedChart:n,filters:l}=this.props;return n?Object(o.createElement)("div",{role:"presentation",className:"woocommerce-dashboard__chart-block-wrapper",onClick:this.handleChartClick},Object(o.createElement)(_.Card,{className:"woocommerce-dashboard__chart-block"},Object(o.createElement)(_.CardHeader,null,Object(o.createElement)(v.Text,{as:"h3",size:16,weight:600,color:"#23282d"},n.label)),Object(o.createElement)(_.CardBody,{size:null},Object(o.createElement)("a",{className:"screen-reader-text",href:Object(f.e)(this.getChartPath(n))},Object(a.sprintf)(Object(a.__)("%s Report",'woocommerce'),n.label)),Object(o.createElement)(g.a,{charts:e,endpoint:t,query:c,interactiveLegend:!1,legendPosition:"bottom",path:r,selectedChart:n,showHeaderControls:!1,filters:l})))):null}}x.propTypes={charts:u.a.array,endpoint:u.a.string.isRequired,path:u.a.string.isRequired,query:u.a.object.isRequired,selectedChart:u.a.object.isRequired};var C=x,S=r(30),k=r(533),T=r(529),E=r(528),R=r(534),F=r(535),A=r(536);const P={revenue:E.b,orders:k.b,products:T.b,coupons:R.b,taxes:F.b,downloads:A.b},q=[{label:Object(a.__)("Total sales",'woocommerce'),report:"revenue",key:"total_sales"},{label:Object(a.__)("Net sales",'woocommerce'),report:"revenue",key:"net_revenue"},{label:Object(a.__)("Orders",'woocommerce'),report:"orders",key:"orders_count"},{label:Object(a.__)("Average order value",'woocommerce'),report:"orders",key:"avg_order_value"},{label:Object(a.__)("Items sold",'woocommerce'),report:"products",key:"items_sold"},{label:Object(a.__)("Returns",'woocommerce'),report:"revenue",key:"refunds"},{label:Object(a.__)("Discounted orders",'woocommerce'),report:"coupons",key:"orders_count"},{label:Object(a.__)("Gross discounted",'woocommerce'),report:"coupons",key:"amount"},{label:Object(a.__)("Total tax",'woocommerce'),report:"taxes",key:"total_tax"},{label:Object(a.__)("Order tax",'woocommerce'),report:"taxes",key:"order_tax"},{label:Object(a.__)("Shipping tax",'woocommerce'),report:"taxes",key:"shipping_tax"},{label:Object(a.__)("Shipping",'woocommerce'),report:"revenue",key:"shipping"},{label:Object(a.__)("Downloads",'woocommerce'),report:"downloads",key:"download_count"}],I=Object(S.applyFilters)("woocommerce_admin_dashboard_charts_filter",q.map(e=>({...P[e.report].find(t=>t.key===e.key),label:e.label,endpoint:e.report})));r(600);const D=e=>{const{controls:t,hiddenBlocks:r,isFirst:c,isLast:l,onMove:s,onRemove:d,onTitleBlur:u,onTitleChange:j,onToggleHiddenBlock:w,path:y,title:f,titleInput:v,filters:g}=e,{updateUserPreferences:x,...S}=Object(b.useUserPreferences)(),[k,T]=Object(o.useState)(S.dashboard_chart_type||"line"),[E,R]=Object(o.useState)(S.dashboard_chart_interval||"day"),F={...e.query,chartType:k,interval:E},A=e=>()=>{T(e);x({dashboard_chart_type:e}),Object(O.recordEvent)("dash_charts_type_toggle",{chart_type:e})};return Object(o.createElement)("div",{className:"woocommerce-dashboard__dashboard-charts"},Object(o.createElement)(p.SectionHeader,{title:f||Object(a.__)("Charts",'woocommerce'),menu:Object(o.createElement)(p.EllipsisMenu,{label:Object(a.__)("Choose which charts to display",'woocommerce'),renderContent:({onToggle:e})=>Object(o.createElement)(o.Fragment,null,Object(o.createElement)(p.MenuTitle,null,Object(a.__)("Charts",'woocommerce')),(({hiddenBlocks:e,onToggleHiddenBlock:t})=>I.map(r=>{const a=r.endpoint+"_"+r.key,c=!e.includes(a);return Object(o.createElement)(p.MenuItem,{checked:c,isCheckbox:!0,isClickable:!0,key:r.endpoint+"_"+r.key,onInvoke:()=>{t(a)(),Object(O.recordEvent)("dash_charts_chart_toggle",{status:c?"off":"on",key:a})}},r.label)}))({hiddenBlocks:r,onToggleHiddenBlock:w}),Object(o.createElement)(t,{onToggle:e,onMove:s,onRemove:d,isFirst:c,isLast:l,onTitleBlur:u,onTitleChange:j,titleInput:v}))}),className:"has-interval-select"},(({chartInterval:e,setInterval:t,query:r})=>{const c=Object(h.getAllowedIntervalsForQuery)(r);if(!c||c.length<1)return null;const n={hour:Object(a.__)("By hour",'woocommerce'),day:Object(a.__)("By day",'woocommerce'),week:Object(a.__)("By week",'woocommerce'),month:Object(a.__)("By month",'woocommerce'),quarter:Object(a.__)("By quarter",'woocommerce'),year:Object(a.__)("By year",'woocommerce')};return Object(o.createElement)(_.SelectControl,{className:"woocommerce-chart__interval-select",value:e,options:c.map(e=>({value:e,label:n[e]})),"aria-label":"Chart period",onChange:t})})({chartInterval:E,setInterval:e=>{R(e);x({dashboard_chart_interval:e}),Object(O.recordEvent)("dash_charts_interval",{interval:e})},query:F}),Object(o.createElement)(_.NavigableMenu,{className:"woocommerce-chart__types",orientation:"horizontal",role:"menubar"},Object(o.createElement)(_.Button,{className:n()("woocommerce-chart__type-button",{"woocommerce-chart__type-button-selected":!F.chartType||"line"===F.chartType}),title:Object(a.__)("Line chart",'woocommerce'),"aria-checked":"line"===F.chartType,role:"menuitemradio",tabIndex:"line"===F.chartType?0:-1,onClick:A("line")},Object(o.createElement)(i.a,null)),Object(o.createElement)(_.Button,{className:n()("woocommerce-chart__type-button",{"woocommerce-chart__type-button-selected":"bar"===F.chartType}),title:Object(a.__)("Bar chart",'woocommerce'),"aria-checked":"bar"===F.chartType,role:"menuitemradio",tabIndex:"bar"===F.chartType?0:-1,onClick:A("bar")},Object(o.createElement)(m.a,null)))),(({hiddenBlocks:e,path:t,query:r,filters:a})=>{const c=I.reduce((e,t)=>(void 0===e[t.endpoint]&&(e[t.endpoint]=[]),e[t.endpoint].push(t),e),{});return Object(o.createElement)("div",{className:"woocommerce-dashboard__columns"},I.map(n=>e.includes(n.endpoint+"_"+n.key)?null:Object(o.createElement)(C,{charts:c[n.endpoint],endpoint:n.endpoint,key:n.endpoint+"_"+n.key,path:t,query:r,selectedChart:n,filters:a})))})({hiddenBlocks:r,path:y,query:F,filters:g}))};D.propTypes={path:u.a.string.isRequired,query:u.a.object.isRequired};t.default=D}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/dashboard.js b/packages/woocommerce-admin/dist/chunks/dashboard.js new file mode 100644 index 0000000..53b5c31 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/dashboard.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[25],{516:function(e,n,t){},606:function(e,n,t){"use strict";t.r(n);var c=t(0),r=t(21);t(516);const s=Object(c.lazy)(()=>t.e(24).then(t.bind(null,604)));class a extends c.Component{render(){const{path:e,query:n}=this.props;return Object(c.createElement)(c.Suspense,{fallback:Object(c.createElement)(r.Spinner,null)},Object(c.createElement)(s,{query:n,path:e}))}}n.default=a}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/homescreen.js b/packages/woocommerce-admin/dist/chunks/homescreen.js new file mode 100644 index 0000000..08ce42c --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/homescreen.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[31],{167:function(e,t,a){"use strict";var c=Object.assign||function(e){for(var t,a=1;a{const t=s.getCurrencyConfig(),a=Object(r.applyFilters)("woocommerce_admin_report_currency",t,e);return o()(a)},m=Object(c.createContext)(s)},509:function(e,t,a){"use strict";var c=a(53);const r=["a","b","em","i","strong","p","br"],n=["target","href","rel","name","download"];t.a=e=>({__html:Object(c.sanitize)(e,{ALLOWED_TAGS:r,ALLOWED_ATTR:n})})},512:function(e,t,a){"use strict";a.d(t,"a",(function(){return O})),a.d(t,"b",(function(){return v}));var c=a(0),r=a(6),n=a.n(r),o=a(61),l=a.n(o),s=a(9),i=a.n(s),m=a(1),A=a.n(m),d=a(21),b=a(3),u=(a(522),a(4));class p extends c.Component{render(){const{className:e,hasAction:t,hasDate:a,hasSubtitle:r,lines:o}=this.props,l=n()("woocommerce-activity-card is-loading",e);return Object(c.createElement)("div",{className:l,"aria-hidden":!0},Object(c.createElement)("span",{className:"woocommerce-activity-card__icon"},Object(c.createElement)("span",{className:"is-placeholder"})),Object(c.createElement)("div",{className:"woocommerce-activity-card__header"},Object(c.createElement)("div",{className:"woocommerce-activity-card__title is-placeholder"}),r&&Object(c.createElement)("div",{className:"woocommerce-activity-card__subtitle is-placeholder"}),a&&Object(c.createElement)("div",{className:"woocommerce-activity-card__date"},Object(c.createElement)("span",{className:"is-placeholder"}))),Object(c.createElement)("div",{className:"woocommerce-activity-card__body"},Object(u.range)(o).map(e=>Object(c.createElement)("span",{className:"is-placeholder",key:e}))),t&&Object(c.createElement)("div",{className:"woocommerce-activity-card__actions"},Object(c.createElement)("span",{className:"is-placeholder"})))}}p.propTypes={className:A.a.string,hasAction:A.a.bool,hasDate:A.a.bool,hasSubtitle:A.a.bool,lines:A.a.number},p.defaultProps={hasAction:!1,hasDate:!1,hasSubtitle:!1,lines:1};var v=p;class O extends c.Component{getCard(){const{actions:e,className:t,children:a,date:r,icon:o,subtitle:l,title:s,unread:m}=this.props,A=n()("woocommerce-activity-card",t),b=Array.isArray(e)?e:[e],u=/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(r)?i.a.utc(r).fromNow():r;return Object(c.createElement)("section",{className:A},m&&Object(c.createElement)("span",{className:"woocommerce-activity-card__unread"}),o&&Object(c.createElement)("span",{className:"woocommerce-activity-card__icon","aria-hidden":!0},o),s&&Object(c.createElement)("header",{className:"woocommerce-activity-card__header"},Object(c.createElement)(d.H,{className:"woocommerce-activity-card__title"},s),l&&Object(c.createElement)("div",{className:"woocommerce-activity-card__subtitle"},l),u&&Object(c.createElement)("span",{className:"woocommerce-activity-card__date"},u)),a&&Object(c.createElement)(d.Section,{className:"woocommerce-activity-card__body"},a),e&&Object(c.createElement)("footer",{className:"woocommerce-activity-card__actions"},b.map((e,t)=>Object(c.cloneElement)(e,{key:t}))))}render(){const{onClick:e}=this.props;return e?Object(c.createElement)(b.Button,{className:"woocommerce-activity-card__button",onClick:e},this.getCard()):this.getCard()}}O.propTypes={actions:A.a.oneOfType([A.a.arrayOf(A.a.element),A.a.element]),onClick:A.a.func,className:A.a.string,children:A.a.node,date:A.a.string,icon:A.a.node,subtitle:A.a.node,title:A.a.oneOfType([A.a.string,A.a.node]),unread:A.a.bool},O.defaultProps={icon:Object(c.createElement)(l.a,{size:48}),unread:!1}},516:function(e,t,a){},517:function(e,t,a){"use strict";function c(e){return e?e.substr(1).split("&").reduce((e,t)=>{const a=t.split("="),c=a[0];let r=decodeURIComponent(a[1]);return r=isNaN(Number(r))?r:Number(r),e[c]=r,e},{}):{}}function r(){let e="";const{page:t,path:a,post_type:r}=c(window.location.search);if(t){const c="wc-admin"===t?"home_screen":t;e=a?a.replace(/\//g,"_").substring(1):c}else r&&(e=r);return e}a.d(t,"b",(function(){return c})),a.d(t,"a",(function(){return r}))},520:function(e,t,a){"use strict";var c=a(0),r=a(6),n=a.n(r),o=a(1),l=a.n(o),s=a(20),i=a(21);a(521);class m extends c.Component{render(){const{className:e,menu:t,subtitle:a,title:r,unreadMessages:o}=this.props,l=n()({"woocommerce-layout__inbox-panel-header":a,"woocommerce-layout__activity-panel-header":!a},e),i=o||0;return Object(c.createElement)("div",{className:l},Object(c.createElement)("div",{className:"woocommerce-layout__inbox-title"},Object(c.createElement)(s.Text,{size:16,weight:600,color:"#23282d"},r),Object(c.createElement)(s.Text,{variant:"button",weight:"600",size:"14",lineHeight:"20px"},i>0&&Object(c.createElement)("span",{className:"woocommerce-layout__inbox-badge"},o))),Object(c.createElement)("div",{className:"woocommerce-layout__inbox-subtitle"},a&&Object(c.createElement)(s.Text,{variant:"body.small",size:"14",lineHeight:"20px"},a)),t&&Object(c.createElement)("div",{className:"woocommerce-layout__activity-panel-header-menu"},t))}}m.propTypes={className:l.a.string,unreadMessages:l.a.number,title:l.a.string.isRequired,subtitle:l.a.string,menu:l.a.shape({type:l.a.oneOf([i.EllipsisMenu])})},t.a=m},521:function(e,t,a){},522:function(e,t,a){},523:function(e,t,a){"use strict";var c=a(0),r=a(2),n=a(21),o=a(11),l=a(7),s=a(16),i=a(172),m=a(169),A=a(20),d=a(512),b=a(164),u=a(517);a(524);const p=(e,t)=>{Object(s.recordEvent)("inbox_action_click",{note_name:e.name,note_title:e.title,note_content_inner_link:t})},v=({hasNotes:e,isBatchUpdating:t,lastRead:a,notes:n,onDismiss:o,onNoteActionClick:l})=>{if(t)return;if(!e)return Object(c.createElement)(d.a,{className:"woocommerce-empty-activity-card",title:Object(r.__)("Your inbox is empty",'woocommerce'),icon:!1},Object(r.__)("As things begin to happen in your store your inbox will start to fill up. You'll see things like achievements, new feature announcements, extension recommendations and more!",'woocommerce'));const b=Object(u.a)(),v=e=>{Object(s.recordEvent)("inbox_note_view",{note_content:e.content,note_name:e.name,note_title:e.title,note_type:e.type,screen:b})},O=Object.keys(n).map(e=>n[e]);return Object(c.createElement)(i.a,{role:"menu"},O.map(e=>{const{id:t,is_deleted:r}=e;return r?null:Object(c.createElement)(m.a,{key:t,timeout:500,classNames:"woocommerce-inbox-message"},Object(c.createElement)(A.InboxNoteCard,{key:t,note:e,lastRead:a,onDismiss:o,onNoteActionClick:l,onBodyLinkClick:p,onNoteVisible:v}))}))},O={page:1,per_page:o.QUERY_DEFAULTS.pageSize,status:"unactioned",type:o.QUERY_DEFAULTS.noteTypes,orderby:"date",order:"desc",_fields:["id","name","title","content","type","status","actions","date_created","date_created_gmt","layout","image","is_deleted"]};t.a=()=>{const{createNotice:e}=Object(l.useDispatch)("core/notices"),{batchUpdateNotes:t,removeAllNotes:a,removeNote:i,updateNote:m,triggerNoteAction:d}=Object(l.useDispatch)(o.NOTES_STORE_NAME),{isError:p,isResolvingNotes:w,isBatchUpdating:f,notes:h}=Object(l.useSelect)(e=>{const{getNotes:t,getNotesError:a,isResolving:c,isNotesRequesting:r}=e(o.NOTES_STORE_NAME);return{notes:t(O),isError:Boolean(a("getNotes",[O])),isResolvingNotes:c("getNotes",[O]),isBatchUpdating:r("batchUpdateNotes")}}),{updateUserPreferences:j,...E}=Object(o.useUserPreferences)(),[g]=Object(c.useState)(E.activity_panel_inbox_last_read),[k,B]=Object(c.useState)();Object(c.useEffect)(()=>{const e=Date.now();j({activity_panel_inbox_last_read:e})},[]);const S=async(c=!1)=>{const n="all"===k.type,o=Object(u.a)();if(Object(s.recordEvent)("inbox_action_dismiss",{note_name:k.note.name,note_title:k.note.title,note_name_dismiss_all:n,note_name_dismiss_confirmation:c,screen:o}),c){const c=k.note.id,o=!c||n;try{let n=[];if(o)n=await a({status:O.status});else{const e=await i(c);n=[e]}B(void 0),e("success",n.length>1?Object(r.__)("All messages dismissed",'woocommerce'):Object(r.__)("Message dismissed",'woocommerce'),{actions:[{label:Object(r.__)("Undo",'woocommerce'),onClick:()=>{n.length>1?t(n.map(e=>e.id),{is_deleted:0}):m(c,{is_deleted:0})}}]})}catch(t){const a=o?h.length:1;e("error",Object(r._n)("Message could not be dismissed","Messages could not be dismissed",a,'woocommerce')),B(void 0)}}else B(void 0)};if(p){const e=Object(r.__)("There was an error getting your inbox. Please try again.",'woocommerce'),t=Object(r.__)("Reload",'woocommerce'),a=()=>{window.location.reload()};return Object(c.createElement)(n.EmptyContent,{title:e,actionLabel:t,actionURL:null,actionCallback:a})}const y=Object(b.b)(h);return Object(c.createElement)(c.Fragment,null,Object(c.createElement)("div",{className:"woocommerce-homepage-notes-wrapper"},(w||f)&&Object(c.createElement)(n.Section,null,Object(c.createElement)(A.InboxNotePlaceholder,{className:"banner message-is-unread"})),Object(c.createElement)(n.Section,null,!w&&!f&&v({hasNotes:y,isBatchUpdating:f,lastRead:g,notes:h,onDismiss:(e,t)=>{B({note:e,type:t})},onNoteActionClick:(e,t)=>{d(e.id,t.id)}})),k&&Object(c.createElement)(A.InboxDismissConfirmationModal,{onClose:S,onDismiss:()=>S(!0)})))}},524:function(e,t,a){},525:function(e,t,a){"use strict";a.d(t,"b",(function(){return A})),a.d(t,"a",(function(){return d}));var c=a(9),r=a.n(c),n=a(4),o=a(19),l=a(11),s=a(12),i=a(120),m=a(13);const A=({indicator:e,primaryData:t,secondaryData:a,currency:c,formatAmount:r,persistedQuery:o})=>{const l=Object(n.find)(t.data,t=>t.stat===e.stat),A=Object(n.find)(a.data,t=>t.stat===e.stat);if(!l||!A)return{};const d=l._links&&l._links.report[0]&&l._links.report[0].href||"",b=function(e,t,a){return e?"/jetpack"===e?Object(m.e)("admin.php?page=jetpack#/dashboard"):Object(s.getNewPath)(t,e,{chart:a.chart}):""}(d,o,l),u="/jetpack"===d?"wp-admin":"wc-admin",p="currency"===l.format,v=Object(i.calculateDelta)(l.value,A.value);return{primaryValue:p?r(l.value):Object(i.formatValue)(c,l.format,l.value),secondaryValue:p?r(A.value):Object(i.formatValue)(c,A.format,A.value),delta:v,reportUrl:b,reportUrlType:u}},d=(e,t,a,c)=>{const{getReportItems:n,getReportItemsError:s,isResolving:i}=e(l.REPORTS_STORE_NAME),{woocommerce_default_date_range:m}=e(l.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings"),A=Object(o.getCurrentDates)(a,m),d=A.primary.before,b=A.secondary.before,u=t.map(e=>e.stat).join(","),p=Object(l.getFilterQuery)({filters:c,query:a}),v={...p,after:Object(o.appendTimestamp)(A.primary.after,"start"),before:Object(o.appendTimestamp)(d,d.isSame(r()(),"day")?"now":"end"),stats:u},O={...p,after:Object(o.appendTimestamp)(A.secondary.after,"start"),before:Object(o.appendTimestamp)(b,b.isSame(r()(),"day")?"now":"end"),stats:u};return{primaryData:n("performance-indicators",v),primaryError:s("performance-indicators",v)||null,primaryRequesting:i("getReportItems",["performance-indicators",v]),secondaryData:n("performance-indicators",O),secondaryError:s("performance-indicators",O)||null,secondaryRequesting:i("getReportItems",["performance-indicators",O]),defaultDateRange:m}}},540:function(e,t,a){"use strict";var c=a(0);a(541);t.a=e=>{const{numTasks:t=5}=e;return Object(c.createElement)("div",{className:"woocommerce-task-dashboard__container"},Object(c.createElement)("div",{className:"woocommerce-card woocommerce-task-card is-loading","aria-hidden":!0},Object(c.createElement)("div",{className:"woocommerce-card__header"},Object(c.createElement)("div",{className:"woocommerce-card__title-wrapper"},Object(c.createElement)("div",{className:"woocommerce-card__title woocommerce-card__header-item"},Object(c.createElement)("span",{className:"is-placeholder"})))),Object(c.createElement)("div",{className:"woocommerce-card__body"},Object(c.createElement)("div",{className:"woocommerce-list"},Array.from(new Array(t)).map((e,t)=>Object(c.createElement)("div",{key:t,className:"woocommerce-list__item has-action"},Object(c.createElement)("div",{className:"woocommerce-list__item-inner"},Object(c.createElement)("div",{className:"woocommerce-list__item-before"},Object(c.createElement)("span",{className:"is-placeholder"})),Object(c.createElement)("div",{className:"woocommerce-list__item-text"},Object(c.createElement)("div",{className:"woocommerce-list__item-title"},Object(c.createElement)("span",{className:"is-placeholder"}))),Object(c.createElement)("div",{className:"woocommerce-list__item-after"},Object(c.createElement)("span",{className:"is-placeholder"})))))))))}},541:function(e,t,a){},543:function(e,t,a){"use strict";a.d(t,"b",(function(){return k})),a.d(t,"a",(function(){return B}));var c=a(0),r=a(2),n=a(30),o=a(3),l=a(8),s=Object(c.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(l.Path,{fillRule:"evenodd",d:"M6.863 13.644L5 13.25h-.5a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5H5L18 6.5h2V16h-2l-3.854-.815.026.008a3.75 3.75 0 01-7.31-1.549zm1.477.313a2.251 2.251 0 004.356.921l-4.356-.921zm-2.84-3.28L18.157 8h.343v6.5h-.343L5.5 11.823v-1.146z",clipRule:"evenodd"})),i=a(499),m=Object(c.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 24 24"},Object(c.createElement)(l.Path,{d:"M18.33 3.57s.27-.8-.31-1.36c-.53-.52-1.22-.24-1.22-.24-.61.3-5.76 3.47-7.67 5.57-.86.96-2.06 3.79-1.09 4.82.92.98 3.96-.17 4.79-1 2.06-2.06 5.21-7.17 5.5-7.79zM1.4 17.65c2.37-1.56 1.46-3.41 3.23-4.64.93-.65 2.22-.62 3.08.29.63.67.8 2.57-.16 3.46-1.57 1.45-4 1.55-6.15.89z"})),A=Object(c.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(l.Path,{d:"M12 4L4 7.9V20h16V7.9L12 4zm6.5 14.5H14V13h-4v5.5H5.5V8.8L12 5.7l6.5 3.1v9.7z"})),d=Object(c.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(l.Path,{d:"M20.1 5.1L16.9 2 6.2 12.7l-1.3 4.4 4.5-1.3L20.1 5.1zM4 20.8h8v-1.5H4v1.5z"})),b=Object(c.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(l.Path,{fillRule:"evenodd",d:"M5.5 9.5v-2h13v2h-13zm0 3v4h13v-4h-13zM4 7a1 1 0 011-1h14a1 1 0 011 1v10a1 1 0 01-1 1H5a1 1 0 01-1-1V7z",clipRule:"evenodd"})),u=Object(c.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(l.Path,{fillRule:"evenodd",d:"M6.5 8a1.5 1.5 0 103 0 1.5 1.5 0 00-3 0zM8 5a3 3 0 100 6 3 3 0 000-6zm6.5 11a1.5 1.5 0 103 0 1.5 1.5 0 00-3 0zm1.5-3a3 3 0 100 6 3 3 0 000-6zM5.47 17.41a.75.75 0 001.06 1.06L18.47 6.53a.75.75 0 10-1.06-1.06L5.47 17.41z",clipRule:"evenodd"})),p=Object(c.createElement)(l.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(l.Path,{d:"M3 6.75C3 5.784 3.784 5 4.75 5H15V7.313l.05.027 5.056 2.73.394.212v3.468a1.75 1.75 0 01-1.75 1.75h-.012a2.5 2.5 0 11-4.975 0H9.737a2.5 2.5 0 11-4.975 0H3V6.75zM13.5 14V6.5H4.75a.25.25 0 00-.25.25V14h.965a2.493 2.493 0 011.785-.75c.7 0 1.332.287 1.785.75H13.5zm4.535 0h.715a.25.25 0 00.25-.25v-2.573l-4-2.16v4.568a2.487 2.487 0 011.25-.335c.7 0 1.332.287 1.785.75zM6.282 15.5a1.002 1.002 0 00.968 1.25 1 1 0 10-.968-1.25zm9 0a1 1 0 101.937.498 1 1 0 00-1.938-.498z"})),v=a(13),O=a(16),w=a(20);a(558),a(559);const f=({title:e,children:t})=>Object(c.createElement)("div",{className:"woocommerce-quick-links__category"},Object(c.createElement)("h3",{className:"woocommerce-quick-links__category-header"},e),t);var h=a(116),j=a(500),E=a(21);a(560);const g=({icon:e,title:t,href:a,linkType:r,onClick:n})=>{const o="external"===r;return Object(c.createElement)("div",{className:"woocommerce-quick-links__item"},Object(c.createElement)(E.Link,{onClick:n,href:a,type:r,target:o?"_blank":null,className:"woocommerce-quick-links__item-link"},Object(c.createElement)(h.a,{className:"woocommerce-quick-links__item-link__icon",icon:e}),Object(c.createElement)(w.Text,{className:"woocommerce-quick-links__item-link__text",as:"div",variant:"button",weight:"600",size:"14",lineHeight:"20px"},t),o&&Object(c.createElement)(h.a,{icon:j.a})))};function k({path:e,tab:t=null,type:a,href:c=null}){return{"wc-admin":{href:"admin.php?page=wc-admin&path=%2F"+e,linkType:"wc-admin"},"wp-admin":{href:e,linkType:"wp-admin"},"wc-settings":{href:"admin.php?page=wc-settings&tab="+t,linkType:"wp-admin"}}[a]||{href:c,linkType:"external"}}const B=()=>{const e=Object(v.f)("shopUrl"),t=Object(n.applyFilters)("woocommerce_admin_homescreen_quicklinks",[]).reduce((e,{icon:t,href:a,title:c})=>(new URL(a,window.location.href).origin===window.location.origin&&e.push({icon:t,link:{href:a,linkType:"wp-admin"},title:c,listItemTag:"quick-links-extension-link"}),e),[]);const a=function(e){return[{title:Object(r.__)("Marketing & Merchandising",'woocommerce'),items:[{title:Object(r.__)("Marketing",'woocommerce'),link:k({type:"wc-admin",path:"marketing"}),icon:s,listItemTag:"marketing"},{title:Object(r.__)("Add products",'woocommerce'),link:k({type:"wp-admin",path:"post-new.php?post_type=product"}),icon:i.a,listItemTag:"add-products"},{title:Object(r.__)("Personalize my store",'woocommerce'),link:k({type:"wp-admin",path:"customize.php"}),icon:m,listItemTag:"personalize-store"},{title:Object(r.__)("View my store",'woocommerce'),link:k({type:"external",href:e}),icon:A,listItemTag:"view-store"}]},{title:Object(r.__)("Settings",'woocommerce'),items:[{title:Object(r.__)("Store details",'woocommerce'),link:k({type:"wc-settings",tab:"general"}),icon:d,listItemTag:"edit-store-details"},{title:Object(r.__)("Payments",'woocommerce'),link:k({type:"wc-settings",tab:"checkout"}),icon:b,listItemTag:"payment-settings"},{title:Object(r.__)("Tax",'woocommerce'),link:k({type:"wc-settings",tab:"tax"}),icon:u,listItemTag:"tax-settings"},{title:Object(r.__)("Shipping",'woocommerce'),link:k({type:"wc-settings",tab:"shipping"}),icon:p,listItemTag:"shipping-settings"}]}]}(e),l={title:Object(r.__)("Extensions",'woocommerce'),items:t},h=t.length?[...a,l]:a;return Object(c.createElement)(o.Card,{size:"medium"},Object(c.createElement)(o.CardHeader,{size:"medium"},Object(c.createElement)(w.Text,{variant:"title.small",size:"20",lineHeight:"28px"},Object(r.__)("Store management",'woocommerce'))),Object(c.createElement)(o.CardBody,{size:"custom",className:"woocommerce-store-management-links__card-body"},h.map(e=>Object(c.createElement)(f,{key:e.title,title:e.title},e.items.map(({icon:e,listItemTag:t,title:a,link:{href:r,linkType:n}})=>Object(c.createElement)(g,{icon:e,key:`${a}_${t}_${r}`,title:a,linkType:n,href:r,onClick:()=>{Object(O.recordEvent)("home_quick_links_click",{task_name:t})}}))))))}},549:function(e,t,a){},550:function(e,t,a){},551:function(e,t,a){"use strict";var c=Object.assign||function(e){for(var t,a=1;ag("orders_manage"),className:"woocommerce-layout__activity-panel-outbound-link woocommerce-layout__activity-panel-empty",type:"wp-admin"},Object(u.__)("Manage all orders",'woocommerce')));const t=e=>{const{first_name:t,last_name:a}=e.customer||{};if(!t&&!a)return"";return`{{customerLink}}${[t,a].join(" ")}{{/customerLink}}`},a=e=>{const{id:a,number:r,customer:n}=e;let o=null;return n&&n.customer_id&&(o=window.wcAdminFeatures.analytics?Object(i.getNewPath)({},"/analytics/customers",{filter:"single_customer",customers:n.customer_id}):Object(l.e)("user-edit.php?user_id="+n.customer_id)),Object(c.createElement)(c.Fragment,null,j()({mixedString:Object(u.sprintf)(Object(u.__)("{{orderLink}}Order #%(orderNumber)s{{/orderLink}} %(customerString)s",'woocommerce'),{orderNumber:r,customerString:t(e)}),components:{orderLink:Object(c.createElement)(v.Link,{href:Object(l.e)("post.php?action=edit&post="+a),onClick:()=>g("order_number"),type:"wp-admin"}),destinationFlag:n&&n.country?Object(c.createElement)(v.Flag,{code:n&&n.country,round:!1}):null,customerLink:o?Object(c.createElement)(v.Link,{href:o,onClick:()=>g("customer_name"),type:"wc-admin"}):Object(c.createElement)("span",null)}}))},r=[];return e.forEach(e=>{const{date_created_gmt:t,products:n,id:o}=e,s=n?n.length:0;r.push(Object(c.createElement)(E.a,{key:o,className:"woocommerce-order-activity-card",title:a(e),date:t,onClick:({target:e})=>{g("orders_begin_fulfillment"),e.href||(window.location.href=Object(l.e)("post.php?action=edit&post="+o))},subtitle:Object(c.createElement)("div",null,Object(c.createElement)("span",null,Object(u.sprintf)(Object(u._n)("%d product","%d products",s,'woocommerce'),s)),Object(c.createElement)("span",null,e.total_formatted))},Object(c.createElement)(v.OrderStatus,{order:e,orderStatusMap:Object(l.f)("orderStatuses",{})})))}),Object(c.createElement)(c.Fragment,null,r,Object(c.createElement)(v.Link,{href:"edit.php?post_type=shop_order",className:"woocommerce-layout__activity-panel-outbound-link",onClick:()=>g("orders_manage"),type:"wp-admin"},Object(u.__)("Manage all orders",'woocommerce')))}function B({countUnreadOrders:e,orderStatuses:t}){const a=Object(c.useMemo)(()=>({page:1,per_page:5,status:t,_fields:["id","number","status","total_formatted","customer","products","customer_id","date_created_gmt"]}),[t]),{orders:r=[],isRequesting:o,isError:i}=Object(n.useSelect)(c=>{const{getItems:r,getItemsError:n,isResolving:o}=c(s.ITEMS_STORE_NAME);if(!t.length&&0===e)return{isRequesting:!1};const l=r("orders",a,null),i=o("getItems",["orders",a]);if(i||null===e||null===l)return{isError:Boolean(n("orders",a)),isRequesting:!0,orderStatuses:t};const m=l?Array.from(l.values()):l;return{orders:m,isError:Boolean(n("orders",m)),isRequesting:i,orderStatuses:t}});if(i){if(!t.length&&window.wcAdminFeatures.analytics)return Object(c.createElement)(v.EmptyContent,{title:Object(u.__)("You currently don't have any actionable statuses. To display orders here, select orders that require further review in settings.",'woocommerce'),actionLabel:Object(u.__)("Settings",'woocommerce'),actionURL:Object(l.e)("admin.php?page=wc-admin&path=/analytics/settings")});const e=Object(u.__)("There was an error getting your orders. Please try again.",'woocommerce'),a=Object(u.__)("Reload",'woocommerce'),r=()=>{window.location.reload()};return Object(c.createElement)(c.Fragment,null,Object(c.createElement)(v.EmptyContent,{title:e,actionLabel:a,actionURL:null,actionCallback:r}))}return Object(c.createElement)(c.Fragment,null,Object(c.createElement)(v.Section,null,o?Object(c.createElement)(E.b,{className:"woocommerce-order-activity-card",hasAction:!0,hasDate:!0,lines:1}):k(r)))}B.propTypes={isError:b.a.bool,isRequesting:b.a.bool,countUnreadOrders:b.a.number,orders:b.a.array.isRequired,orderStatuses:b.a.array},B.defaultProps={orders:[],isError:!1,isRequesting:!1};var S=B,y=a(27),N=a(9),C=a.n(N);class q extends c.Component{constructor(e){super(e),this.state={quantity:e.product.stock_quantity,editing:!1,edited:!1},this.beginEdit=this.beginEdit.bind(this),this.cancelEdit=this.cancelEdit.bind(this),this.onQuantityChange=this.onQuantityChange.bind(this),this.handleKeyDown=this.handleKeyDown.bind(this),this.onSubmit=this.onSubmit.bind(this)}recordStockEvent(e,t={}){Object(w.recordEvent)("activity_panel_stock_"+e,t)}beginEdit(){const{product:e}=this.props;this.setState({editing:!0,quantity:e.stock_quantity},()=>{this.quantityInput&&this.quantityInput.focus()}),this.recordStockEvent("update_stock")}cancelEdit(){const{product:e}=this.props;this.setState({editing:!1,quantity:e.stock_quantity}),this.recordStockEvent("cancel")}handleKeyDown(e){e.keyCode===y.ESCAPE&&this.cancelEdit()}onQuantityChange(e){this.setState({quantity:e.target.value})}async onSubmit(){const{product:e,updateProductStock:t,createNotice:a}=this.props,c=parseInt(this.state.quantity,10);if(e.stock_quantity===c)return void this.setState({editing:!1});this.setState({editing:!1,edited:!0});await t(e,c)?a("success",Object(u.sprintf)(Object(u.__)("%s stock updated",'woocommerce'),e.name),{actions:[{label:Object(u.__)("Undo",'woocommerce'),onClick:()=>{t(e,e.stock_quantity),this.recordStockEvent("undo")}}]}):a("error",Object(u.sprintf)(Object(u.__)("%s stock could not be updated",'woocommerce'),e.name)),this.recordStockEvent("save",{quantity:c})}getActions(){const{editing:e}=this.state;return e?[Object(c.createElement)(O.Button,{key:"save",type:"submit",isPrimary:!0},Object(u.__)("Save",'woocommerce')),Object(c.createElement)(O.Button,{key:"cancel",type:"reset"},Object(u.__)("Cancel",'woocommerce'))]:[Object(c.createElement)(O.Button,{key:"update",isSecondary:!0,onClick:this.beginEdit},Object(u.__)("Update stock",'woocommerce'))]}getBody(){const{product:e}=this.props,{editing:t,quantity:a}=this.state;return t?Object(c.createElement)(c.Fragment,null,Object(c.createElement)(O.BaseControl,{className:"woocommerce-stock-activity-card__edit-quantity"},Object(c.createElement)("input",{className:"components-text-control__input",type:"number",value:a,onKeyDown:this.handleKeyDown,onChange:this.onQuantityChange,ref:e=>{this.quantityInput=e}})),Object(c.createElement)("span",null,Object(u.__)("in stock",'woocommerce'))):Object(c.createElement)("span",{className:A()("woocommerce-stock-activity-card__stock-quantity",{"out-of-stock":e.stock_quantity<1})},Object(u.sprintf)(Object(u.__)("%d in stock",'woocommerce'),e.stock_quantity))}render(){const{product:e}=this.props,{edited:t,editing:a}=this.state,r=Object(l.f)("notifyLowStockAmount",0),n=Number.isFinite(e.low_stock_amount)?e.low_stock_amount:r,s=e.stock_quantity<=n,i=e.last_order_date?Object(u.sprintf)(Object(u.__)("Last ordered %s",'woocommerce'),C.a.utc(e.last_order_date).fromNow()):null;if(!s&&!t)return null;const m=Object(c.createElement)(v.Link,{href:"post.php?action=edit&post="+(e.parent_id||e.id),onClick:()=>this.recordStockEvent("product_name"),type:"wp-admin"},e.name);let d=null;"variation"===e.type&&(d=Object.values(e.attributes).map(e=>e.option).join(", "));const b=Object(o.get)(e,["images",0])||Object(o.get)(e,["image"]),p=A()("woocommerce-stock-activity-card__image-overlay__product",{"is-placeholder":!b||!b.src}),O=Object(c.createElement)("div",{className:"woocommerce-stock-activity-card__image-overlay"},Object(c.createElement)("div",{className:p},Object(c.createElement)(v.ProductImage,{product:e}))),w=A()("woocommerce-stock-activity-card",{"is-dimmed":!a&&!s}),f=Object(c.createElement)(E.a,{className:w,title:m,subtitle:d,icon:O,date:i,actions:this.getActions()},this.getBody());return a?Object(c.createElement)("form",{onReset:this.cancelEdit,onSubmit:this.onSubmit},f):f}}const V={page:1,per_page:5,status:"publish",_fields:["attributes","id","images","last_order_date","low_stock_amount","name","parent_id","stock_quantity","type"]};class z extends c.Component{constructor(e){super(e),this.updateStock=this.updateStock.bind(this)}async updateStock(e,t){const{invalidateResolution:a,updateProductStock:c,products:r}=this.props,n=await c(e,t);return n&&(a("getItems",["products/low-in-stock",V]),r.length<2&&a("getItemsTotalCount",["products",f.b,null])),n}renderProducts(){const{products:e,createNotice:t}=this.props;return e.map(e=>Object(c.createElement)(q,{key:e.id,product:e,updateProductStock:this.updateStock,createNotice:t}))}render(){const{countLowStockProducts:e,isError:t,isRequesting:a,products:r}=this.props;if(t){const e=Object(u.__)("There was an error getting your low stock products. Please try again.",'woocommerce'),t=Object(u.__)("Reload",'woocommerce'),a=()=>{window.location.reload()};return Object(c.createElement)(v.EmptyContent,{title:e,actionLabel:t,actionURL:null,actionCallback:a})}if(a||!r.length){const t=Math.min(5,null!=e?e:1),a=Array.from(new Array(t)).map((e,t)=>Object(c.createElement)(E.b,{key:t,className:"woocommerce-stock-activity-card",hasAction:!0,lines:1}));return Object(c.createElement)(v.Section,null,a)}return Object(c.createElement)(v.Section,null,this.renderProducts())}}z.propTypes={countLowStockProducts:b.a.number,products:b.a.array.isRequired,isError:b.a.bool,isRequesting:b.a.bool},z.defaultProps={products:[],isError:!1,isRequesting:!1};var U=Object(r.compose)(Object(n.withSelect)(e=>{const{getItems:t,getItemsError:a,isResolving:c}=e(s.ITEMS_STORE_NAME);return{products:Array.from(t("products/low-in-stock",V).values()),isError:Boolean(a("products/low-in-stock",V)),isRequesting:c("getItems",["products/low-in-stock",V])}}),Object(n.withDispatch)(e=>{const{invalidateResolution:t,updateProductStock:a}=e(s.ITEMS_STORE_NAME),{createNotice:c}=e("core/notices");return{createNotice:c,invalidateResolution:t,updateProductStock:a}}))(z),M=a(167),Q=a.n(M),X=a(551),T=a.n(X),P=(a(552),()=>Object(c.createElement)("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(c.createElement)("mask",{id:"mask0","mask-type":"alpha",maskUnits:"userSpaceOnUse",x:"1",y:"1",width:"14",height:"14"},Object(c.createElement)("path",{d:"M7.99992 1.33301C4.31992 1.33301 1.33325 4.31967 1.33325 7.99967C1.33325 11.6797 4.31992 14.6663 7.99992 14.6663C11.6799 14.6663 14.6666 11.6797 14.6666 7.99967C14.6666 4.31967 11.6799 1.33301 7.99992 1.33301ZM7.99992 13.333C5.05992 13.333 2.66659 10.9397 2.66659 7.99967C2.66659 5.05967 5.05992 2.66634 7.99992 2.66634C10.9399 2.66634 13.3333 5.05967 13.3333 7.99967C13.3333 10.9397 10.9399 13.333 7.99992 13.333ZM6.66658 9.44634L11.0599 5.05301L11.9999 5.99967L6.66658 11.333L3.99992 8.66634L4.93992 7.72634L6.66658 9.44634Z",fill:"white"})),Object(c.createElement)("g",{mask:"url(#mask0)"},Object(c.createElement)("rect",{width:"16",height:"16",fill:"#4AB866"})))),J=a(501),I=a(509),H=a(103);const W={page:1,per_page:H.a,status:"hold",_embed:1};class D extends c.Component{recordReviewEvent(e,t){Object(w.recordEvent)("reviews_"+e,t||{})}deleteReview(e){const{deleteReview:t,createNotice:a,updateReview:c,clearReviewsCache:r}=this.props;e&&t(e).then(()=>{r(),a("success",Object(u.__)("Review successfully deleted.",'woocommerce'),{actions:[{label:Object(u.__)("Undo",'woocommerce'),onClick:()=>{c(e,{status:"untrash"},{_embed:1}).then(()=>r())}}]})}).catch(()=>{a("error",Object(u.__)("Review could not be deleted.",'woocommerce'))})}updateReviewStatus(e,t,a){const{createNotice:c,updateReview:r,clearReviewsCache:n}=this.props;e&&r(e,{status:t}).then(()=>{n(),c("success",Object(u.__)("Review successfully updated.",'woocommerce'),{actions:[{label:Object(u.__)("Undo",'woocommerce'),onClick:()=>{r(e,{status:a},{_embed:1}).then(()=>n())}}]})}).catch(()=>{c("error",Object(u.__)("Review could not be updated.",'woocommerce'))})}renderReview(e){const t=e&&e._embedded&&e._embedded.up&&e._embedded.up[0]||null;if(e.isUpdating)return Object(c.createElement)(E.b,{key:e.id,className:"woocommerce-review-activity-card",hasAction:!0,hasDate:!0,lines:1});if(Object(o.isNull)(t)||e.status!==W.status)return null;const a=j()({mixedString:Object(u.sprintf)(Object(u.__)("{{authorLink}}%s{{/authorLink}}{{verifiedCustomerIcon/}} reviewed {{productLink}}%s{{/productLink}}",'woocommerce'),e.reviewer,t.name),components:{productLink:Object(c.createElement)(v.Link,{href:t.permalink,onClick:()=>this.recordReviewEvent("product"),type:"external"}),authorLink:Object(c.createElement)(v.Link,{href:Object(l.e)("admin.php?page=wc-admin&path=%2Fcustomers&search="+e.reviewer),onClick:()=>this.recordReviewEvent("customer"),type:"external"}),verifiedCustomerIcon:e.verified?Object(c.createElement)("span",{className:"woocommerce-review-activity-card__verified"},Object(c.createElement)(O.Tooltip,{text:Object(u.__)("Verified owner",'woocommerce')},Object(c.createElement)("span",null,Object(c.createElement)(P,null)))):null}}),r=Object(c.createElement)(c.Fragment,null,Object(c.createElement)(v.ReviewRating,{review:e,icon:T.a,outlineIcon:Q.a,size:13})),n=Object(o.get)(t,["images",0])||Object(o.get)(t,["image"]),s=A()("woocommerce-review-activity-card__image-overlay__product",{"is-placeholder":!n||!n.src}),i=Object(c.createElement)("div",{className:"woocommerce-review-activity-card__image-overlay"},Object(c.createElement)("div",{className:s},Object(c.createElement)(v.ProductImage,{product:t,width:33,height:33}))),m={date:e.date_created_gmt,status:e.status},d=[Object(c.createElement)(O.Button,{key:"approve-action",isSecondary:!0,onClick:()=>{this.recordReviewEvent("approve",m),this.updateReviewStatus(e.id,"approved",e.status)}},Object(u.__)("Approve",'woocommerce')),Object(c.createElement)(O.Button,{key:"spam-action",isTertiary:!0,onClick:()=>{this.recordReviewEvent("mark_as_spam",m),this.updateReviewStatus(e.id,"spam",e.status)}},Object(u.__)("Mark as spam",'woocommerce')),Object(c.createElement)(O.Button,{key:"delete-action",isDestructive:!0,isTertiary:!0,onClick:()=>{this.recordReviewEvent("delete",m),this.deleteReview(e.id)}},Object(u.__)("Delete",'woocommerce'))];return Object(c.createElement)(E.a,{className:"woocommerce-review-activity-card",key:e.id,title:a,subtitle:r,date:e.date_created_gmt,icon:i,actions:d},Object(c.createElement)("span",{dangerouslySetInnerHTML:Object(I.a)(e.review)}))}renderReviews(e){const t=e.map(e=>this.renderReview(e,this.props));return 0===t.filter(Boolean).length?Object(c.createElement)(c.Fragment,null):Object(c.createElement)(c.Fragment,null,t,Object(c.createElement)(v.Link,{href:Object(l.e)("edit-comments.php?comment_type=review"),onClick:()=>this.recordReviewEvent("reviews_manage"),className:"woocommerce-layout__activity-panel-outbound-link woocommerce-layout__activity-panel-empty",type:"wp-admin"},Object(u.__)("Manage all reviews",'woocommerce')))}render(){const{isRequesting:e,isError:t,reviews:a}=this.props;if(t){const e=Object(u.__)("There was an error getting your reviews. Please try again.",'woocommerce'),t=Object(u.__)("Reload",'woocommerce'),a=()=>{window.location.reload()};return Object(c.createElement)(c.Fragment,null,Object(c.createElement)(v.EmptyContent,{title:e,actionLabel:t,actionURL:null,actionCallback:a}))}return Object(c.createElement)(c.Fragment,null,Object(c.createElement)(v.Section,null,e||!a.length?Object(c.createElement)(E.b,{className:"woocommerce-review-activity-card",hasAction:!0,hasDate:!0,lines:1}):Object(c.createElement)(c.Fragment,null,this.renderReviews(a))))}}D.propTypes={reviews:b.a.array.isRequired,isError:b.a.bool,isRequesting:b.a.bool},D.defaultProps={reviews:[],isError:!1,isRequesting:!1},D.contextType=J.a;var x=Object(r.compose)([Object(n.withSelect)((e,t)=>{const{hasUnapprovedReviews:a}=t,{getReviews:c,getReviewsError:r,isResolving:n}=e(s.REVIEWS_STORE_NAME);let o=[],l=!1,i=!1;return a&&(o=c(W),l=Boolean(r(W)),i=n("getReviews",[W])),{reviews:o,isError:l,isRequesting:i}}),Object(n.withDispatch)((e,t)=>{const{deleteReview:a,updateReview:c,invalidateResolution:r}=e(s.REVIEWS_STORE_NAME),{createNotice:n}=e("core/notices");return{deleteReview:a,createNotice:n,updateReview:c,clearReviewsCache:()=>{r("getReviews",[W]),t.reviews&&t.reviews.length<2&&r("getReviewsTotalCount",[H.c])}}})])(D);var F=a(517);const L=()=>{const e=Object(n.useSelect)(e=>{const t=Object(l.f)("orderCount",0),a=Object(f.c)(e),c=Object(l.f)("reviewsEnabled","no"),r=Object(f.d)(e,a),n=Object(l.f)("manageStock","no"),o=Object(f.a)(e),i=Object(H.b)(e),m=Object(l.f)("publishedProductCount",0),{getOption:A}=e(s.OPTIONS_STORE_NAME);return{countLowStockProducts:o,countUnapprovedReviews:i,countUnreadOrders:r,isTaskListHidden:A("woocommerce_task_list_hidden"),manageStock:n,publishedProductCount:m,reviewsEnabled:c,totalOrderCount:t,orderStatuses:a}}),t=function({countLowStockProducts:e,countUnapprovedReviews:t,countUnreadOrders:a,isTaskListHidden:r,manageStock:n,orderStatuses:o,publishedProductCount:l,reviewsEnabled:s,totalOrderCount:i}){return"yes"!==r?[]:[i>0&&{className:"woocommerce-homescreen-card",count:a,collapsible:!0,id:"orders-panel",initialOpen:!1,panel:Object(c.createElement)(S,{countUnreadOrders:a,orderStatuses:o}),title:Object(u.__)("Orders",'woocommerce')},i>0&&l>0&&"yes"===n&&{className:"woocommerce-homescreen-card",count:e,id:"stock-panel",initialOpen:!1,collapsible:0!==e,panel:Object(c.createElement)(U,{countLowStockProducts:e}),title:Object(u.__)("Stock",'woocommerce')},l>0&&"yes"===s&&{className:"woocommerce-homescreen-card",id:"reviews-panel",count:t,initialOpen:!1,collapsible:0!==t,panel:Object(c.createElement)(x,{hasUnapprovedReviews:t>0}),title:Object(u.__)("Reviews",'woocommerce')}].filter(Boolean)}(e);if(Object(c.useEffect)(()=>{if(void 0!==e.isTaskListHidden){const a=t.reduce((e,t)=>(e[Object(o.snakeCase)(t.id)]=!0,e),{task_list:"yes"!==e.isTaskListHidden});Object(w.recordEvent)("activity_panel_visible_panels",a)}},[e.isTaskListHidden]),0===t.length)return null;const a=e=>{const{opened_panel:t}=Object(F.b)(window.location.search);return e===t};return Object(c.createElement)(O.Panel,{className:"woocommerce-activity-panel"},t.map(e=>{const{className:t,count:r,id:n,initialOpen:o,panel:l,title:s,collapsible:i}=e;return i?Object(c.createElement)(O.PanelBody,{title:[Object(c.createElement)(O.__experimentalText,{key:s,variant:"title.small",size:"20",lineHeight:"28px"},s),null!==r&&Object(c.createElement)(v.Badge,{key:s+"-badge",count:r})],key:n,className:t,initialOpen:a(n)||o,collapsible:i,disabled:!i,onToggle:e=>{e&&Object(w.recordEvent)("activity_panel_open",{tab:n})}},Object(c.createElement)(O.PanelRow,null,l)):Object(c.createElement)("div",{className:"components-panel__body",key:n},Object(c.createElement)("h2",{className:"components-panel__body-title"},Object(c.createElement)(O.Button,{className:"components-panel__body-toggle","aria-expanded":!1,disabled:!0},Object(c.createElement)(O.__experimentalText,{variant:"title.small",size:"20",lineHeight:"28px"},s),null!==r&&Object(c.createElement)(v.Badge,{count:r}))))}))},R=({children:e,shouldStick:t=!1})=>{const[a,r]=Object(c.useState)(!1),n=Object(c.useRef)(null),o=Object(c.useRef)(null),l=Object(c.useCallback)(()=>{if(!n.current)return;const{bottom:e,top:t}=n.current.getBoundingClientRect();null===o.current&&(o.current=t);const a=e{if(t)return l(),window.addEventListener("resize",l),window.addEventListener("scroll",l),()=>{window.removeEventListener("resize",l),window.removeEventListener("scroll",l)}},[l,t]),Object(c.createElement)("div",{className:"woocommerce-homescreen-column",ref:n,style:{position:t&&a?"sticky":"static"}},e)};var _=a(523),G=a(20),K=(a(553),a(554)),Z=a.n(K),Y=a(555),$=a.n(Y),ee=a(556),te=a.n(ee);const ae=()=>{const[e,t]=Object(c.useState)(!0),{updateOptions:a}=Object(n.useDispatch)(s.OPTIONS_STORE_NAME),{isDismissed:r,isResolving:o,isWelcomeModalShown:l}=Object(n.useSelect)(e=>{const{getOption:t,isResolving:a}=e(s.OPTIONS_STORE_NAME),c=t("woocommerce_navigation_intro_modal_dismissed");return{isDismissed:"yes"===c,isWelcomeModalShown:"yes"!==t("woocommerce_task_list_welcome_modal_dismissed"),isResolving:void 0===c||a("getOption",["woocommerce_navigation_intro_modal_dismissed"])||a("getOption",["woocommerce_task_list_welcome_modal_dismissed"])}}),i=()=>{a({woocommerce_navigation_intro_modal_dismissed:"yes"}),Object(w.recordEvent)("navigation_intro_modal_close",{}),t(!1)};if(Object(c.useEffect)(()=>{o||!l||r||i()},[r,o,l]),!e||r||o||l)return null;const m=(e,t,a)=>({content:Object(c.createElement)("div",{className:"woocommerce-navigation-intro-modal__page-wrapper"},Object(c.createElement)("div",{className:"woocommerce-navigation-intro-modal__page-text"},Object(c.createElement)(G.Text,{variant:"title.large",as:"h2",size:"32",lineHeight:"40px"},e),Object(c.createElement)(G.Text,{as:"p",variant:"body.large",size:"16",lineHeight:"24px"},t)),Object(c.createElement)("div",{className:"woocommerce-navigation-intro-modal__image-wrapper"},Object(c.createElement)("img",{alt:e,src:a})))});return Object(c.createElement)(O.Guide,{className:"woocommerce-navigation-intro-modal",onFinish:i,pages:[m(Object(u.__)("A new navigation for WooCommerce",'woocommerce'),Object(u.__)("All of your store management features in one place",'woocommerce'),Z.a),m(Object(u.__)("Focus on managing your store",'woocommerce'),Object(u.__)("Give your attention to key areas of WooCommerce with little distraction",'woocommerce'),$.a),m(Object(u.__)("Easily find and favorite your extensions",'woocommerce'),Object(u.__)("They'll appear in the top level of the navigation for quick access",'woocommerce'),te.a)]})};var ce=a(122),re=(a(557),a(30));const ne=Object(re.applyFilters)("woocommerce_admin_homepage_default_stats",["revenue/total_sales","revenue/net_revenue","orders/orders_count","products/items_sold","jetpack/stats/visitors","jetpack/stats/views"]),oe=["revenue/net_revenue","products/items_sold"];var le=a(525);var se=Object(n.withSelect)((e,{stats:t,query:a})=>{if(0!==t.length)return Object(le.a)(e,t,a)})(({stats:e,primaryData:t,secondaryData:a,primaryRequesting:r,secondaryRequesting:n,primaryError:o,secondaryError:l,query:s})=>{const{formatAmount:m,getCurrencyConfig:d}=Object(c.useContext)(J.a);if(o||l)return null;const b=Object(i.getPersistedQuery)(s),p=d();return Object(c.createElement)("ul",{className:A()("woocommerce-stats-overview__stats",{"is-even":e.length%2==0})},e.map(e=>{if(r||n)return Object(c.createElement)(v.SummaryNumberPlaceholder,{key:e.stat});const{primaryValue:o,secondaryValue:l,delta:s,reportUrl:i,reportUrlType:A}=Object(le.b)({indicator:e,primaryData:t,secondaryData:a,currency:p,formatAmount:m,persistedQuery:b});return Object(c.createElement)(v.SummaryNumber,{isHomescreen:!0,key:e.stat,href:i,hrefType:A,label:e.label,value:o,prevLabel:Object(u.__)("Previous period:",'woocommerce'),prevValue:l,delta:s,onLinkClickCallback:()=>{Object(w.recordEvent)("statsoverview_indicators_click",{key:e.stat})}})}))}),ie=a(10);Object(u.__)("Facebook for WooCommerce",'woocommerce'),Object(u.__)("Jetpack",'woocommerce'),Object(u.__)("Klarna Checkout for WooCommerce",'woocommerce'),Object(u.__)("Klarna Payments for WooCommerce",'woocommerce'),Object(u.__)("Mailchimp for WooCommerce",'woocommerce'),Object(u.__)("Creative Mail for WooCommerce",'woocommerce'),Object(u.__)("WooCommerce PayPal",'woocommerce'),Object(u.__)("WooCommerce Stripe",'woocommerce'),Object(u.__)("WooCommerce PayFast",'woocommerce'),Object(u.__)("WooCommerce Payments",'woocommerce'),Object(u.__)("WooCommerce Shipping & Tax",'woocommerce'),Object(u.__)("WooCommerce Shipping & Tax",'woocommerce'),Object(u.__)("WooCommerce Shipping & Tax",'woocommerce'),Object(u.__)("WooCommerce ShipStation Gateway",'woocommerce'),Object(u.__)("Mercado Pago payments for WooCommerce",'woocommerce'),Object(u.__)("Google Listings and Ads",'woocommerce'),Object(u.__)("Razorpay",'woocommerce'),Object(u.__)("MailPoet",'woocommerce');let me;!function(e){e.UPDATE_ACTIVE_PLUGINS="UPDATE_ACTIVE_PLUGINS",e.UPDATE_INSTALLED_PLUGINS="UPDATE_INSTALLED_PLUGINS",e.SET_IS_REQUESTING="SET_IS_REQUESTING",e.SET_ERROR="SET_ERROR",e.UPDATE_JETPACK_CONNECTION="UPDATE_JETPACK_CONNECTION",e.UPDATE_JETPACK_CONNECT_URL="UPDATE_JETPACK_CONNECT_URL",e.SET_PAYPAL_ONBOARDING_STATUS="SET_PAYPAL_ONBOARDING_STATUS",e.SET_RECOMMENDED_PLUGINS="SET_RECOMMENDED_PLUGINS"}(me||(me={}));const Ae=n.controls&&n.controls.dispatch?n.controls.dispatch:ie.dispatch;n.controls&&n.controls.resolveSelect?n.controls.resolveSelect:ie.select;const de=e=>Ae("core/notices","createNotice","error",e);const be=({onClickInstall:e,onClickDismiss:t,isBusy:a,jetpackInstallState:r})=>Object(c.createElement)("article",{className:"woocommerce-stats-overview__install-jetpack-promo"},Object(c.createElement)("div",{className:"woocommerce-stats-overview__install-jetpack-promo__content"},Object(c.createElement)(v.H,null,Object(u.__)("Get traffic stats with Jetpack",'woocommerce')),Object(c.createElement)("p",null,Object(u.__)("Keep an eye on your views and visitors metrics with Jetpack. Requires Jetpack plugin and a WordPress.com account.",'woocommerce'))),Object(c.createElement)("footer",null,Object(c.createElement)(O.Button,{isSecondary:!0,onClick:()=>{Object(w.recordEvent)("statsoverview_install_jetpack"),e()},disabled:a,isBusy:a},(e=>({unavailable:Object(u.__)("Get Jetpack",'woocommerce'),installed:Object(u.__)("Activate Jetpack",'woocommerce'),activated:Object(u.__)("Connect Jetpack",'woocommerce')}[e]||""))(r)),Object(c.createElement)(O.Button,{isTertiary:!0,onClick:()=>{Object(w.recordEvent)("statsoverview_dismiss_install_jetpack"),t()},disabled:a,isBusy:a},Object(u.__)("No thanks",'woocommerce')))),ue=()=>{const{currentUserCan:e}=Object(s.useUser)(),{updateUserPreferences:t,...a}=Object(s.useUserPreferences)(),{canUserInstallPlugins:r,jetpackInstallState:o,isBusy:i}=Object(n.useSelect)(t=>{const{getPluginInstallState:a,isPluginsRequesting:c}=t(s.PLUGINS_STORE_NAME),r=a("jetpack");return{isBusy:c("getJetpackConnectUrl")||c("installPlugins")||c("activatePlugins"),jetpackInstallState:r,canUserInstallPlugins:e("install_plugins")}}),{installJetpackAndConnect:m}=Object(n.useDispatch)(s.PLUGINS_STORE_NAME);if(!r)return null;return Object(c.createElement)(be,{jetpackInstallState:o,isBusy:i,onClickInstall:()=>{m(de,l.e)},onClickDismiss:()=>{const e=a.homepage_stats||{};e.installJetpackDismissed=!0,t({homepage_stats:e})}})},{performanceIndicators:pe=[]}=Object(l.f)("dataEndpoints",{performanceIndicators:[]}),ve=pe.filter(e=>ne.includes(e.stat));var Oe=()=>{const{updateUserPreferences:e,...t}=Object(s.useUserPreferences)(),a=Object(o.get)(t,["homepage_stats","hiddenStats"],oe),r=Object(n.useSelect)(e=>e(s.PLUGINS_STORE_NAME).isJetpackConnected(),[]),l=(t.homepage_stats||{}).installJetpackDismissed,m=ve.filter(e=>!a.includes(e.stat)),A=Object(c.createElement)(G.Text,{variant:"title.small",size:"20",lineHeight:"28px"},Object(u.__)("Stats overview",'woocommerce'));return Object(c.createElement)(O.Card,{size:"large",className:"woocommerce-stats-overview woocommerce-homescreen-card"},Object(c.createElement)(O.CardHeader,{size:"medium"},Object(c.createElement)(ce.Experiment,{name:"woocommerce_test_experiment",defaultExperience:A,treatmentExperience:A,loadingExperience:A}),Object(c.createElement)(v.EllipsisMenu,{label:Object(u.__)("Choose which values to display",'woocommerce'),renderContent:()=>Object(c.createElement)(c.Fragment,null,Object(c.createElement)(v.MenuTitle,null,Object(u.__)("Display stats:",'woocommerce')),ve.map(t=>{const r=!a.includes(t.stat);return Object(c.createElement)(v.MenuItem,{checked:r,isCheckbox:!0,isClickable:!0,key:t.stat,onInvoke:()=>(t=>{const c=Object(o.xor)(a,[t]);e({homepage_stats:{hiddenStats:c}}),Object(w.recordEvent)("statsoverview_indicators_toggle",{indicator_name:t,status:c.includes(t)?"off":"on"})})(t.stat)},t.label)}))})),Object(c.createElement)(O.TabPanel,{className:"woocommerce-stats-overview__tabs",onSelect:e=>{Object(w.recordEvent)("statsoverview_date_picker_update",{period:e})},tabs:[{title:Object(u.__)("Today",'woocommerce'),name:"today"},{title:Object(u.__)("Week to date",'woocommerce'),name:"week"},{title:Object(u.__)("Month to date",'woocommerce'),name:"month"}]},e=>Object(c.createElement)(c.Fragment,null,!r&&!l&&Object(c.createElement)(ue,null),Object(c.createElement)(se,{query:{period:e.name,compare:"previous_period"},stats:m}))),Object(c.createElement)(O.CardFooter,null,Object(c.createElement)(v.Link,{className:"woocommerce-stats-overview__more-btn",href:Object(i.getNewPath)({},"/analytics/overview"),type:"wc-admin",onClick:()=>{Object(w.recordEvent)("statsoverview_indicators_click",{key:"view_detailed_stats"})}},Object(u.__)("View detailed stats",'woocommerce'))))},we=a(543),fe=a(540);const he=()=>Object(c.createElement)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"517",height:"160",fill:"none"},Object(c.createElement)("defs",null),Object(c.createElement)("g",{clipPath:"url(#clip0)"},Object(c.createElement)("path",{className:"fill-theme-color",d:"M0 0h517v160H0z"}),Object(c.createElement)("path",{fill:"#fff",fillOpacity:".65",d:"M125.85 217.924l-1.055-.321c-34.868-10.598-101.138-36.619-95.91-101.998 7.156-89.462 89.192-28.933 194.231-87.715 161.485-90.37 242.851-42.249 253.957 78.717 10.842 118.101-82.942 115.494-138.938 123.306-118.486 16.529-165.516 2.231-212.285-11.989z"}),Object(c.createElement)("path",{fill:"#F6F7F7",d:"M125 33h267v185H125z"}),Object(c.createElement)("path",{fill:"#CCC",d:"M327.367 93.974a6.417 6.417 0 00-6.285 7.671 6.418 6.418 0 005.035 5.044 6.405 6.405 0 006.579-2.73 6.427 6.427 0 00-.797-8.105 6.404 6.404 0 00-4.532-1.88zm0 11.615a5.18 5.18 0 01-3.668-1.522 5.2 5.2 0 01-1.23-5.38 5.196 5.196 0 014.168-3.447 5.18 5.18 0 014.967 2.137 5.201 5.201 0 01-1.546 7.453 5.186 5.186 0 01-2.706.75l.015.009z"}),Object(c.createElement)("path",{fill:"#CCC",d:"M329.332 103.181l.806-.811a.354.354 0 00.078-.391.365.365 0 00-.078-.116l-1.456-1.461 1.456-1.458a.363.363 0 00.105-.254.36.36 0 00-.105-.254l-.806-.81a.354.354 0 00-.254-.106.356.356 0 00-.255.106l-1.456 1.458-1.456-1.458a.35.35 0 00-.253-.105.355.355 0 00-.253.105l-.809.826a.362.362 0 00-.078.39.363.363 0 00.078.117l1.456 1.458-1.456 1.461a.369.369 0 00-.105.254.356.356 0 00.105.254l.809.81a.354.354 0 00.39.078.354.354 0 00.116-.078l1.456-1.461 1.456 1.461a.366.366 0 00.509-.015zM314.559 145.63a6.413 6.413 0 00-2.722-4.13 6.429 6.429 0 00-4.883-.957l-.192.046c-.346.08-.684.191-1.01.33a6.437 6.437 0 00-3.892 5.926 6.433 6.433 0 003.907 5.916l.183.074a6.402 6.402 0 007.999-2.997 6.423 6.423 0 00.735-3.001 6.196 6.196 0 00-.125-1.207zm-1.184 1.978a.028.028 0 010 .018v.058a5.213 5.213 0 01-.913 2.181 5.191 5.191 0 01-4.068 2.146 5.162 5.162 0 01-3.445-1.2 5.2 5.2 0 01.693-8.443 4.936 4.936 0 011.026-.464l.192-.058a5.176 5.176 0 014.527.859 5.201 5.201 0 012.058 4.129 4.906 4.906 0 01-.07.774z"}),Object(c.createElement)("path",{fill:"#CCC",d:"M310.223 149.613l.808-.81a.349.349 0 00.078-.116.348.348 0 000-.275.353.353 0 00-.078-.117l-1.455-1.458 1.455-1.458a.36.36 0 00.079-.393.36.36 0 00-.079-.117l-.808-.807a.362.362 0 00-.391-.078.349.349 0 00-.116.078l-1.455 1.464-1.465-1.464a.366.366 0 00-.254-.106.36.36 0 00-.253.106l-.809.807a.358.358 0 00-.078.393.358.358 0 00.078.117l1.459 1.458-1.459 1.458a.356.356 0 00-.078.392.382.382 0 00.078.116l.809.81a.365.365 0 00.253.106.366.366 0 00.254-.106l1.458-1.458 1.456 1.458a.353.353 0 00.513 0zM295.605 51.781l-.583-.587a.297.297 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372a.31.31 0 00.339.07.297.297 0 00.1-.07l4.465-4.439a.316.316 0 00.097-.22.313.313 0 00-.094-.223zm0 0l-.583-.587a.297.297 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372a.31.31 0 00.339.07.297.297 0 00.1-.07l4.465-4.439a.316.316 0 00.097-.22.313.313 0 00-.094-.223zm0 0l-.583-.587a.297.297 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372a.31.31 0 00.339.07.297.297 0 00.1-.07l4.465-4.439a.316.316 0 00.097-.22.313.313 0 00-.094-.223zm0 0l-.583-.587a.297.297 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372a.31.31 0 00.339.07.297.297 0 00.1-.07l4.465-4.439a.316.316 0 00.097-.22.313.313 0 00-.094-.223zm-3.628-4.619a6.402 6.402 0 00-5.921 3.963 6.432 6.432 0 001.389 6.996 6.404 6.404 0 009.86-.973 6.428 6.428 0 00-.797-8.106 6.403 6.403 0 00-4.531-1.88zm0 11.616a5.186 5.186 0 01-4.793-3.208 5.2 5.2 0 011.124-5.663 5.186 5.186 0 015.654-1.126 5.204 5.204 0 011.685 8.476 5.17 5.17 0 01-3.67 1.515v.006zm3.628-6.99l-.583-.588a.298.298 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372c.029.03.063.053.1.07a.31.31 0 00.239 0 .297.297 0 00.1-.07l4.465-4.438a.304.304 0 00.075-.347.31.31 0 00-.072-.103v.006zm0 0l-.583-.588a.298.298 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372c.029.03.063.053.1.07a.31.31 0 00.239 0 .297.297 0 00.1-.07l4.465-4.438a.304.304 0 00.075-.347.31.31 0 00-.072-.103v.006zm0 0l-.583-.588a.298.298 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372c.029.03.063.053.1.07a.31.31 0 00.239 0 .297.297 0 00.1-.07l4.465-4.438a.304.304 0 00.075-.347.31.31 0 00-.072-.103v.006zm0 0l-.583-.588a.298.298 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372c.029.03.063.053.1.07a.31.31 0 00.239 0 .297.297 0 00.1-.07l4.465-4.438a.304.304 0 00.075-.347.31.31 0 00-.072-.103v.006zm0 0l-.583-.588a.298.298 0 00-.219-.093.31.31 0 00-.22.093l-3.662 3.635-1.547-1.562a.308.308 0 00-.437 0l-.589.584a.313.313 0 00-.093.22.307.307 0 00.093.22l2.35 2.372c.029.03.063.053.1.07a.31.31 0 00.239 0 .297.297 0 00.1-.07l4.465-4.438a.304.304 0 00.075-.347.31.31 0 00-.072-.103v.006zM306.96 98.595l-.582-.59a.311.311 0 00-.22-.093.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.303.303 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.312.312 0 00.097-.22.308.308 0 00-.091-.22zm0 0l-.582-.59a.311.311 0 00-.22-.093.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.303.303 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.312.312 0 00.097-.22.308.308 0 00-.091-.22zm0 0l-.582-.59a.311.311 0 00-.22-.093.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.303.303 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.312.312 0 00.097-.22.308.308 0 00-.091-.22zm0 0l-.582-.59a.311.311 0 00-.22-.093.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.303.303 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.312.312 0 00.097-.22.308.308 0 00-.091-.22zm-3.628-4.621a6.417 6.417 0 00-6.285 7.671 6.412 6.412 0 005.035 5.044 6.401 6.401 0 006.578-2.73 6.42 6.42 0 00-.797-8.105 6.4 6.4 0 00-4.531-1.88zm0 11.615a5.18 5.18 0 01-4.793-3.208 5.201 5.201 0 013.781-7.085 5.179 5.179 0 015.326 2.21c.57.854.874 1.86.874 2.887a5.202 5.202 0 01-1.516 3.677 5.178 5.178 0 01-3.672 1.516v.003zm3.628-6.99l-.582-.59a.31.31 0 00-.22-.094.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.302.302 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.3.3 0 00.098-.22.304.304 0 00-.092-.223v.002zm0 0l-.582-.59a.31.31 0 00-.22-.094.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.302.302 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.3.3 0 00.098-.22.304.304 0 00-.092-.223v.002zm0 0l-.582-.59a.31.31 0 00-.22-.094.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.302.302 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.3.3 0 00.098-.22.304.304 0 00-.092-.223v.002zm0 0l-.582-.59a.31.31 0 00-.22-.094.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.302.302 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.3.3 0 00.098-.22.304.304 0 00-.092-.223v.002zm0 0l-.582-.59a.31.31 0 00-.22-.094.308.308 0 00-.22.093l-3.662 3.635-1.547-1.562a.31.31 0 00-.22-.094.302.302 0 00-.22.094l-.589.584a.31.31 0 000 .44l2.347 2.372c.029.03.063.053.101.069a.302.302 0 00.339-.069l4.467-4.438a.3.3 0 00.098-.22.304.304 0 00-.092-.223v.002zM287.774 145.407l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.309.309 0 00.097-.219.309.309 0 00-.091-.221zm0 0l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.309.309 0 00.097-.219.309.309 0 00-.091-.221zm0 0l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.309.309 0 00.097-.219.309.309 0 00-.091-.221zm0 0l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.309.309 0 00.097-.219.309.309 0 00-.091-.221zm-3.628-4.622a6.416 6.416 0 00-6.285 7.671 6.414 6.414 0 005.035 5.044 6.393 6.393 0 003.702-.365 6.418 6.418 0 003.957-5.931 6.43 6.43 0 00-1.877-4.539 6.403 6.403 0 00-4.532-1.88zm0 11.616a5.181 5.181 0 01-2.882-.876 5.2 5.2 0 011.87-9.418 5.186 5.186 0 015.326 2.21c.57.855.874 1.859.874 2.887a5.191 5.191 0 01-1.515 3.678 5.163 5.163 0 01-3.673 1.516v.003zm3.628-6.991l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.297.297 0 00.098-.22.293.293 0 00-.023-.121.284.284 0 00-.069-.102v.003zm0 0l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.297.297 0 00.098-.22.293.293 0 00-.023-.121.284.284 0 00-.069-.102v.003zm0 0l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.297.297 0 00.098-.22.293.293 0 00-.023-.121.284.284 0 00-.069-.102v.003zm0 0l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.297.297 0 00.098-.22.293.293 0 00-.023-.121.284.284 0 00-.069-.102v.003zm0 0l-.582-.59a.303.303 0 00-.101-.069.302.302 0 00-.339.069l-3.662 3.634-1.547-1.562a.31.31 0 00-.439 0l-.589.584a.301.301 0 00-.07.34c.017.038.04.072.07.1l2.346 2.372a.301.301 0 00.339.07.321.321 0 00.101-.07l4.467-4.438a.297.297 0 00.098-.22.293.293 0 00-.023-.121.284.284 0 00-.069-.102v.003zM349.568 75.187l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447zm0 0l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447zm0 0l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447zm0 0l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447zm-3.628-4.619a6.402 6.402 0 00-4.17 1.5 6.422 6.422 0 00-1.386 8.21 6.415 6.415 0 003.447 2.79 6.4 6.4 0 004.477-.092c.317-.126.624-.278.915-.456a6.418 6.418 0 002.93-7.236 6.422 6.422 0 00-2.309-3.413 6.4 6.4 0 00-3.904-1.303zm2.273 11.087a5.056 5.056 0 01-.665.272 5.213 5.213 0 01-3.406-.067 5.197 5.197 0 01-1.681-8.731 5.182 5.182 0 018.501 2.56 5.195 5.195 0 01-2.749 5.966zm1.355-6.468l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447zm0 0l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447zm0 0l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447zm0 0l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447zm0 0l-.583-.578a.298.298 0 00-.219-.093.306.306 0 00-.22.093l-1.904 1.895-.687.682-.058.055-.357.358-.638.632-1.547-1.562a.308.308 0 00-.44 0l-.589.584a.312.312 0 00-.093.22.307.307 0 00.093.22l2.216 2.241.131.132a.304.304 0 00.44.003l1.849-1.835.61-.61 2.002-1.99a.306.306 0 00-.006-.447z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M268.92 47H150.08c-3.358 0-6.08 2.91-6.08 6.5s2.722 6.5 6.08 6.5h118.84c3.358 0 6.08-2.91 6.08-6.5s-2.722-6.5-6.08-6.5z",opacity:".6"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M321.919 71H150.081c-3.359 0-6.081 2.686-6.081 6s2.722 6 6.081 6h171.838c3.359 0 6.081-2.686 6.081-6s-2.722-6-6.081-6z",opacity:".3"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M279.927 94H150.073c-3.354 0-6.073 2.91-6.073 6.5s2.719 6.5 6.073 6.5h129.854c3.354 0 6.073-2.91 6.073-6.5s-2.719-6.5-6.073-6.5z",opacity:".6"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M321.919 118H150.081c-3.359 0-6.081 2.686-6.081 6s2.722 6 6.081 6h171.838c3.359 0 6.081-2.686 6.081-6s-2.722-6-6.081-6z",opacity:".3"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M261.916 141H150.084c-3.36 0-6.084 2.686-6.084 6s2.724 6 6.084 6h111.832c3.36 0 6.084-2.686 6.084-6s-2.724-6-6.084-6z",opacity:".1"}),Object(c.createElement)("path",{fill:"#CCC",d:"M316.161 47.162a6.4 6.4 0 00-5.92 3.963 6.432 6.432 0 001.389 6.996 6.404 6.404 0 009.86-.973 6.428 6.428 0 00-.797-8.106 6.404 6.404 0 00-4.532-1.88zm0 11.616a5.18 5.18 0 01-2.882-.876 5.198 5.198 0 011.87-9.417 5.181 5.181 0 015.326 2.21c.57.854.874 1.859.874 2.887a5.195 5.195 0 01-3.201 4.8c-.63.26-1.305.392-1.987.39v.006z"}),Object(c.createElement)("path",{fill:"#CCC",d:"M318.127 56.366l.808-.807a.35.35 0 00.078-.117.346.346 0 000-.276.35.35 0 00-.078-.117l-1.458-1.458 1.455-1.458a.35.35 0 00.078-.117.346.346 0 000-.277.35.35 0 00-.078-.117l-.808-.807a.364.364 0 00-.254-.105.358.358 0 00-.253.105l-1.456 1.458-1.455-1.458a.361.361 0 00-.51 0l-.806.807a.365.365 0 00-.107.255.365.365 0 00.107.256l1.456 1.458-1.453 1.455a.365.365 0 00-.079.394.381.381 0 00.079.116l.806.807a.353.353 0 00.255.106.363.363 0 00.255-.106l1.455-1.458 1.456 1.458a.352.352 0 00.253.107.356.356 0 00.254-.104zM369.966 70.568a6.402 6.402 0 00-5.921 3.963 6.432 6.432 0 001.389 6.995 6.404 6.404 0 0010.94-4.539 6.403 6.403 0 00-3.953-5.935 6.383 6.383 0 00-2.455-.484zm0 11.616a5.179 5.179 0 01-3.17-1.076 5.203 5.203 0 01-1.621-6.136 5.187 5.187 0 015.512-3.13 5.186 5.186 0 012.985 1.519 5.2 5.2 0 01-1.158 8.146 5.18 5.18 0 01-2.548.674v.003z"}),Object(c.createElement)("path",{fill:"#CCC",d:"M371.925 79.772l.808-.807a.363.363 0 000-.51l-1.458-1.459 1.458-1.458a.348.348 0 00.078-.116.343.343 0 000-.275.346.346 0 00-.078-.116l-.808-.81a.358.358 0 00-.507 0l-1.452 1.458-1.456-1.458a.358.358 0 00-.507 0l-.808.81a.36.36 0 00-.078.391.348.348 0 00.078.116l1.455 1.458-1.455 1.458a.364.364 0 000 .51l.808.808a.35.35 0 00.507 0l1.456-1.458 1.458 1.458a.358.358 0 00.501 0z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M344 94h90v80h-90z"}),Object(c.createElement)("path",{fill:"#fff",fillOpacity:".65",d:"M364.607 150.419H357v25.307h7.607v-25.307zM379.317 132h-7.607v43.455h7.607V132zM394.026 136h-7.607v61.603h7.607V136zM408.736 123h-7.607v55.726h7.607V123zM423.445 132.197h-7.607v38.342h7.607v-38.342z",opacity:".2"}),Object(c.createElement)("path",{fill:"#fff",d:"M356.331 134l-.331-.495 15.486-21.052 13.65 14.005 11.039-17.456 4.84 5.868 13.168-11.268 14.625 14.021L451.763 99l.237.594-23.213 18.833-14.619-14.015-13.201 11.297-4.748-5.756-11.014 17.418-13.677-14.031L356.331 134z"})),Object(c.createElement)("defs",null,Object(c.createElement)("clipPath",{id:"clip0"},Object(c.createElement)("path",{fill:"#fff",d:"M0 0h517v160H0z"})))),je=({title:e,body:t})=>Object(c.createElement)("div",{className:"woocommerce__welcome-modal__page-content"},Object(c.createElement)("h2",{className:"woocommerce__welcome-modal__page-content__header"},e),Object(c.createElement)("p",{className:"woocommerce__welcome-modal__page-content__body"},t));a(561);const Ee={image:Object(c.createElement)(he,null),content:Object(c.createElement)(je,{title:Object(u.__)("Welcome to your new store management experience",'woocommerce'),body:j()({mixedString:Object(u.__)("We've designed your navigation and home screen to help you focus on the things that matter most in managing your online store. {{link}}Learn more{{/link}} about these changes – or explore on your own.",'woocommerce'),components:{link:Object(c.createElement)(v.Link,{href:"https://wordpress.com/support/new-woocommerce-experience-on-wordpress-dot-com/",type:"external",target:"_blank"})}})})};function ge({onClose:e}){const[t,a]=Object(c.useState)(!0);if(Object(c.useEffect)(()=>{Object(w.recordEvent)("welcome_from_calypso_modal_open")},[]),!t)return null;const r=A()("woocommerce__welcome-modal","woocommerce__welcome-from-calypso-modal");return Object(c.createElement)(O.Guide,{onFinish:()=>{e&&e(),a(!1),Object(w.recordEvent)("welcome_from_calypso_modal_close")},className:r,finishButtonText:Object(u.__)("Let's go",'woocommerce'),pages:[Ee]})}a(562);const ke=[{image:Object(c.createElement)(he,null),content:Object(c.createElement)(je,{title:Object(u.__)("Welcome to your WooCommerce store’s online HQ!",'woocommerce'),body:Object(u.__)("Here's where you’ll find setup suggestions, tips and tools, and key data on your store’s performance and earnings — all the basics for store management and growth.",'woocommerce')})},{image:Object(c.createElement)(()=>Object(c.createElement)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"517",height:"160",fill:"none"},Object(c.createElement)("defs",null),Object(c.createElement)("g",{clipPath:"url(#clip0)"},Object(c.createElement)("path",{className:"fill-theme-color",d:"M0 0h517v160H0z"}),Object(c.createElement)("path",{fill:"#fff",fillOpacity:".65",d:"M33.576 185.926c-6.271-.911-14.742-.279-17.182 7.085-1.239 3.736-.178 7.645.98 11.08 4.89 14.682 11.49 28.444 19.643 40.954 3.897 5.965 8.253 11.884 9.592 19.504 1.34 7.619-.56 16.084-2.934 23.945-5.595 18.62-13.762 36.371-24.188 52.572 16.006 9.711 34.165 19.634 52.684 12.57 11.09-4.232 21.041-14.268 32.365-15.961 7.562-1.132 14.735 1.648 21.594 4.467a998.376 998.376 0 0195.343 45.227c13.023 7.042 26.207 14.481 40.901 16.153 14.694 1.672 31.486-3.518 41.947-17.66 1.611-2.179 3.241-4.669 5.483-5.546 2.02-.776 4.069-.045 5.952.688l113.896 44.033c6.241 2.411 12.718 4.853 19.534 3.832 6.606-.985 12.833-5.095 18.858-9.148 13.771-9.237 29.242-21.105 32.239-39.005 2.407-14.347-4.339-27.253-11.974-37.283-7.636-10.03-16.705-19.204-20.353-32.315-5.549-19.955 2.798-42.949 9.281-64.164a405.4 405.4 0 0013.244-58.574c2.588-17.377 4.004-35.179.91-51.659-3.095-16.481-11.265-31.624-24.089-38.27-16.746-8.681-38.828-2.057-54.255-13.347-13.04-9.513-17.58-29.035-25.856-44.316-14.698-27.146-41.453-40.923-67.958-50.405-28.1-10.066-58.213-16.679-88.607-10-6.962 1.527-14.047 3.833-20.152 8.649-9.36 7.388-15.196 19.616-22.986 29.33C156.104 57.468 100.341 49.156 68.22 87.48c-11.398 13.594-17.581 31.878-18.797 49.831-1.31 19.318 8.69 33.652 8.706 50.888-7.135 2.277-17.21-1.211-24.553-2.273z"}),Object(c.createElement)("path",{fill:"#F6F7F7",d:"M113 33h267v185H113z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M248.466 73.79h-114.69V47.88h114.69V73.79zm-114.015-.673h113.341V48.554H134.451v24.563z"}),Object(c.createElement)("path",{fill:"#CCC",d:"M155.702 56.63h-12.818v12.786h12.818V56.63z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M154.016 67.733h-13.493V54.274h13.493v13.46zm-12.819-.673h12.144V54.947h-12.144V67.06z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M225.267 56.966h-50v.673h50v-.673z",opacity:".7"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M235.311 61.677h-60.044v.673h60.044v-.673z",opacity:".5"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M225.267 66.387h-50v.673h50v-.673z",opacity:".2"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M248.466 147.142h-114.69v-25.909h114.69v25.909zm-114.015-.673h113.341v-24.563H134.451v24.563z"}),Object(c.createElement)("path",{fill:"#CCC",d:"M155.702 129.981h-12.818v12.786h12.818v-12.786z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M154.016 141.085h-13.493v-13.459h13.493v13.459zm-12.819-.673h12.144v-12.113h-12.144v12.113z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M235.311 130.318h-60.044v.673h60.044v-.673z",opacity:".7"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M225.267 135.028h-50v.673h50v-.673z",opacity:".5"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M215.267 139.739h-40v.673h40v-.673z",opacity:".2"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M289.62 110.465H174.93V84.557h114.69v25.908zm-114.016-.672h113.341V85.23H175.604v24.563z"}),Object(c.createElement)("path",{fill:"#CCC",d:"M267.694 106.092h12.818V93.305h-12.818v12.787z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M282.873 104.409H269.38V90.95h13.493v13.459zm-12.818-.673h12.144V91.623h-12.144v12.113z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M248.129 93.642h-60.044v.673h60.044v-.673z",opacity:".7"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M238.085 98.353h-50v.672h50v-.672z",opacity:".5"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M243.085 103.063h-55v.673h55v-.673z",opacity:".2"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M266.035 66.154a5.363 5.363 0 005.369-5.356 5.363 5.363 0 00-5.369-5.356c-2.966 0-5.37 2.398-5.37 5.356 0 2.958 2.404 5.356 5.37 5.356zM273.793 140.515c2.966 0 5.37-2.398 5.37-5.356 0-2.958-2.404-5.356-5.37-5.356a5.363 5.363 0 00-5.369 5.356 5.363 5.363 0 005.369 5.356zM153.706 102.83a5.363 5.363 0 005.37-5.356c0-2.959-2.404-5.357-5.37-5.357s-5.37 2.398-5.37 5.357a5.363 5.363 0 005.37 5.356z",opacity:".5"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M401.276 172h-70.552a8.79 8.79 0 01-6.169-2.517 8.532 8.532 0 01-2.555-6.078V131.56a3.368 3.368 0 011.078-2.471l37.386-34.915A8.113 8.113 0 01366 92c2.06 0 4.041.778 5.536 2.174l35.645 33.289a8.882 8.882 0 012.084 2.944 8.78 8.78 0 01.735 3.515v29.483c0 2.28-.919 4.466-2.555 6.078a8.79 8.79 0 01-6.169 2.517z"}),Object(c.createElement)("path",{fill:"#F0F0F0",d:"M393.267 106h-54.534c-2.614 0-4.733 2.053-4.733 4.585v52.83c0 2.532 2.119 4.585 4.733 4.585h54.534c2.614 0 4.733-2.053 4.733-4.585v-52.83c0-2.532-2.119-4.585-4.733-4.585z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M366 150.493l-41.579-20.323a1.667 1.667 0 00-1.631.091 1.695 1.695 0 00-.579.619 1.725 1.725 0 00-.211.826v34.967a5.345 5.345 0 001.543 3.767 5.261 5.261 0 003.725 1.56h77.464a5.261 5.261 0 003.725-1.56 5.345 5.345 0 001.543-3.767v-34.368c0-.352-.088-.699-.257-1.008a2.069 2.069 0 00-1.688-1.071 2.035 2.035 0 00-1.009.205L366 150.493zM390 118h-48v2h48v-2zM390 124h-48v2h48v-2z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M384 130h-42v2h42v-2z"}),Object(c.createElement)("path",{fill:"#fff",d:"M335 112a7 7 0 100-14 7 7 0 000 14z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M336 98a8.003 8.003 0 00-7.391 4.939 7.992 7.992 0 00-.455 4.622 7.993 7.993 0 006.285 6.285A8 8 0 00344 106a8.022 8.022 0 00-8-8zm-1.642 12.265l-4.1-4.1 1.15-1.15 2.954 2.954 6.234-6.234 1.15 1.15-7.388 7.38z"})),Object(c.createElement)("defs",null,Object(c.createElement)("clipPath",{id:"clip0"},Object(c.createElement)("path",{fill:"#fff",d:"M0 0h517v160H0z"})))),null),content:Object(c.createElement)(je,{title:Object(u.__)("A personalized inbox full of relevant advice",'woocommerce'),body:Object(u.__)("Check your inbox for helpful growth tips tailored to your store and notifications about key traffic and sales milestones. We look forward to celebrating them with you!",'woocommerce')})},{image:Object(c.createElement)(()=>Object(c.createElement)("svg",{xmlns:"http://www.w3.org/2000/svg",width:"517",height:"160",fill:"none"},Object(c.createElement)("defs",null),Object(c.createElement)("g",{clipPath:"url(#clip0)"},Object(c.createElement)("path",{className:"fill-theme-color",d:"M0 0h517v160H0z"}),Object(c.createElement)("path",{fill:"#fff",fillOpacity:".65",d:"M30.501 63.74c7.21-10.372 19.533-17.051 31.735-22.399l2.057-.888c12.774-5.469 25.944-10.008 39.27-14.127 7.129-2.21 14.285-4.313 21.448-6.389l5.615-1.62c7.29-2.106 14.596-4.21 21.916-6.315a6165.97 6165.97 0 0121.511-6.139 3346.684 3346.684 0 0127.597-7.677 2189.847 2189.847 0 0121.603-5.782c9.237-2.42 18.491-4.764 27.761-7.035 7.246-1.77 14.502-3.483 21.767-5.14a1152.381 1152.381 0 0128.025-6 940.985 940.985 0 0119.106-3.654l2.908-.52c27.416-4.852 55.724-8.222 82.193-2.775l.715.151c.355.074.71.148 1.067.23a87.181 87.181 0 0114.309 4.404c8.282 3.398 15.644 8.247 20.596 14.967 7.763 10.54 8.624 24.398 6.126 37.281-2.498 12.884-8.007 25.346-12.299 37.974-1.257 3.7-2.378 7.49-3.34 11.33-5.997 24.068-5.398 49.993 11.766 67.323a93.715 93.715 0 007.029 6.227c3.928 3.218 7.905 6.424 11.03 10.3 7.28 9.017 9.211 20.756 10.296 32.099 1.425 15.086 1.236 31.775-9.516 44.175-11.153 12.875-30.519 17.317-48.211 18.232-27.498 1.457-54.442-3.316-81.339-6.956-26.898-3.641-54.739-6.141-81.787-.263a121.18 121.18 0 00-17.082 5.062 108.9 108.9 0 00-21.21 10.677c-9.622 6.318-17.826 14.22-23.006 23.613-11.123 20.092-39.488 28.645-62.664 24.15-22.115-4.288-39.921-20.774-44.019-40.738-4.538-22.229 6.615-44.308 16.332-66.515a358.83 358.83 0 003.437-8.081 238.988 238.988 0 001.795-4.513 165.185 165.185 0 002.828-7.947c4.39-13.591 6.016-28.984-2.295-40.321-4.658-6.347-11.477-10.355-19.238-13.393-17.388-6.801-39.481-8.722-52.38-21.167C22.84 94.854 21.359 76.92 30.502 63.74z"}),Object(c.createElement)("path",{fill:"#F6F7F7",d:"M124 33h267v185H124z"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M169 152.005V229a734.947 734.947 0 01-15.628-.991l-2.372-.181v-75.823c0-.395.072-.785.212-1.15.14-.365.345-.696.604-.975.258-.279.565-.5.903-.651a2.61 2.61 0 011.066-.229h12.43c.366 0 .728.078 1.066.229.338.151.645.372.903.651.259.279.464.61.604.975.14.365.212.755.212 1.15z",opacity:".7"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M186 229.733V127.377c0-.63.31-1.235.861-1.681.551-.446 1.299-.696 2.079-.696h13.12c.386 0 .768.061 1.125.181.357.119.681.294.954.515.273.221.489.483.637.771.148.289.224.598.224.91V230l-19-.267z",opacity:".5"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M225 230.002v-97.406a2.843 2.843 0 012.843-2.845h12.689a2.844 2.844 0 012.844 2.845v97.196l-18.376.21z",opacity:".7"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M282 88.368v140.224c-6 .145-12 .281-18 .408V88.368c0-.628.293-1.23.816-1.674.522-.445 1.231-.694 1.969-.694h12.43c.738 0 1.447.25 1.969.694.523.444.816 1.046.816 1.674z",opacity:".5"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M319 112.954v115.709c-6 .12-12 .232-18 .337V112.954c0-.518.293-1.015.816-1.382.522-.366 1.231-.572 1.969-.572h12.43c.738 0 1.447.206 1.969.572.523.367.816.864.816 1.382z",opacity:".7"}),Object(c.createElement)("path",{stroke:"#CCC",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M160.125 133.501l41.91-46.767 41.91 23.545 41.91-72.248 41.909 34.511"}),Object(c.createElement)("path",{className:"fill-theme-color",d:"M160 139.005c2.761 0 5-2.24 5-5.003a5.002 5.002 0 00-5-5.002c-2.761 0-5 2.24-5 5.002a5.002 5.002 0 005 5.003zM201.5 93.007c4.142 0 7.5-3.36 7.5-7.504A7.502 7.502 0 00201.5 78c-4.142 0-7.5 3.36-7.5 7.504a7.502 7.502 0 007.5 7.503zM243.784 119.31c4.985 0 9.026-4.043 9.026-9.031s-4.041-9.031-9.026-9.031c-4.986 0-9.027 4.043-9.027 9.031s4.041 9.031 9.027 9.031zM286.027 46.062c4.985 0 9.027-4.043 9.027-9.031S291.012 28 286.027 28c-4.986 0-9.027 4.043-9.027 9.031s4.041 9.031 9.027 9.031zM327.5 80.007c4.142 0 7.5-3.36 7.5-7.504A7.502 7.502 0 00327.5 65c-4.142 0-7.5 3.36-7.5 7.504a7.502 7.502 0 007.5 7.503zM408 137l-36 2-18-30.926c5.588-3.326 12.033-5.083 18.606-5.074C392.154 103 408 118.222 408 137zM351.107 110l-.143.088c-7.887 4.836-13.573 12.518-15.859 21.429a35.211 35.211 0 003.603 26.338l.084.145L370 140.317 351.107 110zm-12.19 47.543a34.886 34.886 0 01-3.485-25.944c2.25-8.77 7.826-16.342 15.566-21.138l18.531 29.738-30.612 17.344zM408.664 138.651l-35.891 2.797 10.3 32.297.162-.046c7.808-2.265 14.585-6.957 19.211-13.301 4.626-6.344 6.824-13.96 6.23-21.588l-.012-.159zm-35.447 3.081l35.134-2.738c1.116 15.348-9.387 29.753-25.051 34.355l-10.083-31.617zM370.719 142.639l-30.714 17.335.088.131c3.977 5.942 9.926 10.554 16.982 13.165 7.056 2.61 14.849 3.083 22.245 1.349l.164-.038-8.765-31.942zm-30.249 17.435l30.034-16.951 8.57 31.234c-7.278 1.673-14.935 1.192-21.871-1.374-6.936-2.566-12.794-7.086-16.733-12.909z"}),Object(c.createElement)("path",{fill:"#fff",d:"M423 97h-17v-1h17v1zM423 101h-17v-3h17v3zM416 104h-17.979l-.05.068L384 122.821l.28.179 13.92-18.685H416V104z"})),Object(c.createElement)("defs",null,Object(c.createElement)("clipPath",{id:"clip0"},Object(c.createElement)("path",{fill:"#fff",d:"M0 0h517v160H0z"})))),null),content:Object(c.createElement)(je,{title:Object(u.__)("Good data leads to smart business decisions",'woocommerce'),body:Object(u.__)("Monitor your stats to improve performance, increase sales, and track your progress toward revenue goals. The more you know, the better you can serve your customers and grow your store.",'woocommerce')})}],Be=({onClose:e})=>{const[t,a]=Object(c.useState)(!0);return Object(c.useEffect)(()=>{Object(w.recordEvent)("task_list_welcome_modal_open")},[]),Object(c.createElement)(c.Fragment,null,t&&Object(c.createElement)(O.Guide,{onFinish:()=>{a(!1),e(),Object(w.recordEvent)("task_list_welcome_modal_close")},className:"woocommerce__welcome-modal",finishButtonText:Object(u.__)("Let's go",'woocommerce'),pages:ke}))};a(563),a(516);const Se=Object(c.lazy)(()=>Promise.all([a.e(3),a.e(51)]).then(a.bind(null,611))),ye=({defaultHomescreenLayout:e,isBatchUpdating:t,query:a,taskListComplete:r,bothTaskListsHidden:n,shouldShowWelcomeModal:o,shouldShowWelcomeFromCalypsoModal:l,isTaskListHidden:i,updateOptions:m})=>{const d=Object(s.useUserPreferences)(),b=r||i,v=b||window.wcAdminFeatures.analytics,O="two_columns"===(d.homepage_layout||e)&&v,[w,f]=Object(c.useState)(!0),h=!1===n,j=!a.task;t&&!w&&f(!0);const E=Object(c.useRef)(!0),g=Object(c.useCallback)(()=>{E.current=window.innerWidth>=782},[]);Object(c.useLayoutEffect)(()=>(g(),window.addEventListener("resize",g),()=>{window.removeEventListener("resize",g)}),[g]);const k=E.current&&O,B=()=>{const e=Boolean(a.task);return Object(c.createElement)(c.Suspense,{fallback:e?null:Object(c.createElement)(fe.a,null)},Object(c.createElement)(Se,{query:a,userPreferences:d}))};return Object(c.createElement)("div",{className:A()("woocommerce-homescreen",{"two-columns":O})},j?Object(c.createElement)(c.Fragment,null,Object(c.createElement)(R,{shouldStick:k},Object(c.createElement)(p.a,{className:"your-store-today",title:Object(u.__)("Your store today",'woocommerce'),subtitle:Object(u.__)("To do's, tips, and insights for your business",'woocommerce')}),Object(c.createElement)(L,null),h&&B(),Object(c.createElement)(_.a,null)),Object(c.createElement)(R,{shouldStick:k},window.wcAdminFeatures.analytics&&Object(c.createElement)(Oe,null),b&&Object(c.createElement)(we.a,null))):B(),o&&Object(c.createElement)(Be,{onClose:()=>{m({woocommerce_task_list_welcome_modal_dismissed:"yes"})}}),l&&Object(c.createElement)(ge,{onClose:()=>{m({woocommerce_welcome_from_calypso_modal_dismissed:"yes"})}}),window.wcAdminFeatures.navigation&&Object(c.createElement)(ae,null))};ye.propTypes={taskListComplete:b.a.bool,bothTaskListsHidden:b.a.bool,query:b.a.object.isRequired,shouldShowWelcomeModal:b.a.bool,shouldShowWelcomeFromCalypsoModal:b.a.bool,updateOptions:b.a.func.isRequired};var Ne=Object(r.compose)(Object(n.withSelect)(e=>{const{isNotesRequesting:t}=e(s.NOTES_STORE_NAME),{getOption:a,hasFinishedResolution:c}=e(s.OPTIONS_STORE_NAME),r="yes"===a("woocommerce_welcome_from_calypso_modal_dismissed"),n=c("getOption",["woocommerce_welcome_from_calypso_modal_dismissed"]),o=!!window.location.search.match("from-calypso"),l=n&&!r&&o,i="yes"===a("woocommerce_task_list_welcome_modal_dismissed"),m=c("getOption",["woocommerce_task_list_welcome_modal_dismissed"])&&!i&&n&&!r,A=a("woocommerce_default_homepage_layout")||"single_column",d="yes"===a("woocommerce_task_list_hidden");return{defaultHomescreenLayout:A,isBatchUpdating:t("batchUpdateNotes"),shouldShowWelcomeModal:m,shouldShowWelcomeFromCalypsoModal:l,isTaskListHidden:d,bothTaskListsHidden:d&&"yes"===a("woocommerce_extended_task_list_hidden"),taskListComplete:"yes"===a("woocommerce_task_list_complete")}}),Object(n.withDispatch)(e=>({updateOptions:e(s.OPTIONS_STORE_NAME).updateOptions})))(ye);const Ce=Object(l.f)("onboarding",{});t.default=Object(r.compose)(Ce.profile||Ce.tasksStatus?Object(s.withOnboardingHydration)({profileItems:Ce.profile,tasksStatus:Ce.tasksStatus}):o.identity,Object(n.withSelect)(e=>{const{getProfileItems:t}=e(s.ONBOARDING_STORE_NAME);return{profileItems:t()}}))(({profileItems:e,query:t})=>{const{completed:a,skipped:r}=e||{};return a||r||Object(i.getHistory)().push(Object(i.getNewPath)({},"/setup-wizard",{})),Object(c.createElement)(Ne,{query:t})})}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/leaderboards.js b/packages/woocommerce-admin/dist/chunks/leaderboards.js new file mode 100644 index 0000000..860760f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/leaderboards.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[32],{504:function(e,t,a){"use strict";var r=a(0),o=a(2),s=a(1),l=a.n(s),n=a(21);function c({className:e}){const t=Object(o.__)("There was an error getting your stats. Please try again.",'woocommerce'),a=Object(o.__)("Reload",'woocommerce');return Object(r.createElement)(n.EmptyContent,{className:e,title:t,actionLabel:a,actionCallback:()=>{window.location.reload()}})}c.propTypes={className:l.a.string},t.a=c},509:function(e,t,a){"use strict";var r=a(53);const o=["a","b","em","i","strong","p","br"],s=["target","href","rel","name","download"];t.a=e=>({__html:Object(r.sanitize)(e,{ALLOWED_TAGS:o,ALLOWED_ATTR:s})})},601:function(e,t,a){},602:function(e,t,a){},620:function(e,t,a){"use strict";a.r(t);var r=a(0),o=a(2),s=a(14),l=a(1),n=a.n(l),c=a(3),d=a(7),i=a(21),m=a(11),b=a(13),u=a(16),g=a(12),p=a(20),w=a(504),O=a(509);a(601);class _ extends r.Component{getFormattedHeaders(){return this.props.headers.map((e,t)=>({isLeftAligned:0===t,hiddenByDefault:!1,isSortable:!1,key:e.label,label:e.label}))}getFormattedRows(){return this.props.rows.map(e=>e.map(e=>({display:Object(r.createElement)("div",{dangerouslySetInnerHTML:Object(O.a)(e.display)}),value:e.value})))}render(){const{isRequesting:e,isError:t,totalRows:a,title:s}=this.props,l="woocommerce-leaderboard";if(t)return Object(r.createElement)(w.a,{className:l});const n=this.getFormattedRows();return e||0!==n.length?Object(r.createElement)(i.TableCard,{className:l,headers:this.getFormattedHeaders(),isLoading:e,rows:n,rowsPerPage:a,showMenu:!1,title:s,totalRows:a}):Object(r.createElement)(c.Card,{className:l},Object(r.createElement)(c.CardHeader,null,Object(r.createElement)(p.Text,{size:16,weight:600,as:"h3",color:"#23282d"},s)),Object(r.createElement)(c.CardBody,{size:null},Object(r.createElement)(i.EmptyTable,null,Object(o.__)("No data recorded for the selected time period.",'woocommerce'))))}}_.propTypes={headers:n.a.arrayOf(n.a.shape({label:n.a.string})),id:n.a.string.isRequired,query:n.a.object,rows:n.a.arrayOf(n.a.arrayOf(n.a.shape({display:n.a.node,value:n.a.oneOfType([n.a.string,n.a.number,n.a.bool])}))).isRequired,title:n.a.string.isRequired,totalRows:n.a.number.isRequired},_.defaultProps={rows:[],isError:!1,isRequesting:!1};var h=Object(s.compose)(Object(d.withSelect)((e,t)=>{const{id:a,query:r,totalRows:o,filters:s}=t,{woocommerce_default_date_range:l}=e(m.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings"),n=Object(m.getFilterQuery)({filters:s,query:r}),c={id:a,per_page:o,persisted_query:Object(g.getPersistedQuery)(r),query:r,select:e,defaultDateRange:l,filterQuery:n};return Object(m.getLeaderboard)(c)}))(_);a(602);const j=e=>{const{allLeaderboards:t,controls:a,isFirst:s,isLast:l,hiddenBlocks:n,onMove:d,onRemove:b,onTitleBlur:g,onTitleChange:p,onToggleHiddenBlock:w,query:O,title:_,titleInput:j,filters:E}=e,{updateUserPreferences:y,...f}=Object(m.useUserPreferences)(),[T,R]=Object(r.useState)(parseInt(f.dashboard_leaderboard_rows||5,10)),k=e=>{R(parseInt(e,10));const t={dashboard_leaderboard_rows:parseInt(e,10)};y(t)};return Object(r.createElement)(r.Fragment,null,Object(r.createElement)("div",{className:"woocommerce-dashboard__dashboard-leaderboards"},Object(r.createElement)(i.SectionHeader,{title:_||Object(o.__)("Leaderboards",'woocommerce'),menu:Object(r.createElement)(i.EllipsisMenu,{label:Object(o.__)("Choose which leaderboards to display and other settings",'woocommerce'),renderContent:({onToggle:e})=>Object(r.createElement)(r.Fragment,null,Object(r.createElement)(i.MenuTitle,null,Object(o.__)("Leaderboards",'woocommerce')),(({allLeaderboards:e,hiddenBlocks:t,onToggleHiddenBlock:a})=>e.map(e=>{const o=!t.includes(e.id);return Object(r.createElement)(i.MenuItem,{checked:o,isCheckbox:!0,isClickable:!0,key:e.id,onInvoke:()=>{a(e.id)(),Object(u.recordEvent)("dash_leaderboards_toggle",{status:o?"off":"on",key:e.id})}},e.label)}))({allLeaderboards:t,hiddenBlocks:n,onToggleHiddenBlock:w}),Object(r.createElement)(c.SelectControl,{className:"woocommerce-dashboard__dashboard-leaderboards__select",label:Object(o.__)("Rows per table",'woocommerce'),value:T,options:Array.from({length:20},(e,t)=>({v:t+1,label:t+1})),onChange:k}),Object(r.createElement)(a,{onToggle:e,onMove:d,onRemove:b,isFirst:s,isLast:l,onTitleBlur:g,onTitleChange:p,titleInput:j}))})}),Object(r.createElement)("div",{className:"woocommerce-dashboard__columns"},(({allLeaderboards:e,hiddenBlocks:t,query:a,rowsPerTable:o,filters:s})=>e.map(e=>{if(!t.includes(e.id))return Object(r.createElement)(h,{headers:e.headers,id:e.id,key:e.id,query:a,title:e.label,totalRows:o,filters:s})}))({allLeaderboards:t,hiddenBlocks:n,query:O,rowsPerTable:T,filters:E}))))};j.propTypes={query:n.a.object.isRequired};t.default=Object(s.compose)(Object(d.withSelect)(e=>{const{getItems:t,getItemsError:a}=e(m.ITEMS_STORE_NAME),{leaderboards:r}=Object(b.f)("dataEndpoints",{leaderboards:[]});return{allLeaderboards:r,getItems:t,getItemsError:a}}))(j)}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/marketing-overview.js b/packages/woocommerce-admin/dist/chunks/marketing-overview.js new file mode 100644 index 0000000..555f203 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/marketing-overview.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[34],{102:function(M,e,t){"use strict";t.d(e,"a",(function(){return D})),t.d(e,"b",(function(){return Y})),t.d(e,"c",(function(){return b}));var j={};t.r(j),t.d(j,"blank",(function(){return A})),t.d(j,"amazonEbayIntegration",(function(){return T})),t.d(j,"woocommerceAmazonEbayIntegration",(function(){return T})),t.d(j,"automatewoo",(function(){return r})),t.d(j,"automatewooAlt",(function(){return S})),t.d(j,"facebook",(function(){return O})),t.d(j,"facebookForWoocommerce",(function(){return O})),t.d(j,"pinterest",(function(){return o})),t.d(j,"pinterestForWoocommerce",(function(){return o})),t.d(j,"googleAds",(function(){return E})),t.d(j,"googleListingsAndAds",(function(){return E})),t.d(j,"hubspotForWoocommerce",(function(){return l})),t.d(j,"mailchimpForWoocommerce",(function(){return C})),t.d(j,"woocommerceStoreCredit",(function(){return s})),t.d(j,"woocommerceFreeGiftCoupons",(function(){return w})),t.d(j,"woocommerceUrlCoupons",(function(){return x})),t.d(j,"woocommerceGroupCoupons",(function(){return m})),t.d(j,"woocommerceSmartCoupons",(function(){return d}));var L=t(35),N=t.n(L),u=t(0),c=t(3),i=t(6),g=t.n(i),D=(t(272),M=>Object(u.createElement)(c.Button,N()({},M,{className:g()(M.className,"woocommerce-admin-marketing-button")}))),I=(t(77),t(1)),n=t.n(I),y=t(116),z=t(4),a=(t(274),t(8));var A=Object(u.createElement)(a.SVG,{width:"36",height:"36",fill:"none",xmlns:"http://www.w3.org/2000/svg"});var T=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"100",height:"100",viewBox:"0 0 100 100"},Object(u.createElement)("defs",null,Object(u.createElement)("clipPath",{id:"b"},Object(u.createElement)("rect",{width:"100",height:"100"}))),Object(u.createElement)("g",{id:"a",clipPath:"url(#b)"},Object(u.createElement)("rect",{width:"100",height:"100",fill:"#fff"}),Object(u.createElement)("rect",{width:"100",height:"100",fill:"#eee"}),Object(u.createElement)("g",{transform:"translate(9 25.655)"},Object(u.createElement)(a.Path,{d:"M179.753,195.8c-4.732,3.488-11.591,5.349-17.5,5.349a31.66,31.66,0,0,1-21.374-8.156c-.443-.4-.046-.946.486-.634a43.018,43.018,0,0,0,21.384,5.671,42.523,42.523,0,0,0,16.312-3.335c.8-.34,1.471.525.688,1.106",transform:"translate(-129.235 -176.611)",fill:"#f90",fillRule:"evenodd"}),Object(u.createElement)(a.Path,{d:"M577.807,183.949c-.6-.773-4-.365-5.522-.184-.464.057-.535-.347-.117-.638,2.7-1.9,7.142-1.354,7.66-.716s-.135,5.09-2.676,7.213c-.39.326-.762.152-.588-.28.571-1.425,1.85-4.619,1.244-5.395",transform:"translate(-525.323 -167.01)",fill:"#f90",fillRule:"evenodd"}),Object(u.createElement)(a.Path,{d:"M572.708,6.758V4.908a.457.457,0,0,1,.468-.468h8.284a.461.461,0,0,1,.479.468V6.493a2.605,2.605,0,0,1-.624,1.163l-4.292,6.129a9.146,9.146,0,0,1,4.725,1.014.843.843,0,0,1,.44.72v1.974c0,.269-.3.585-.61.422a9.542,9.542,0,0,0-8.752.014c-.287.156-.588-.156-.588-.425V15.627a2.238,2.238,0,0,1,.3-1.272l4.973-7.132h-4.328a.458.458,0,0,1-.479-.464",transform:"translate(-525.64 -4.078)",fillRule:"evenodd"}),Object(u.createElement)(a.Path,{d:"M173.431,15.624h-2.52a.476.476,0,0,1-.45-.429V2.261a.473.473,0,0,1,.486-.464h2.35a.475.475,0,0,1,.457.432V3.92h.046a3.463,3.463,0,0,1,6.589,0,3.722,3.722,0,0,1,6.4-.982c.8,1.088.634,2.669.634,4.055l0,8.163a.476.476,0,0,1-.486.468h-2.517a.479.479,0,0,1-.454-.468V8.3a16.192,16.192,0,0,0-.071-2.424,1.312,1.312,0,0,0-1.482-1.113,1.674,1.674,0,0,0-1.506,1.06,7.831,7.831,0,0,0-.234,2.478v6.855a.476.476,0,0,1-.486.468h-2.517a.476.476,0,0,1-.454-.468l0-6.855c0-1.443.238-3.566-1.553-3.566-1.811,0-1.74,2.07-1.74,3.566v6.855a.476.476,0,0,1-.486.468",transform:"translate(-156.58 -1.399)",fillRule:"evenodd"}),Object(u.createElement)(a.Path,{d:"M714.982,1.524c3.739,0,5.763,3.211,5.763,7.295,0,3.945-2.237,7.075-5.763,7.075-3.672,0-5.671-3.211-5.671-7.213,0-4.027,2.024-7.156,5.671-7.156M715,4.164c-1.857,0-1.974,2.531-1.974,4.108s-.025,4.955,1.953,4.955c1.953,0,2.045-2.722,2.045-4.381a11.959,11.959,0,0,0-.376-3.431A1.577,1.577,0,0,0,715,4.164",transform:"translate(-651.552 -1.399)",fillRule:"evenodd"}),Object(u.createElement)(a.Path,{d:"M875.817,15.624h-2.51a.479.479,0,0,1-.454-.468l0-12.938a.477.477,0,0,1,.486-.422h2.336a.482.482,0,0,1,.45.362V4.136h.046c.705-1.769,1.694-2.612,3.435-2.612a3.307,3.307,0,0,1,2.942,1.524c.659,1.035.659,2.775.659,4.027v8.142a.484.484,0,0,1-.486.408h-2.527a.477.477,0,0,1-.447-.408V8.191c0-1.414.163-3.484-1.577-3.484a1.647,1.647,0,0,0-1.457,1.035,5.724,5.724,0,0,0-.4,2.449v6.965a.485.485,0,0,1-.493.468",transform:"translate(-801.775 -1.399)",fillRule:"evenodd"}),Object(u.createElement)(a.Path,{d:"M413.163,8.046a4.93,4.93,0,0,1-.471,2.673,2.048,2.048,0,0,1-1.744,1.145c-.968,0-1.535-.737-1.535-1.825,0-2.148,1.925-2.538,3.75-2.538v.546m2.541,6.143a.526.526,0,0,1-.6.06,6.143,6.143,0,0,1-1.446-1.68,4.991,4.991,0,0,1-4.154,1.833,3.575,3.575,0,0,1-3.771-3.927,4.277,4.277,0,0,1,2.687-4.119,17.463,17.463,0,0,1,4.739-.876V5.154a3.214,3.214,0,0,0-.308-1.825,1.677,1.677,0,0,0-1.414-.656,1.917,1.917,0,0,0-2.024,1.514.527.527,0,0,1-.439.461l-2.442-.262a.444.444,0,0,1-.376-.528C406.719.893,409.4,0,411.795,0a5.714,5.714,0,0,1,3.8,1.255C416.818,2.4,416.7,3.928,416.7,5.59V9.517a3.447,3.447,0,0,0,.95,2.336.477.477,0,0,1-.011.67c-.514.429-1.428,1.226-1.932,1.673l0-.007",transform:"translate(-372.698 0)",fillRule:"evenodd"}),Object(u.createElement)(a.Path,{d:"M7.426,8.046a4.93,4.93,0,0,1-.471,2.673,2.043,2.043,0,0,1-1.744,1.145c-.968,0-1.531-.737-1.531-1.825C3.679,7.89,5.6,7.5,7.426,7.5v.546m2.541,6.143a.526.526,0,0,1-.6.06,6.2,6.2,0,0,1-1.446-1.68A4.986,4.986,0,0,1,3.771,14.4,3.576,3.576,0,0,1,0,10.474,4.282,4.282,0,0,1,2.687,6.356,17.462,17.462,0,0,1,7.426,5.48V5.154a3.243,3.243,0,0,0-.3-1.825,1.686,1.686,0,0,0-1.414-.656A1.921,1.921,0,0,0,3.679,4.186a.527.527,0,0,1-.436.461L.8,4.385a.446.446,0,0,1-.376-.528C.985.893,3.662,0,6.058,0a5.714,5.714,0,0,1,3.8,1.255C11.08,2.4,10.963,3.928,10.963,5.59V9.517a3.447,3.447,0,0,0,.95,2.336.473.473,0,0,1-.007.67c-.514.429-1.428,1.226-1.932,1.673l-.007-.007",transform:"translate(0 0)",fillRule:"evenodd"})),Object(u.createElement)("g",{transform:"translate(18.9 54.637)"},Object(u.createElement)(a.Path,{d:"M8.055,26.308C3.716,26.308.1,28.149.1,33.7c0,4.4,2.431,7.171,8.067,7.171,6.633,0,7.059-4.37,7.059-4.37H12.011s-.689,2.353-4.04,2.353a4.4,4.4,0,0,1-4.693-4.428H15.562V32.807c0-2.557-1.623-6.5-7.507-6.5Zm-.112,2.073c2.6,0,4.37,1.592,4.37,3.977H3.349C3.349,29.826,5.661,28.381,7.943,28.381Z",transform:"translate(0 -20.83)",fill:"#e53238"}),Object(u.createElement)(a.Path,{d:"M75.169.1V17.254c0,.974-.069,2.341-.069,2.341h3.066s.11-.982.11-1.879c0,0,1.515,2.37,5.633,2.37a6.961,6.961,0,0,0,7.283-7.325A6.922,6.922,0,0,0,83.915,5.52c-4.279,0-5.609,2.311-5.609,2.311V.1Zm7.955,7.542c2.945,0,4.818,2.186,4.818,5.119a4.857,4.857,0,0,1-4.8,5.2c-3.143,0-4.839-2.454-4.839-5.175C78.306,10.254,79.827,7.642,83.123,7.642Z",transform:"translate(-59.609)",fill:"#0064d2"}),Object(u.createElement)(a.Path,{d:"M159.834,26.308c-6.528,0-6.947,3.574-6.947,4.146h3.249s.17-2.087,3.473-2.087c2.146,0,3.809.982,3.809,2.871v.672h-3.809c-5.057,0-7.731,1.479-7.731,4.482,0,2.955,2.47,4.562,5.809,4.562,4.55,0,6.015-2.514,6.015-2.514,0,1,.077,1.985.077,1.985h2.888s-.112-1.221-.112-2V31.669c0-4.428-3.572-5.36-6.722-5.36Zm3.585,7.619v.9c0,1.169-.721,4.075-4.968,4.075-2.326,0-3.323-1.161-3.323-2.507C155.128,33.943,158.486,33.927,163.419,33.927Z",transform:"translate(-120.634 -20.83)",fill:"#f5af02"}),Object(u.createElement)(a.Path,{d:"M214.879,29.041h3.655l5.246,10.51,5.234-10.51h3.311l-9.533,18.711h-3.473l2.751-5.216Z",transform:"translate(-170.706 -23.002)",fill:"#86b817"}))));var r=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"100",height:"100",viewBox:"0 0 100 100"},Object(u.createElement)("defs",null,Object(u.createElement)("clipPath",{id:"b"},Object(u.createElement)("rect",{width:"100",height:"100"}))),Object(u.createElement)("g",{id:"a",clipPath:"url(#b)"},Object(u.createElement)("rect",{width:"100",height:"100",fill:"#fff"}),Object(u.createElement)("rect",{width:"100",height:"100",fill:"#7532e4"}),Object(u.createElement)("g",{transform:"translate(-43.503 -133.512)"},Object(u.createElement)(a.Path,{d:"M78.217,193.13H64.405l-2.823,7.764H54.6L67.648,166.9h7.669l12.934,33.995H81.059Zm-11.6-6.047h9.4L71.33,174.245Z",transform:"translate(0 0)",fill:"#1ff2e6"}),Object(u.createElement)(a.Path,{d:"M246.639,166.9h6.753l-9.4,33.995h-6.81l-7.764-24.208-7.764,24.208h-6.906L205.3,166.9h7l6.238,23.388,7.535-23.388h6.849l7.592,23.483Z",transform:"translate(-121.952)",fill:"#1ff2e6"}))));var S=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg"},Object(u.createElement)(a.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M4.67708 14.1615h3.77084l.77604 2.1198h1.96354L7.65625 7H5.5625L2 16.2813h1.90625l.77083-2.1198zm3.17188-1.6511H5.28125l1.28646-3.50519 1.28125 3.50519zM22.9791 7h-1.8437l-1.6719 6.4115L17.3906 7h-1.8698l-2.0573 6.3854L11.7604 7H9.8489l2.5781 9.2813h1.8854l2.1198-6.60942 2.1198 6.60942h1.8594L22.9791 7z"}));var O=Object(u.createElement)(a.SVG,{width:"36",height:"36",viewBox:"0 0 36 36",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(u.createElement)(a.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M34 0H2C0.8 0 0 0.8 0 2V34C0 35 0.8 36 2 36H19.2V22H14.6V16.6H19.2V12.6C19.2 8 22 5.4 26.2 5.4C28.2 5.4 29.8 5.6 30.4 5.6V10.4H27.6C25.4 10.4 25 11.4 25 13V16.4H30.4L29.6 22H25V36H34C35 36 36 35.2 36 34V2C36 0.8 35.2 0 34 0Z",fill:"#3B5997"}));var o=Object(u.createElement)(a.SVG,{width:"303",height:"303",viewBox:"-30 -30 303 303",fill:"none",xmlns:"http://www.w3.org/2000/SVG"},Object(u.createElement)(a.Path,{fill:"#E60023",d:"M121.5,0C54.4,0,0,54.4,0,121.5C0,173,32,217,77.2,234.7c-1.1-9.6-2-24.4,0.4-34.9 c2.2-9.5,14.2-60.4,14.2-60.4s-3.6-7.3-3.6-18c0-16.9,9.8-29.5,22-29.5c10.4,0,15.4,7.8,15.4,17.1c0,10.4-6.6,26-10.1,40.5 c-2.9,12.1,6.1,22,18,22c21.6,0,38.2-22.8,38.2-55.6c0-29.1-20.9-49.4-50.8-49.4C86.3,66.5,66,92.4,66,119.2c0,10.4,4,21.6,9,27.7 c1,1.2,1.1,2.3,0.8,3.5c-0.9,3.8-3,12.1-3.4,13.8c-0.5,2.2-1.8,2.7-4.1,1.6c-15.2-7.1-24.7-29.2-24.7-47.1 c0-38.3,27.8-73.5,80.3-73.5c42.1,0,74.9,30,74.9,70.2c0,41.9-26.4,75.6-63,75.6c-12.3,0-23.9-6.4-27.8-14c0,0-6.1,23.2-7.6,28.9 c-2.7,10.6-10.1,23.8-15.1,31.9c11.4,3.5,23.4,5.4,36,5.4c67.1,0,121.5-54.4,121.5-121.5C243,54.4,188.6,0,121.5,0z"}));var E=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"128",height:"128",viewBox:"0 0 128 128"},Object(u.createElement)("rect",{width:"128",height:"128",fill:"#eee"}),Object(u.createElement)(a.Path,{d:"M92.4539 42.3419c3.7531 0 6.7961-3.0427 6.7961-6.7959 0-3.7531-3.043-6.7958-6.7961-6.7958s-6.7958 3.0427-6.7958 6.7958c0 3.7532 3.0427 6.7959 6.7958 6.7959zm-48.1438-2.0744l20.848-20.8481c1.5904-1.5903 3.7989-2.562 6.2285-2.562h30.9214c1.161-.0041 2.312.2217 3.386.6642 1.073.4426 2.049 1.0932 2.87 1.9143.821.8212 1.472 1.7967 1.914 2.8704.443 1.0737.669 2.2244.665 3.3857v30.9213c0 2.4297-.972 4.6384-2.607 6.2285L87.7202 83.678 44.3101 40.2675z",fill:"#4285F4"}),Object(u.createElement)(a.Path,{d:"M87.7202 83.678l-25.6915 25.716c-1.6346 1.59-3.8431 2.606-6.2726 2.606-2.4294 0-4.6383-1.016-6.2285-2.606L18.6061 78.4725C16.9717 76.8821 16 74.6736 16 72.244c0-2.4737 1.0159-4.6824 2.6061-6.2726L44.31 40.2675 87.7202 83.678z",fill:"#34A853"}),Object(u.createElement)(a.Path,{d:"M33.6115 93.4777L18.6061 78.4723C16.9717 76.8825 16 74.6736 16 72.2442c0-2.4737 1.0159-4.6824 2.6061-6.2726L44.31 40.2677l21.2557 21.256-31.9542 31.954z",fill:"#FBBC05"}),Object(u.createElement)(a.Path,{d:"M108.092 18.9973c-1.607-1.3873-3.661-2.1473-5.784-2.1399H71.3866c-2.4296 0-4.6381.9717-6.2285 2.562l-20.848 20.8481 21.2556 21.256 21.649-21.649c-1.0082-1.2168-1.5589-2.7482-1.5565-4.3285 0-3.7531 3.0426-6.7958 6.7957-6.7958 1.5804-.0025 3.1118.5482 4.3287 1.5565l11.3094-11.3094z",fill:"#EA4335"}),Object(u.createElement)(a.Path,{d:"M65.5535 77.7372c7.6238 0 13.8041-6.1803 13.8041-13.8041S73.1773 50.129 65.5535 50.129s-13.8041 6.1803-13.8041 13.8041 6.1803 13.8041 13.8041 13.8041z",fill:"#4285F4"}),Object(u.createElement)(a.Path,{d:"M84.3608 59.8724H66.0013v7.877h10.568c-.9853 5.0043-5.1048 7.8771-10.568 7.8771-6.4483 0-11.6427-5.3749-11.6427-12.0473s5.1944-12.0473 11.6427-12.0473c2.7764 0 5.284 1.0194 7.2543 2.6875l5.7318-5.9311c-3.4928-3.1508-7.9708-5.0969-12.9861-5.0969-10.9261 0-19.7029 9.0819-19.7029 20.3878S55.0752 83.967 66.0013 83.967c9.8514 0 18.8073-7.4138 18.8073-20.3878 0-1.2047-.1791-2.5022-.4478-3.7068z",fill:"#fff"}));var l=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg",width:"100",height:"100",viewBox:"0 0 100 100"},Object(u.createElement)("defs",null,Object(u.createElement)("clipPath",{id:"b"},Object(u.createElement)("rect",{width:"100",height:"100"}))),Object(u.createElement)("g",{id:"a",clipPath:"url(#b)"},Object(u.createElement)("rect",{width:"100",height:"100",fill:"#fff"}),Object(u.createElement)(a.Path,{d:"M100,100H0V0H100V100ZM40.665,75.539a6.446,6.446,0,1,0,6.447,6.447,6.376,6.376,0,0,0-.3-1.843L54.158,72.8A19.808,19.808,0,1,0,69.206,37.48h.015V28.455a6.959,6.959,0,0,0,4.013-6.273v-.211a6.971,6.971,0,0,0-6.952-6.953H66.07a6.97,6.97,0,0,0-6.952,6.953v.211a6.957,6.957,0,0,0,4.013,6.273V37.5a19.745,19.745,0,0,0-9.376,4.126L28.935,22.295a7.919,7.919,0,0,0-4.148-9.145,7.845,7.845,0,0,0-3.5-.817,7.919,7.919,0,1,0,3.938,14.786l24.4,19a19.775,19.775,0,0,0,.3,22.3l-7.426,7.427A6.362,6.362,0,0,0,40.665,75.539Zm25.522-8.321h0l-.023,0a10.164,10.164,0,0,1,.023-20.328H66.2a10.166,10.166,0,0,1-.012,20.333Z",fill:"#ff7a59"})));var C=Object(u.createElement)(a.SVG,{width:"36",height:"36",viewBox:"0 0 36 36",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(u.createElement)("rect",{width:"36",height:"36",rx:"3",fill:"#FFE071"}),Object(u.createElement)(a.Path,{d:"M24.0534 17.2863C24.2392 17.2638 24.4176 17.2625 24.5813 17.2863C24.6764 17.0647 24.6923 16.6823 24.6071 16.2661C24.4808 15.6471 24.3091 15.2728 23.9546 15.331C23.6002 15.3892 23.5873 15.8374 23.7143 16.4564C23.7848 16.8043 23.9117 17.1023 24.0534 17.2863Z",fill:"black"}),Object(u.createElement)(a.Path,{d:"M21.0119 17.7757C21.2652 17.8889 21.4209 17.9647 21.4823 17.899C21.5215 17.8576 21.5099 17.7794 21.4491 17.6786C21.3241 17.4702 21.0665 17.2587 20.7937 17.1404C20.2357 16.895 19.5697 16.9764 19.0559 17.3532C18.886 17.4802 18.7254 17.6555 18.7487 17.7625C18.756 17.7969 18.7812 17.8232 18.8413 17.8314C18.9811 17.8476 19.4698 17.5954 20.0321 17.5603C20.4294 17.5353 20.7587 17.6624 21.0119 17.7757Z",fill:"black"}),Object(u.createElement)(a.Path,{d:"M20.5024 18.073C20.1725 18.1262 19.9904 18.237 19.8733 18.3409C19.7733 18.4298 19.712 18.5281 19.7126 18.5975C19.7126 18.6307 19.7267 18.6495 19.7378 18.6589C19.7531 18.6726 19.7709 18.6802 19.7923 18.6802C19.8671 18.6802 20.0339 18.6119 20.0339 18.6119C20.4932 18.4442 20.7961 18.4642 21.0966 18.4993C21.2627 18.518 21.3406 18.5287 21.3774 18.4705C21.3884 18.4536 21.4013 18.4179 21.3682 18.3628C21.2903 18.2339 20.9568 18.0179 20.5024 18.073Z",fill:"black"}),Object(u.createElement)(a.Path,{d:"M23.0263 19.1626C23.2501 19.2753 23.4972 19.2309 23.5775 19.0644C23.6578 18.8973 23.5413 18.6713 23.3169 18.5587C23.0925 18.446 22.846 18.4904 22.7657 18.6569C22.6859 18.824 22.8025 19.0506 23.0263 19.1626Z",fill:"black"}),Object(u.createElement)(a.Path,{d:"M24.4673 17.8777C24.2851 17.8746 24.1343 18.0786 24.13 18.3334C24.1257 18.5881 24.2698 18.7971 24.4519 18.8003C24.634 18.8034 24.7849 18.5994 24.7892 18.3446C24.7935 18.0899 24.6494 17.8809 24.4673 17.8777Z",fill:"black"}),Object(u.createElement)(a.Path,{d:"M12.2373 22.4735C12.1919 22.4153 12.1177 22.4335 12.0454 22.4504C11.9951 22.4623 11.9381 22.476 11.8755 22.4748C11.7419 22.4723 11.6284 22.4134 11.5646 22.3139C11.4819 22.1837 11.4868 21.9903 11.5781 21.7682C11.5904 21.7381 11.6051 21.7049 11.6211 21.6686C11.767 21.3344 12.0117 20.7743 11.7369 20.241C11.5303 19.8398 11.1937 19.5895 10.7884 19.5369C10.3996 19.4868 9.99919 19.6339 9.7441 19.9212C9.34124 20.3749 9.27808 20.9921 9.35595 21.2099C9.38477 21.29 9.42892 21.3119 9.46142 21.3163C9.5301 21.3257 9.63127 21.275 9.69505 21.1003C9.69934 21.0878 9.70547 21.0684 9.71344 21.0434C9.74165 20.9514 9.79438 20.7799 9.88084 20.6422C9.98508 20.4763 10.147 20.3618 10.3371 20.3205C10.5308 20.2779 10.7289 20.3161 10.8944 20.4269C11.1765 20.6153 11.285 20.9683 11.1648 21.305C11.1023 21.479 11.0011 21.812 11.0238 22.0855C11.0692 22.6394 11.4028 22.8616 11.7026 22.8854C11.9939 22.8966 12.1981 22.7295 12.2496 22.6075C12.279 22.5361 12.2539 22.4923 12.2373 22.4735Z",fill:"black"}),Object(u.createElement)(a.Path,{d:"M29.0624 21.4609C29.0513 21.4209 28.979 21.1511 28.8796 20.8263C28.7803 20.5015 28.6773 20.2724 28.6773 20.2724C29.0759 19.6634 29.0826 19.1189 29.0299 18.8109C28.9735 18.4285 28.8177 18.1031 28.5031 17.7663C28.1892 17.4296 27.5466 17.0847 26.6434 16.8262C26.5403 16.7968 26.1994 16.7011 26.1694 16.6917C26.1669 16.6717 26.1442 15.5513 26.124 15.0706C26.1093 14.7233 26.0798 14.18 25.9149 13.6455C25.7181 12.922 25.3759 12.2886 24.9479 11.8836C26.1283 10.635 26.8647 9.25926 26.8629 8.07947C26.8592 5.81 24.1293 5.1234 20.7642 6.54542C20.7605 6.54667 20.0565 6.85147 20.051 6.8546C20.048 6.85147 18.7621 5.56402 18.7431 5.5465C14.907 2.13103 2.91255 15.7391 6.7474 19.0444L7.58562 19.7692C7.36794 20.3437 7.28271 21.0028 7.35261 21.7107C7.44213 22.6201 7.90202 23.4926 8.64704 24.166C9.35404 24.8057 10.2842 25.2106 11.1868 25.21C12.6793 28.72 16.0886 30.8737 20.0872 30.9951C24.3758 31.1253 27.9758 29.0711 29.4842 25.3815C29.583 25.1224 30.0018 23.9557 30.0018 22.9255C30.0005 21.8903 29.4272 21.4609 29.0624 21.4609ZM11.5161 24.2236C11.3861 24.2461 11.2531 24.2555 11.1188 24.2518C9.82374 24.2161 8.42445 23.0263 8.28526 21.6143C8.13135 20.054 8.91255 18.8535 10.2953 18.5687C10.4608 18.5349 10.6601 18.5149 10.876 18.5268C11.651 18.57 12.7928 19.1777 13.0534 20.9002C13.2845 22.4261 12.9172 23.9801 11.5161 24.2236ZM10.0696 17.6361C9.20872 17.807 8.45021 18.3052 7.98603 18.9931C7.70887 18.7571 7.19195 18.3002 7.10059 18.1218C6.35986 16.686 7.90877 13.8946 8.99104 12.318C11.6657 8.42245 15.8544 5.4739 17.7939 6.00903C18.1091 6.10041 19.1533 7.33591 19.1533 7.33591C19.1533 7.33591 17.2151 8.43372 15.4172 9.96402C12.9951 11.8667 11.1654 14.6338 10.0696 17.6361ZM23.6657 23.6403C23.694 23.6284 23.7136 23.5952 23.7099 23.5627C23.7056 23.5226 23.6706 23.4932 23.6314 23.4976C23.6314 23.4976 21.6024 23.8043 19.6856 23.0876C19.8941 22.3948 20.4496 22.6451 21.2884 22.714C22.8012 22.806 24.1563 22.5807 25.1582 22.2871C26.0265 22.033 27.1664 21.5317 28.0525 20.8182C28.3511 21.4879 28.4565 22.2252 28.4565 22.2252C28.4565 22.2252 28.6877 22.1832 28.8809 22.304C29.0636 22.4186 29.1973 22.657 29.1059 23.2735C28.9195 24.4252 28.44 25.3596 27.6343 26.2196C27.1437 26.7585 26.5477 27.2273 25.8665 27.5684C25.5047 27.7624 25.119 27.9301 24.7118 28.0659C21.6735 29.0786 18.5628 27.9652 17.5603 25.5737C17.4799 25.394 17.4125 25.2056 17.3592 25.0091C16.9318 23.4331 17.2948 21.5423 18.4285 20.3525V20.3519C18.4984 20.2761 18.5696 20.1866 18.5696 20.0746C18.5696 19.9807 18.5113 19.8818 18.4604 19.8111C18.0637 19.224 16.6896 18.2232 16.9655 16.2861C17.1635 14.8948 18.3556 13.9146 19.4673 13.9728C19.5611 13.9778 19.6549 13.9835 19.7487 13.9891C20.2307 14.0179 20.6507 14.0811 21.0468 14.098C21.7103 14.1274 22.3069 14.0285 23.0139 13.4277C23.2525 13.2249 23.4438 13.049 23.7669 12.9933C23.8006 12.9877 23.8853 12.9564 24.0545 12.9645C24.2268 12.9739 24.3911 13.0221 24.5389 13.1222C25.1055 13.5072 25.1858 14.4391 25.2153 15.1213C25.2318 15.5106 25.2778 16.4526 25.2937 16.7224C25.3299 17.3407 25.4887 17.4277 25.8113 17.536C25.9922 17.5967 26.1608 17.6424 26.4085 17.7131C27.1584 17.9278 27.603 18.1462 27.8838 18.426C28.0512 18.6013 28.1285 18.7872 28.153 18.9643C28.2413 19.6227 27.6521 20.4364 26.0921 21.1755C24.3868 21.9836 22.3174 22.1882 20.888 22.0255C20.7783 22.013 20.3883 21.9679 20.3871 21.9679C19.2435 21.8108 18.591 23.3192 19.2778 24.3525C19.7199 25.0185 20.9248 25.4522 22.1303 25.4522C24.8939 25.4529 27.0186 24.248 27.8084 23.2078C27.8323 23.1765 27.8342 23.1734 27.8716 23.1158C27.9102 23.0557 27.8783 23.0232 27.8299 23.057C27.1842 23.5076 24.3169 25.2976 21.2492 24.7594C21.2492 24.7594 20.8764 24.6968 20.5361 24.5616C20.2656 24.4546 19.6997 24.1886 19.631 23.5958C22.107 24.3788 23.6657 23.6403 23.6657 23.6403ZM19.7444 23.1677C19.7444 23.1684 19.7444 23.1684 19.7444 23.1677C19.745 23.169 19.745 23.169 19.745 23.1696C19.745 23.169 19.7444 23.1684 19.7444 23.1677ZM15.0088 12.3023C15.9599 11.1807 17.1304 10.2056 18.1784 9.65858C18.2145 9.6398 18.2532 9.67986 18.2336 9.71616C18.1502 9.87013 17.9901 10.1993 17.9392 10.4497C17.9313 10.4885 17.9729 10.5179 18.0048 10.4954C18.6573 10.0416 19.7916 9.55531 20.7875 9.49272C20.8304 9.49022 20.8506 9.54592 20.8169 9.57283C20.6654 9.69113 20.4999 9.85511 20.3791 10.021C20.3582 10.0491 20.3779 10.0898 20.4122 10.0898C21.1112 10.0948 22.0966 10.3446 22.7386 10.712C22.7821 10.737 22.7509 10.8227 22.7024 10.8115C21.7305 10.5843 20.1406 10.4115 18.488 10.8227C17.0133 11.1901 15.8875 11.7572 15.0665 12.3668C15.0254 12.3981 14.9757 12.3418 15.0088 12.3023Z",fill:"black"}));var s=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(u.createElement)(a.Path,{d:"M3.25 12a8.75 8.75 0 1117.5 0 8.75 8.75 0 01-17.5 0zM12 4.75a7.25 7.25 0 100 14.5 7.25 7.25 0 000-14.5zm-1.338 4.877c-.314.22-.412.452-.412.623 0 .171.098.403.412.623.312.218.783.377 1.338.377.825 0 1.605.233 2.198.648.59.414 1.052 1.057 1.052 1.852 0 .795-.461 1.438-1.052 1.852-.41.286-.907.486-1.448.582v.316a.75.75 0 01-1.5 0v-.316a3.64 3.64 0 01-1.448-.582c-.59-.414-1.052-1.057-1.052-1.852a.75.75 0 011.5 0c0 .171.098.403.412.623.312.218.783.377 1.338.377s1.026-.159 1.338-.377c.314-.22.412-.452.412-.623 0-.171-.098-.403-.412-.623-.312-.218-.783-.377-1.338-.377-.825 0-1.605-.233-2.198-.648-.59-.414-1.052-1.057-1.052-1.852 0-.795.461-1.438 1.052-1.852a3.64 3.64 0 011.448-.582V7.5a.75.75 0 011.5 0v.316c.54.096 1.039.296 1.448.582.59.414 1.052 1.057 1.052 1.852a.75.75 0 01-1.5 0c0-.171-.098-.403-.412-.623-.312-.218-.783-.377-1.338-.377s-1.026.159-1.338.377z"}));var w=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg"},Object(u.createElement)(a.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M14.75 9C16.1307 9 17.25 7.88071 17.25 6.5C17.25 5.11929 16.1307 4 14.75 4C13.3693 4 12.25 5.11929 12.25 6.5C12.25 5.11929 11.1307 4 9.75 4C8.36929 4 7.25 5.11929 7.25 6.5C7.25 7.88071 8.36929 9 9.75 9H4V20L20 20V9L14.75 9ZM14.75 7.5C15.3023 7.5 15.75 7.05228 15.75 6.5C15.75 5.94772 15.3023 5.5 14.75 5.5C14.1977 5.5 13.75 5.94772 13.75 6.5V7.5H14.75ZM18.5 18.5V10.5H13V18.5H18.5ZM11.5 18.5H5.5L5.5 10.5H11.5L11.5 18.5ZM8.75 6.5C8.75 7.05228 9.19772 7.5 9.75 7.5H10.75V6.5C10.75 5.94772 10.3023 5.5 9.75 5.5C9.19772 5.5 8.75 5.94772 8.75 6.5Z"}));var x=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(u.createElement)(a.Path,{d:"M15.6 7.2H14v1.5h1.6c2 0 3.7 1.7 3.7 3.7s-1.7 3.7-3.7 3.7H14v1.5h1.6c2.8 0 5.2-2.3 5.2-5.2 0-2.9-2.3-5.2-5.2-5.2zM4.7 12.4c0-2 1.7-3.7 3.7-3.7H10V7.2H8.4c-2.9 0-5.2 2.3-5.2 5.2 0 2.9 2.3 5.2 5.2 5.2H10v-1.5H8.4c-2 0-3.7-1.7-3.7-3.7zm4.6.9h5.3v-1.5H9.3v1.5z"}));var m=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(u.createElement)(a.Path,{d:"M17.5 9a2 2 0 11-4 0 2 2 0 014 0zm-4.25 8v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM8.5 11a2 2 0 100-4 2 2 0 000 4z"}));var d=Object(u.createElement)(a.SVG,{xmlns:"http://www.w3.org/2000/svg"},Object(u.createElement)(a.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M15 16.5H9V15h6v1.5zM15.0052 5.99481c-1.6597-1.65973-4.3507-1.65973-6.0104 0-1.65973 1.65973-1.65973 4.35069 0 6.01039.29289.2929.29289.7678 0 1.0607-.2929.2929-.76777.2929-1.06066 0-2.24552-2.2455-2.24552-5.88624 0-8.13175 2.24556-2.24551 5.88616-2.24551 8.13176 0 2.2455 2.24551 2.2455 5.88625 0 8.13175-.2929.2929-.7678.2929-1.0607 0-.2929-.2929-.2929-.7678 0-1.0607 1.6597-1.6597 1.6597-4.35066 0-6.01039zM14 19.5h-4V18h4v1.5z"}));class k extends u.Component{render(){const M=Object(z.camelCase)(this.props.product);let e=A;return M in j&&(e=j[M]),Object(u.createElement)("div",{className:g()(this.props.className,"woocommerce-admin-marketing-product-icon")},Object(u.createElement)(y.a,{icon:e}))}}k.propTypes={product:n.a.string.isRequired,className:n.a.string};var Y=k,Q=t(172),U=t(169);t(275);const h=({children:M,animationKey:e,animate:t})=>{const[j,L]=Object(u.useState)(null),N=Object(u.useRef)(),c=g()("woocommerce-marketing-slider",t&&"animate-"+t),i={};j&&(i.height=j);const D=Object(z.debounce)(()=>{const M=N.current.querySelector(".woocommerce-marketing-slider__slide");L(M.clientHeight)},50);Object(u.useEffect)(()=>(window.addEventListener("resize",D),()=>{window.removeEventListener("resize",D)}),[]);return Object(u.createElement)("div",{className:c,ref:N,style:i},Object(u.createElement)(Q.a,null,Object(u.createElement)(U.a,{timeout:320,classNames:"slide",key:e,onEnter:()=>{const M=N.current.querySelector(".slide-enter");L(M.clientHeight)}},Object(u.createElement)("div",{className:"woocommerce-marketing-slider__slide"},M))))};h.propTypes={animationKey:n.a.any.isRequired,animate:n.a.oneOf([null,"left","right"])};var b=h},121:function(M,e,t){},162:function(M,e,t){"use strict";t.d(e,"a",(function(){return N}));var j=t(15),L=t(13);const N=(M,e={})=>{const{pathname:t,search:N}=window.location,u=Object(L.f)("connectNonce","");return e={"wccom-site":Object(L.f)("siteUrl"),"wccom-back":t+N,"wccom-woo-version":Object(L.f)("wcVersion"),"wccom-connect-nonce":u,...e},Object(j.addQueryArgs)(M,e)}},163:function(M,e,t){},170:function(M,e,t){"use strict";var j=t(35),L=t.n(j),N=t(0),u=t(2),c=t(14),i=t(6),g=t.n(i),D=t(7),I=t(1),n=t.n(I),y=(t(121),t(16)),z=t(102),a=t(162);const A=({title:M,description:e,url:t,product:j,category:L})=>{const u="woocommerce-marketing-recommended-extensions-item",c=Object(a.a)(t);return"coupons"===L&&"automatewoo"===j&&(j="automatewoo-alt"),Object(N.createElement)("a",{href:c,className:u,onClick:()=>{Object(y.recordEvent)("marketing_recommended_extension",{name:M})}},Object(N.createElement)(z.b,{product:j}),Object(N.createElement)("div",{className:u+"__text"},Object(N.createElement)("h4",null,M),Object(N.createElement)("p",null,e)))};A.propTypes={title:n.a.string.isRequired,description:n.a.string.isRequired,url:n.a.string.isRequired,product:n.a.string.isRequired,category:n.a.string.isRequired};var T=A;var r=()=>{const M="is-loading woocommerce-marketing-recommended-extensions-item";return Object(N.createElement)("div",{className:M,"aria-hidden":"true"},Object(N.createElement)("div",{className:"woocommerce-admin-marketing-product-icon is-placeholder"}),Object(N.createElement)("div",{className:M+"__text"},Object(N.createElement)("h4",{className:"is-placeholder","aria-hidden":"true"}),Object(N.createElement)("p",null,Object(N.createElement)("span",{className:"is-placeholder"}),Object(N.createElement)("span",{className:"is-placeholder"}),Object(N.createElement)("span",{className:"is-placeholder"}))))},S=t(46),O=t(77);const o=({extensions:M,isLoading:e,title:t,description:j,category:u})=>{if(0===M.length&&!e)return null;const c=u?"woocommerce-marketing-recommended-extensions-card__category-"+u:"";return Object(N.createElement)(O.a,{title:t,description:j,className:g()("woocommerce-marketing-recommended-extensions-card",c)},e?Object(N.createElement)("div",{className:g()("woocommerce-marketing-recommended-extensions-card__items","woocommerce-marketing-recommended-extensions-card__items--count-5")},[...Array(5).keys()].map(M=>Object(N.createElement)(r,{key:M}))):Object(N.createElement)("div",{className:g()("woocommerce-marketing-recommended-extensions-card__items","woocommerce-marketing-recommended-extensions-card__items--count-"+M.length)},M.map(M=>Object(N.createElement)(T,L()({key:M.product,category:u},M)))))};o.propTypes={extensions:n.a.arrayOf(n.a.object).isRequired,isLoading:n.a.bool.isRequired,title:n.a.string,description:n.a.string,category:n.a.string},o.defaultProps={title:Object(u.__)("Recommended extensions",'woocommerce'),description:Object(u.__)("Great marketing requires the right tools. Take your marketing to the next level with our recommended marketing extensions.",'woocommerce')};e.a=Object(c.compose)(Object(D.withSelect)((M,e)=>{const{getRecommendedPlugins:t,isResolving:j}=M(S.b);return{extensions:t(e.category),isLoading:j("getRecommendedPlugins",[e.category])}}),Object(D.withDispatch)(M=>{const{createNotice:e}=M("core/notices");return{createNotice:e}}))(o)},171:function(M,e,t){"use strict";var j=t(0),L=t(14),N=t(2),u=t(6),c=t.n(u),i=t(7),g=t(1),D=t.n(g),I=t(21),n=t(16),y=(t(163),t(102)),z=t(46),a=t(77),A=t(18),T=t.n(A);var r=()=>T()({mixedString:Object(N.__)("Read {{link}}the WooCommerce blog{{/link}} for more tips on marketing your store",'woocommerce'),components:{link:Object(j.createElement)(I.Link,{type:"external",href:"https://woocommerce.com/blog/marketing/coupons/?utm_medium=product",target:"_blank"})}});var S=M=>{const e="woocommerce-marketing-knowledgebase-card__post";return Object(j.createElement)("div",{className:"is-loading "+e,key:M,"aria-hidden":"true"},Object(j.createElement)("div",{className:e+"-img is-placeholder"}),Object(j.createElement)("div",{className:e+"-text"},Object(j.createElement)("h3",{className:"is-placeholder","aria-hidden":"true"}),Object(j.createElement)("p",{className:e+"-meta is-placeholder"})))};const O=({posts:M,isLoading:e,error:t,title:L,description:u,category:i})=>{const[g,D]=Object(j.useState)(1),[z,A]=Object(j.useState)(null),T=M=>{let e;M>g?(e="left",Object(n.recordEvent)("marketing_knowledge_carousel",{direction:"forward",page:M})):(e="right",Object(n.recordEvent)("marketing_knowledge_carousel",{direction:"back",page:M})),D(M),A(e)},O=()=>{const e=M.slice(2*(g-1),2*(g-1)+2),t=c()("woocommerce-marketing-knowledgebase-card__page",{"page-with-single-post":1===e.length}),L=e.map((M,e)=>Object(j.createElement)("a",{className:"woocommerce-marketing-knowledgebase-card__post",href:M.link,key:e,onClick:()=>{(M=>{Object(n.recordEvent)("marketing_knowledge_article",{title:M.title})})(M)},target:"_blank",rel:"noopener noreferrer"},M.image&&Object(j.createElement)("div",{className:"woocommerce-marketing-knowledgebase-card__post-img"},Object(j.createElement)("img",{src:M.image,alt:""})),Object(j.createElement)("div",{className:"woocommerce-marketing-knowledgebase-card__post-text"},Object(j.createElement)("h3",null,M.title),Object(j.createElement)("p",{className:"woocommerce-marketing-knowledgebase-card__post-meta"},Object(N.__)("By",'woocommerce')+" ",M.author_name,M.author_avatar&&Object(j.createElement)("img",{src:M.author_avatar.replace("s=96","s=32"),className:"woocommerce-gravatar",alt:"",width:"16",height:"16"})))));return Object(j.createElement)("div",{className:t},L)},o=i?"woocommerce-marketing-knowledgebase-card__category-"+i:"";return Object(j.createElement)(a.a,{title:L,description:u,className:c()("woocommerce-marketing-knowledgebase-card",o)},e?Object(j.createElement)("div",{className:"woocommerce-marketing-knowledgebase-card__posts"},Object(j.createElement)("div",{className:"woocommerce-marketing-knowledgebase-card__page"},Object(j.createElement)(S,null),Object(j.createElement)(S,null))):t?(()=>{const M=Object(N.__)("Oops, our posts aren't loading right now",'woocommerce');return Object(j.createElement)(I.EmptyContent,{title:M,message:Object(j.createElement)(r,null),illustration:"",actionLabel:""})})():0===M.length?(()=>{const M=Object(N.__)("No posts yet",'woocommerce');return Object(j.createElement)(I.EmptyContent,{title:M,message:Object(j.createElement)(r,null),illustration:"",actionLabel:""})})():Object(j.createElement)("div",{className:"woocommerce-marketing-knowledgebase-card__posts"},Object(j.createElement)(y.c,{animationKey:g,animate:z},O()),Object(j.createElement)(I.Pagination,{page:g,perPage:2,total:M.length,onPageChange:T,showPagePicker:!1,showPerPagePicker:!1,showPageArrowsLabel:!1})))};O.propTypes={posts:D.a.arrayOf(D.a.object).isRequired,isLoading:D.a.bool.isRequired,title:D.a.string,description:D.a.string,category:D.a.string},O.defaultProps={title:Object(N.__)("WooCommerce knowledge base",'woocommerce'),description:Object(N.__)("Learn the ins and outs of successful marketing from the experts at WooCommerce.",'woocommerce')};e.a=Object(L.compose)(Object(i.withSelect)((M,e)=>{const{getBlogPosts:t,getBlogPostsError:j,isResolving:L}=M(z.b);return{posts:t(e.category),isLoading:L("getBlogPosts",[e.category]),error:j(e.category)}}),Object(i.withDispatch)(M=>{const{createNotice:e}=M("core/notices");return{createNotice:e}}))(O)},272:function(M,e,t){},273:function(M,e,t){},274:function(M,e,t){},275:function(M,e,t){},276:function(M,e,t){"use strict";var j={};t.r(j),t.d(j,"receiveInstalledPlugins",(function(){return I})),t.d(j,"receiveActivatingPlugin",(function(){return n})),t.d(j,"removeActivatingPlugin",(function(){return y})),t.d(j,"receiveRecommendedPlugins",(function(){return z})),t.d(j,"receiveBlogPosts",(function(){return a})),t.d(j,"handleFetchError",(function(){return A})),t.d(j,"setError",(function(){return T})),t.d(j,"activateInstalledPlugin",(function(){return r})),t.d(j,"loadInstalledPluginsAfterActivation",(function(){return S}));var L={};t.r(L),t.d(L,"getInstalledPlugins",(function(){return O})),t.d(L,"getActivatingPlugins",(function(){return o})),t.d(L,"getRecommendedPlugins",(function(){return E})),t.d(L,"getBlogPosts",(function(){return l})),t.d(L,"getBlogPostsError",(function(){return C}));var N={};t.r(N),t.d(N,"getRecommendedPlugins",(function(){return s})),t.d(N,"getBlogPosts",(function(){return w}));var u=t(10),c=t(7),i=t(46),g=t(2);var D={SET_INSTALLED_PLUGINS:"SET_INSTALLED_PLUGINS",SET_ACTIVATING_PLUGIN:"SET_ACTIVATING_PLUGIN",REMOVE_ACTIVATING_PLUGIN:"REMOVE_ACTIVATING_PLUGIN",SET_RECOMMENDED_PLUGINS:"SET_RECOMMENDED_PLUGINS",SET_BLOG_POSTS:"SET_BLOG_POSTS",SET_ERROR:"SET_ERROR"};function I(M){return{type:D.SET_INSTALLED_PLUGINS,plugins:M}}function n(M){return{type:D.SET_ACTIVATING_PLUGIN,pluginSlug:M}}function y(M){return{type:D.REMOVE_ACTIVATING_PLUGIN,pluginSlug:M}}function z(M,e){return{type:D.SET_RECOMMENDED_PLUGINS,data:{plugins:M,category:e}}}function a(M,e){return{type:D.SET_BLOG_POSTS,data:{posts:M,category:e}}}function A(M,e){const{createNotice:t}=Object(c.dispatch)("core/notices");t("error",e),console.log(M)}function T(M,e){return{type:D.SET_ERROR,category:M,error:e}}function*r(M){const{createNotice:e}=Object(c.dispatch)("core/notices");yield n(M);try{if(!(yield Object(u.apiFetch)({path:i.a+"/overview/activate-plugin",method:"POST",data:{plugin:M}})))throw new Error;yield e("success",Object(g.__)("The extension has been successfully activated.",'woocommerce')),yield S(M)}catch(e){yield A(e,Object(g.__)("There was an error trying to activate the extension.",'woocommerce')),yield y(M)}return!0}function*S(M){try{const e=yield Object(u.apiFetch)({path:i.a+"/overview/installed-plugins"});if(!e)throw new Error;yield I(e),yield y(M)}catch(M){yield A(M,Object(g.__)("There was an error loading installed extensions.",'woocommerce'))}}function O(M){return M.installedPlugins}function o(M){return M.activatingPlugins}function E(M,e){return M.recommendedPlugins[e]||[]}function l(M,e){return M.blogPosts[e]||[]}function C(M,e){return M.errors.blogPosts&&M.errors.blogPosts[e]}function*s(M){try{const e=yield M?"&category="+M:"",t=yield Object(u.apiFetch)({path:`${i.a}/recommended?per_page=6${e}`});if(!t)throw new Error;yield z(t,M)}catch(M){yield A(M,Object(g.__)("There was an error loading recommended extensions.",'woocommerce'))}}function*w(M){try{const e=yield M?"?category="+M:"",t=yield Object(u.apiFetch)({path:`${i.a}/knowledge-base${e}`,method:"GET"});if(!t)throw new Error;yield a(t,M)}catch(e){yield T(M,e)}}var x=t(13),m=t(4);const{installedExtensions:d}=Object(x.f)("marketing",{}),k={installedPlugins:d,activatingPlugins:[],recommendedPlugins:{},blogPosts:{},errors:{}};var Y=(M=k,e)=>{switch(e.type){case D.SET_INSTALLED_PLUGINS:return{...M,installedPlugins:e.plugins};case D.SET_ACTIVATING_PLUGIN:return{...M,activatingPlugins:[...M.activatingPlugins,e.pluginSlug]};case D.REMOVE_ACTIVATING_PLUGIN:return{...M,activatingPlugins:Object(m.without)(M.activatingPlugins,e.pluginSlug)};case D.SET_RECOMMENDED_PLUGINS:return{...M,recommendedPlugins:{...M.recommendedPlugins,[e.data.category]:e.data.plugins}};case D.SET_BLOG_POSTS:return{...M,blogPosts:{...M.blogPosts,[e.data.category]:e.data.posts}};case D.SET_ERROR:return{...M,errors:{...M.errors,blogPosts:{...M.errors.blogPosts,[e.category]:e.error}}};default:return M}};Object(c.registerStore)(i.b,{actions:j,selectors:L,resolvers:N,controls:u.controls,reducer:Y})},46:function(M,e,t){"use strict";t.d(e,"b",(function(){return j})),t.d(e,"a",(function(){return L}));const j="wc/marketing",L="/wc-admin/marketing"},564:function(M,e,t){},565:function(M,e,t){},566:function(M,e,t){"use strict";var j=Object.assign||function(M){for(var e,t=1;tObject(j.createElement)("li",{key:M.key},Object(j.createElement)(a.Link,{href:M.href,type:"external",onClick:this.onLinkClick.bind(this,M)},M.text))))}onLinkClick(M){const{name:e}=this.props;Object(A.recordEvent)("marketing_installed_options",{name:e,link:M.key})}onActivateClick(){const{activatePlugin:M,name:e}=this.props;Object(A.recordEvent)("marketing_installed_activate",{name:e}),M()}onFinishSetupClick(){const{name:M}=this.props;Object(A.recordEvent)("marketing_installed_finish_setup",{name:M})}getActivateButton(){const{isLoading:M}=this.props;return Object(j.createElement)(T.a,{isSecondary:!0,onClick:this.onActivateClick,disabled:M},Object(i.__)("Activate",'woocommerce'))}getFinishSetupButton(){return Object(j.createElement)(T.a,{isSecondary:!0,href:this.props.settingsUrl,onClick:this.onFinishSetupClick},Object(i.__)("Finish setup",'woocommerce'))}render(){const{name:M,description:e,status:t,slug:L}=this.props;let N=null;switch(t){case"installed":N=this.getActivateButton();break;case"activated":N=this.getFinishSetupButton();break;case"configured":N=this.getLinks()}return Object(j.createElement)("div",{className:"woocommerce-marketing-installed-extensions-card__item"},Object(j.createElement)(T.b,{product:L}),Object(j.createElement)("div",{className:"woocommerce-marketing-installed-extensions-card__item-text-and-actions"},Object(j.createElement)("div",{className:"woocommerce-marketing-installed-extensions-card__item-text"},Object(j.createElement)("h4",null,M),"configured"===t||Object(j.createElement)("p",{className:"woocommerce-marketing-installed-extensions-card__item-description"},e)),Object(j.createElement)("div",{className:"woocommerce-marketing-installed-extensions-card__item-actions"},N)))}}r.defaultProps={isLoading:!1},r.propTypes={name:n.a.string.isRequired,slug:n.a.string.isRequired,description:n.a.string.isRequired,status:n.a.string.isRequired,settingsUrl:n.a.string,docsUrl:n.a.string,supportUrl:n.a.string,dashboardUrl:n.a.string,activatePlugin:n.a.func.isRequired};var S=r,O=t(46);class o extends j.Component{activatePlugin(M){const{activateInstalledPlugin:e}=this.props;e(M)}isActivatingPlugin(M){const{activatingPlugins:e}=this.props;return e.includes(M)}render(){const{plugins:M}=this.props;if(0===M.length)return null;const e=Object(i.__)("Installed marketing extensions",'woocommerce');return Object(j.createElement)(y.Card,{className:"woocommerce-marketing-installed-extensions-card"},Object(j.createElement)(y.CardHeader,null,Object(j.createElement)(z.Text,{variant:"title.small",size:"20",lineHeight:"28px"},e)),M.map(M=>Object(j.createElement)(S,c()({key:M.slug},M,{activatePlugin:()=>this.activatePlugin(M.slug),isLoading:this.isActivatingPlugin(M.slug)}))))}}o.propTypes={plugins:n.a.arrayOf(n.a.object).isRequired,activatingPlugins:n.a.arrayOf(n.a.string).isRequired};var E=Object(g.compose)(Object(D.withSelect)(M=>{const{getInstalledPlugins:e,getActivatingPlugins:t}=M(O.b);return{plugins:e(),activatingPlugins:t()}}),Object(D.withDispatch)(M=>{const{activateInstalledPlugin:e}=M(O.b);return{activateInstalledPlugin:e}}))(o),l=t(170),C=t(171),s=t(566),w=t.n(s),x=(t(567),t(568)),m=t.n(x);const d=({isHidden:M,updateOptions:e})=>M?null:Object(j.createElement)(y.Card,{className:"woocommerce-marketing-overview-welcome-card"},Object(j.createElement)(y.CardBody,null,Object(j.createElement)(y.Button,{label:Object(i.__)("Hide",'woocommerce'),onClick:()=>{e({woocommerce_marketing_overview_welcome_hidden:"yes"}),Object(A.recordEvent)("marketing_intro_close",{})},className:"woocommerce-marketing-overview-welcome-card__hide-button"},Object(j.createElement)(w.a,null)),Object(j.createElement)("img",{src:m.a,alt:""}),Object(j.createElement)("h3",null,Object(i.__)("Grow your customer base and increase your sales with marketing tools built for WooCommerce",'woocommerce'))));d.propTypes={isHidden:n.a.bool.isRequired,updateOptions:n.a.func.isRequired};var k=Object(g.compose)(Object(D.withSelect)(M=>{const{getOption:e,isOptionsUpdating:t}=M(N.OPTIONS_STORE_NAME),j=t();return{isHidden:"yes"===e("woocommerce_marketing_overview_welcome_hidden")||j}}),Object(D.withDispatch)(M=>{const{updateOptions:e}=M(N.OPTIONS_STORE_NAME);return{updateOptions:e}}))(d);t(276);e.default=Object(N.withOptionsHydration)({...Object(L.f)("preloadOptions",{})})(()=>{const M=Object(L.f)("allowMarketplaceSuggestions",!1);return Object(j.createElement)("div",{className:"woocommerce-marketing-overview"},Object(j.createElement)(k,null),Object(j.createElement)(E,null),M&&Object(j.createElement)(l.a,{category:"marketing"}),Object(j.createElement)(C.a,{category:"marketing"}))})},77:function(M,e,t){"use strict";var j=t(0),L=t(3),N=t(1),u=t.n(N),c=t(6),i=t.n(c),g=t(20);t(273);const D=M=>{const{title:e,description:t,children:N,className:u}=M;return Object(j.createElement)(L.Card,{className:i()(u,"woocommerce-admin-marketing-card")},Object(j.createElement)(L.CardHeader,null,Object(j.createElement)("div",null,Object(j.createElement)(g.Text,{variant:"title.small",as:"p",size:"20",lineHeight:"28px"},e),Object(j.createElement)(g.Text,{variant:"subtitle.small",as:"p",className:"woocommerce-admin-marketing-card-subtitle",size:"14",lineHeight:"20px"},t))),Object(j.createElement)(L.CardBody,null,N))};D.propTypes={title:u.a.string,description:u.a.string,className:u.a.string,children:u.a.node},e.a=D}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/payment-recommendations.js b/packages/woocommerce-admin/dist/chunks/payment-recommendations.js new file mode 100644 index 0000000..223f37b --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/payment-recommendations.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[45],{537:function(e,t,n){"use strict";var c=Object.assign||function(e){for(var t,n=1;n{const[e,t]=Object(c.useState)(null),{updateOptions:n}=Object(a.useDispatch)(m.OPTIONS_STORE_NAME),{installAndActivatePlugins:u}=Object(a.useDispatch)(m.PLUGINS_STORE_NAME),{displayable:O,recommendedPlugins:f,isLoading:j}=Object(a.useSelect)(g),h=Object(c.useRef)(!1),y=O&&f&&f.length>0;if(Object(c.useEffect)(()=>{if((y||v&&!j)&&!h.current){h.current=!0;const e=(f||[]).reduce((e,t)=>t.product?{...e,[t.product.replace(/\-/g,"_")+"_displayed"]:!0}:e,{woocommerce_payments_displayed:!!v});Object(d.recordEvent)("settings_payments_recommendations_pageview",e)}},[y,v,j]),!y)return null;const E=()=>{Object(d.recordEvent)("settings_payments_recommendations_dismiss",{}),n({[w]:"yes"})},N=(f||[]).map(n=>({key:n.slug,title:Object(c.createElement)(c.Fragment,null,n.title,n.recommended&&Object(c.createElement)(s.Pill,null,Object(o.__)("Recommended",'woocommerce'))),content:Object(r.decodeEntities)(n.copy),after:Object(c.createElement)(i.Button,{isSecondary:!0,onClick:()=>(n=>{e||(t(n.product),Object(d.recordEvent)("settings_payments_recommendations_setup",{extension_selected:n.product}),u([n.product]).then(()=>{window.location.href=Object(p.e)(n["setup-link"].replace("/wp-admin/",""))}).catch(e=>{Object(b.a)(e),t(null)}))})(n),isBusy:e===n.product,disabled:!!e},n["button-text"]),before:Object(c.createElement)("img",{src:n.icon,alt:""})}));return Object(c.createElement)(i.Card,{size:"medium",className:"woocommerce-recommended-payments-card"},Object(c.createElement)(i.CardHeader,null,Object(c.createElement)("div",{className:"woocommerce-recommended-payments-card__header"},Object(c.createElement)(l.Text,{variant:"title.small",as:"p",size:"20",lineHeight:"28px"},Object(o.__)("Recommended ways to get paid",'woocommerce')),Object(c.createElement)(l.Text,{className:"woocommerce-recommended-payments__header-heading",variant:"caption",as:"p",size:"12",lineHeight:"16px"},Object(o.__)('We recommend adding one of the following payment extensions to your store. The extension will be installed and activated for you when you click "Get started".','woocommerce'))),Object(c.createElement)("div",{className:"woocommerce-card__menu woocommerce-card__header-item"},Object(c.createElement)(s.EllipsisMenu,{label:Object(o.__)("Task List Options",'woocommerce'),renderContent:()=>Object(c.createElement)("div",{className:"woocommerce-review-activity-card__section-controls"},Object(c.createElement)(i.Button,{onClick:E},Object(o.__)("Hide this",'woocommerce')))}))),Object(c.createElement)(s.List,{items:N}),Object(c.createElement)(i.CardFooter,null,Object(c.createElement)(i.Button,{href:"https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_source=payments_recommendations",target:"_blank",isTertiary:!0},Object(o.__)("See more options",'woocommerce'),Object(c.createElement)(_.a,{size:18}))))}},61:function(e,t,n){"use strict";var c=Object.assign||function(e){for(var t,n=1;n{const t=a.getCurrencyConfig(),o=Object(s.applyFilters)("woocommerce_admin_report_currency",t,e);return c()(o)},m=Object(r.createContext)(a)},507:function(e,t,o){"use strict";o.d(t,"a",(function(){return s}));var r=o(7);function s(e){const{createNotice:t}=Object(r.dispatch)("core/notices");e.error_data&&e.errors&&Object.keys(e.errors).length?Object.keys(e.errors).forEach(o=>{t("error",e.errors[o].join(" "))}):e.message&&t(e.code?"error":"success",e.message)}},509:function(e,t,o){"use strict";var r=o(53);const s=["a","b","em","i","strong","p","br"],n=["target","href","rel","name","download"];t.a=e=>({__html:Object(r.sanitize)(e,{ALLOWED_TAGS:s,ALLOWED_ATTR:n})})},514:function(e,t,o){"use strict";var r=o(0),s=o(2),n=o(14),c=o(7),i=o(18),a=o.n(i),l=o(3),m=o(21),d=o(11),u=o(122);class p extends r.Component{constructor(e){super(e),this.state={isLoadingScripts:!1,isRequestStarted:!1}}async componentDidUpdate(e,t){const{hasErrors:o,isRequesting:r,onClose:n,onContinue:c,createNotice:i}=this.props,{isLoadingScripts:a,isRequestStarted:l}=this.state;if(!l)return;const m=!r&&!a&&(e.isRequesting||t.isLoadingScripts)&&!o,d=!r&&e.isRequesting&&o;m&&(n(),c()),d&&(i("error",Object(s.__)("There was a problem updating your preferences",'woocommerce')),n())}updateTracking({allowTracking:e}){const{updateOptions:t}=this.props;e&&"function"==typeof window.wcTracks.enable?(this.setState({isLoadingScripts:!0}),window.wcTracks.enable(()=>{this._isMounted&&(Object(u.initializeExPlat)(),this.setState({isLoadingScripts:!1}))})):e||(window.wcTracks.isEnabled=!1);const o=e?"yes":"no";this.setState({isRequestStarted:!0}),t({woocommerce_allow_tracking:o})}componentDidMount(){this._isMounted=!0}componentWillUnmount(){this._isMounted=!1}render(){const{allowTracking:e,isResolving:t,onClose:o,onContinue:n}=this.props;if(t)return null;if(e)return o(),n(),null;const{isRequesting:c,title:i=Object(s.__)("Build a better WooCommerce",'woocommerce'),message:d=a()({mixedString:Object(s.__)("Get improved features and faster fixes by sharing non-sensitive data via {{link}}usage tracking{{/link}} that shows us how WooCommerce is used. No personal data is tracked or stored.",'woocommerce'),components:{link:Object(r.createElement)(m.Link,{href:"https://woocommerce.com/usage-tracking?utm_medium=product",target:"_blank",type:"external"})}}),dismissActionText:u=Object(s.__)("No thanks",'woocommerce'),acceptActionText:p=Object(s.__)("Yes, count me in!",'woocommerce')}=this.props,{isRequestStarted:b}=this.state,h=b&&c;return Object(r.createElement)(l.Modal,{title:i,isDismissible:this.props.isDismissible,onRequestClose:()=>this.props.onClose(),className:"woocommerce-usage-modal"},Object(r.createElement)("div",{className:"woocommerce-usage-modal__wrapper"},Object(r.createElement)("div",{className:"woocommerce-usage-modal__message"},d),Object(r.createElement)("div",{className:"woocommerce-usage-modal__actions"},Object(r.createElement)(l.Button,{isSecondary:!0,isBusy:h,onClick:()=>this.updateTracking({allowTracking:!1})},u),Object(r.createElement)(l.Button,{isPrimary:!0,isBusy:h,onClick:()=>this.updateTracking({allowTracking:!0})},p))))}}t.a=Object(n.compose)(Object(c.withSelect)(e=>{const{getOption:t,getOptionsUpdatingError:o,isOptionsUpdating:r,hasFinishedResolution:s}=e(d.OPTIONS_STORE_NAME);return{allowTracking:"yes"===t("woocommerce_allow_tracking"),isRequesting:Boolean(r()),isResolving:!s("getOption",["woocommerce_allow_tracking"])||void 0===t("woocommerce_allow_tracking"),hasErrors:Boolean(o())}}),Object(c.withDispatch)(e=>{const{createNotice:t}=e("core/notices"),{updateOptions:o}=e(d.OPTIONS_STORE_NAME);return{createNotice:t,updateOptions:o}}))(p)},526:function(e,t,o){"use strict";o.d(t,"a",(function(){return r})),o.d(t,"b",(function(){return s}));const r=(e,t,o="undefined")=>e&&Array.isArray(e)&&e.length?t?e.reduce((e,r)=>(r[t]||(r[t]=o),(e[r[t]]=e[r[t]]||[]).push(r),e),{}):e:{},s=(e,t)=>Object.entries(e).reduce((e,[o])=>({...e,[o]:t}),{})},527:function(e,t,o){"use strict";o.d(t,"b",(function(){return u})),o.d(t,"a",(function(){return p}));var r=o(35),s=o.n(r),n=o(0),c=o(2),i=o(28),a=o(4),l=o(13),m=o(21);const{countries:d}=Object(l.f)("dataEndpoints",{countries:{}});function u(e){const t={};return e.addressLine1.trim().length||(t.addressLine1=Object(c.__)("Please add an address",'woocommerce')),e.countryState.trim().length||(t.countryState=Object(c.__)("Please select a country / region",'woocommerce')),e.city.trim().length||(t.city=Object(c.__)("Please add a city",'woocommerce')),e.postCode.trim().length||(t.postCode=Object(c.__)("Please add a post code",'woocommerce')),t}function p(e){const{getInputProps:t,setValue:o}=e,r=Object(n.useMemo)(()=>d.reduce((e,t)=>{if(!t.states.length)return e.push({key:t.code,label:Object(i.decodeEntities)(t.name)}),e;const o=t.states.map(e=>({key:t.code+":"+e.code,label:Object(i.decodeEntities)(t.name)+" — "+Object(i.decodeEntities)(e.name)}));return e.push(...o),e},[]),[]),l=function(e,t,o){const[r,s]=Object(n.useState)(""),[c,i]=Object(n.useState)(""),l=Object(n.useRef)();return Object(n.useEffect)(()=>{const o=e.find(e=>e.key===t),n=o?o.label.split(/\u2013|\u2014|\-/):[],a=(n[0]||"").trim(),m=(n[1]||"").trim();l.current||a===r&&m===c||(s(a),i(m)),l.current=!1},[t]),Object(n.useEffect)(()=>{r||c||!t||(l.current=!0,o("countryState",""));let s=[];const n=new RegExp(Object(a.escapeRegExp)(r),"i"),i=new RegExp(Object(a.escapeRegExp)(c.replace(/\s/g,""))+"$","i");if((c.length||r.length)&&(s=e.filter(e=>(r.length?n:i).test(e.label))),r.length&&c.length){const e=c.length<3;s=s.filter(t=>i.test((e?t.key:t.label).replace("-","").replace(/\s/g,"")));const t=r.length<3;if(s.length>1){let e=[];e=s.filter(e=>n.test(t?e.key:e.label)),e.length>0&&(s=e)}if(s.length>1){let t=[];t=s.filter(t=>i.test((e?t.key:t.label).replace("-","").replace(/\s/g,""))),1===t.length&&(s=t)}}1===s.length&&t!==s[0].key&&(l.current=!0,o("countryState",s[0].key))},[r,c,e,o]),Object(n.createElement)(n.Fragment,null,Object(n.createElement)("input",{onChange:e=>s(e.target.value),value:r,name:"country",type:"text",className:"woocommerce-select-control__autofill-input",tabIndex:"-1",autoComplete:"country"}),Object(n.createElement)("input",{onChange:e=>i(e.target.value),value:c,name:"state",type:"text",className:"woocommerce-select-control__autofill-input",tabIndex:"-1",autoComplete:"address-level1"}))}(r,t("countryState").value,o);return Object(n.createElement)("div",{className:"woocommerce-store-address-fields"},Object(n.createElement)(m.TextControl,s()({label:Object(c.__)("Address line 1",'woocommerce'),required:!0,autoComplete:"address-line1"},t("addressLine1"))),Object(n.createElement)(m.TextControl,s()({label:Object(c.__)("Address line 2 (optional)",'woocommerce'),required:!0,autoComplete:"address-line2"},t("addressLine2"))),Object(n.createElement)(m.SelectControl,s()({label:Object(c.__)("Country / Region",'woocommerce'),required:!0,autoComplete:"new-password",options:r,excludeSelectedOptions:!1,showAllOnFocus:!0,isSearchable:!0},t("countryState"),{controlClassName:t("countryState").className}),l),Object(n.createElement)(m.TextControl,s()({label:Object(c.__)("City",'woocommerce'),required:!0},t("city"),{autoComplete:"address-level2"})),Object(n.createElement)(m.TextControl,s()({label:Object(c.__)("Post code",'woocommerce'),required:!0,autoComplete:"postal-code"},t("postCode"))))}},569:function(e,t,o){},570:function(e,t,o){},571:function(e,t,o){},572:function(e,t,o){},573:function(e,t,o){},574:function(e,t,o){"use strict";var r=Object.assign||function(e){for(var t,o=1;oObject(r.createElement)("div",null,"Settings page")},613:function(e,t,o){"use strict";o.r(t);var r=o(0),s=o(2),n=o(30),c=o(14),i=o(4),a=o(7),l=o(12),m=o(11),d=o(16),u=o(13),p=o(35),b=o.n(p),h=o(21),_=o(3),O=o(501),g=o(507);const j=[{key:"shopify",label:Object(s.__)("Shopify",'woocommerce')},{key:"bigcommerce",label:Object(s.__)("BigCommerce",'woocommerce')},{key:"magento",label:Object(s.__)("Magento",'woocommerce')},{key:"wix",label:Object(s.__)("Wix",'woocommerce')},{key:"amazon",label:Object(s.__)("Amazon",'woocommerce')},{key:"ebay",label:Object(s.__)("eBay",'woocommerce')},{key:"etsy",label:Object(s.__)("Etsy",'woocommerce')},{key:"squarespace",label:Object(s.__)("Squarespace",'woocommerce')},{key:"other",label:Object(s.__)("Other",'woocommerce')}],w=[{key:"no",label:Object(s.__)("No",'woocommerce')},{key:"other",label:Object(s.__)("Yes, on another platform",'woocommerce')},{key:"other-woocommerce",label:Object(s.__)("Yes, I own a different store powered by WooCommerce",'woocommerce')},{key:"brick-mortar",label:Object(s.__)("Yes, in person at physical stores and/or events",'woocommerce')},{key:"brick-mortar-other",label:Object(s.__)("Yes, on another platform and in person at physical stores and/or events",'woocommerce')}];var f=o(60),C=o(120);const E=(e,t)=>Object(C.formatValue)(e,"number",t),y=(e,t,o=!1,r=E)=>o?Object(s.sprintf)(Object(s._x)("%1$s - %2$s","store product count or revenue range",'woocommerce'),r(e,t),r(e,o)):Object(s.sprintf)(Object(s._x)("%s+","store product count or revenue",'woocommerce'),r(e,t)),v={US:1,EU:.9,IN:71.24,GB:.76,BR:4.19,VN:23172.5,ID:14031,BD:84.87,PK:154.8,RU:63.74,TR:5.75,MX:19.37,CA:1.32},S=(e,t)=>{const o=Object(f.c)(t);if("US"===o)return e;const r=v[o]||v.US,s=r.toString().split(".")[0].length,n=Math.pow(10,2+s);return Math.round(e*r/n)*n},k=(e,t,o)=>[{key:"none",label:Object(s.sprintf)(Object(s.__)("%s (I'm just getting started)",'woocommerce'),o(0))},{key:"up-to-2500",label:Object(s.sprintf)(Object(s.__)("Up to %s",'woocommerce'),o(S(2500,t)))},{key:"2500-10000",label:y(e,S(2500,t),S(1e4,t),(e,t)=>o(t))},{key:"10000-50000",label:y(e,S(1e4,t),S(5e4,t),(e,t)=>o(t))},{key:"50000-250000",label:y(e,S(5e4,t),S(25e4,t),(e,t)=>o(t))},{key:"more-than-250000",label:Object(s.sprintf)(Object(s.__)("More than %s",'woocommerce'),o(S(25e4,t)))},{key:"rather-not-say",label:Object(s.__)("I'd rather not say",'woocommerce')}];var N=o(20),T=o(116),P=o(277),x=o(278),I=o(18),M=o.n(I);const A=()=>Object(r.createElement)("svg",{width:"200",height:"148",viewBox:"0 0 200 148",fill:"none",xmlns:"http://www.w3.org/2000/svg"},Object(r.createElement)("g",{clipPath:"url(#clip0)"},Object(r.createElement)("path",{d:"M197.563 2.53875e-09H62.909C62.3961 0.000450584 61.9043 0.205742 61.5416 0.570805C61.179 0.935868 60.975 1.43087 60.9746 1.94714V50.9404H93.5623C94.4445 50.9415 95.2902 51.2947 95.9141 51.9226C96.5379 52.5505 96.8888 53.4019 96.8899 54.2899V95.7402H197.563C197.843 95.7402 198.119 95.6791 198.373 95.5612C198.627 95.4432 198.853 95.2712 199.034 95.0569C199.05 95.0402 199.064 95.0222 199.076 95.0033C199.192 94.8612 199.285 94.7024 199.354 94.5322C199.451 94.2981 199.501 94.0468 199.5 93.7931V1.94714C199.499 1.43051 199.295 0.935241 198.932 0.57014C198.569 0.20504 198.077 -2.63458e-05 197.563 2.53875e-09Z",fill:"#F2F2F2"}),Object(r.createElement)("path",{d:"M199.222 7.80469H61.25V8.36132H199.222V7.80469Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M65.95 5.84371C66.8662 5.84371 67.609 5.09607 67.609 4.17381C67.609 3.25155 66.8662 2.50391 65.95 2.50391C65.0338 2.50391 64.291 3.25155 64.291 4.17381C64.291 5.09607 65.0338 5.84371 65.95 5.84371Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M70.72 5.84371C71.6363 5.84371 72.379 5.09607 72.379 4.17381C72.379 3.25155 71.6363 2.50391 70.72 2.50391C69.8038 2.50391 69.061 3.25155 69.061 4.17381C69.061 5.09607 69.8038 5.84371 70.72 5.84371Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M75.4896 5.84371C76.4058 5.84371 77.1486 5.09607 77.1486 4.17381C77.1486 3.25155 76.4058 2.50391 75.4896 2.50391C74.5733 2.50391 73.8306 3.25155 73.8306 4.17381C73.8306 5.09607 74.5733 5.84371 75.4896 5.84371Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M164.842 19.957H95.6295C94.8646 19.957 94.1311 20.2629 93.5903 20.8073C93.0494 21.3516 92.7456 22.09 92.7456 22.8599C92.7456 23.6298 93.0494 24.3681 93.5903 24.9125C94.1311 25.4569 94.8646 25.7627 95.6295 25.7627H164.842C165.607 25.7627 166.341 25.4569 166.882 24.9125C167.422 24.3681 167.726 23.6298 167.726 22.8599C167.726 22.09 167.422 21.3516 166.882 20.8073C166.341 20.2629 165.607 19.957 164.842 19.957ZM164.842 25.3161H95.6295C94.9823 25.3161 94.3616 25.0573 93.904 24.5967C93.4464 24.1361 93.1893 23.5113 93.1893 22.8599C93.1893 22.2084 93.4464 21.5837 93.904 21.123C94.3616 20.6624 94.9823 20.4036 95.6295 20.4036H164.842C165.489 20.4036 166.11 20.6624 166.568 21.123C167.025 21.5837 167.283 22.2084 167.283 22.8599C167.283 23.5113 167.025 24.1361 166.568 24.5967C166.11 25.0573 165.489 25.3161 164.842 25.3161Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M186.022 43.0859H116.809C116.044 43.0859 115.31 43.3918 114.769 43.9362C114.229 44.4806 113.925 45.2189 113.925 45.9888C113.925 46.7587 114.229 47.497 114.769 48.0414C115.31 48.5858 116.044 48.8916 116.809 48.8916H186.022C186.786 48.8916 187.52 48.5858 188.061 48.0414C188.602 47.497 188.905 46.7587 188.905 45.9888C188.905 45.2189 188.602 44.4806 188.061 43.9362C187.52 43.3918 186.786 43.0859 186.022 43.0859Z",fill:"white"}),Object(r.createElement)("path",{d:"M186.022 53.8047H116.809C116.044 53.8047 115.31 54.1105 114.769 54.6549C114.229 55.1993 113.925 55.9376 113.925 56.7075C113.925 57.4774 114.229 58.2158 114.769 58.7601C115.31 59.3045 116.044 59.6104 116.809 59.6104H186.022C186.786 59.6104 187.52 59.3045 188.061 58.7601C188.602 58.2158 188.905 57.4774 188.905 56.7075C188.905 55.9376 188.602 55.1993 188.061 54.6549C187.52 54.1105 186.786 53.8047 186.022 53.8047Z",fill:"white"}),Object(r.createElement)("path",{d:"M186.022 64.5195H116.809C116.044 64.5195 115.31 64.8254 114.769 65.3698C114.229 65.9141 113.925 66.6525 113.925 67.4224C113.925 68.1923 114.229 68.9306 114.769 69.475C115.31 70.0194 116.044 70.3252 116.809 70.3252H186.022C186.786 70.3252 187.52 70.0194 188.061 69.475C188.602 68.9306 188.905 68.1923 188.905 67.4224C188.905 66.6525 188.602 65.9141 188.061 65.3698C187.52 64.8254 186.786 64.5195 186.022 64.5195Z",fill:"white"}),Object(r.createElement)("path",{d:"M105.623 38.2852H74.1183C73.4425 38.286 72.7947 38.5565 72.3168 39.0375C71.839 39.5185 71.5702 40.1706 71.5693 40.8508V50.9416H72.013V40.8508C72.0139 40.2891 72.2359 39.7506 72.6306 39.3533C73.0252 38.9561 73.5602 38.7326 74.1183 38.7317H105.623C106.182 38.7322 106.717 38.9556 107.112 39.3529C107.506 39.7502 107.728 40.289 107.729 40.8508V72.5633C107.728 73.1251 107.506 73.6638 107.112 74.0611C106.717 74.4585 106.182 74.6819 105.623 74.6824H96.8897V75.1289H105.623C106.299 75.1285 106.947 74.858 107.425 74.377C107.903 73.8959 108.172 73.2436 108.172 72.5633V40.8508C108.172 40.1705 107.903 39.5182 107.425 39.0371C106.947 38.556 106.299 38.2856 105.623 38.2852Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M23.9309 70.9116C23.8195 70.9162 19.0705 70.5847 18.9492 70.5806L19.3758 66.294L22.0808 66.212L27.2495 56.5756C26.5327 55.1996 27.4148 53.3739 28.9355 53.0925C32 52.3914 33.0526 57.2443 29.9789 57.8901L25.7036 69.6652C25.5695 70.03 25.3278 70.3449 25.011 70.5676C24.6942 70.7904 24.3174 70.9104 23.9309 70.9116Z",fill:"#FFB8B8"}),Object(r.createElement)("path",{d:"M11.4107 73.118C6.89154 73.1291 6.49482 66.2544 11.024 65.7699C23.0006 65.0415 21.485 62.0137 22.3945 70.9448C22.4224 71.2097 22.3448 71.475 22.1787 71.6824C22.0126 71.8898 21.7715 72.0223 21.5084 72.051L11.803 73.0968C11.6727 73.1109 11.5417 73.1179 11.4107 73.118Z",className:"fill-theme-color"}),Object(r.createElement)("path",{d:"M10.3793 51.3852C16.605 54.9512 11.494 64.3601 5.15222 61.0097C-1.0733 57.4438 4.03771 48.0349 10.3793 51.3852Z",fill:"#FFB8B8"}),Object(r.createElement)("path",{d:"M16.0395 132.376L18.759 132.376L20.053 121.816L16.0391 121.817L16.0395 132.376Z",fill:"#FFB8B8"}),Object(r.createElement)("path",{d:"M15.4567 134.915L24.0042 134.915C23.9716 130.476 18.2546 131.755 15.4565 131.591L15.4567 134.915Z",fill:"#2F2E41"}),Object(r.createElement)("path",{d:"M4.28218 132.376L7.00167 132.376L8.29564 121.816L4.28174 121.817L4.28218 132.376Z",fill:"#FFB8B8"}),Object(r.createElement)("path",{d:"M3.69937 134.915L12.2469 134.915C12.2142 130.476 6.49728 131.755 3.69922 131.591L3.69937 134.915Z",fill:"#2F2E41"}),Object(r.createElement)("path",{d:"M7.37266 128.688C6.71536 128.507 3.14362 129.056 2.72209 128.335C1.24999 113.483 1.57722 98.9486 4.845 90.9619L16.0806 90.2695C18.5931 94.8863 24.3684 125.522 20.8847 127.385L16.4048 127.546C16.1493 127.554 15.8988 127.474 15.6952 127.318C15.4916 127.163 15.3475 126.941 15.2869 126.691L11.539 105.229C10.6057 103.916 8.77111 127.832 8.46815 127.742C8.42864 128.006 8.29626 128.247 8.09515 128.42C7.89404 128.594 7.63762 128.689 7.37266 128.688Z",fill:"#2F2E41"}),Object(r.createElement)("path",{d:"M4.48843 92.8373C-1.18427 86.8634 2.43414 70.8475 2.25101 71.1881C2.26655 70.7958 3.55141 64.7536 6.87506 63.762C9.51196 62.886 12.4305 65.5063 12.7906 68.1566L16.7406 91.6368C16.7611 91.7646 16.7514 91.8955 16.7123 92.0188C16.6733 92.1422 16.6059 92.2546 16.5158 92.3469C16.71 92.9556 4.76613 92.7153 4.48843 92.8373Z",className:"fill-theme-color"}),Object(r.createElement)("path",{d:"M6.32941 55.5845C6.82759 55.2675 7.07312 54.7238 7.36309 54.2285C8.89053 53.3522 10.6464 55.472 12.262 54.6809C16.8302 50.2665 12.1346 48.9642 7.92721 48.7314C6.9333 48.6081 6.08781 48.963 5.33637 49.5492C-3.06805 48.9031 0.962591 60.1519 6.26679 61.6376C7.2033 62.0505 7.90582 61.2148 7.07994 60.4551C5.93728 59.2493 4.52181 56.8221 6.32941 55.5845Z",fill:"#2F2E41"}),Object(r.createElement)("path",{d:"M93.5621 50.4922H32.779C31.7793 50.4936 30.8209 50.894 30.1139 51.6056C29.407 52.3172 29.0092 53.2819 29.0078 54.2882V131.548C29.0092 132.555 29.407 133.52 30.1139 134.231C30.8209 134.943 31.7793 135.343 32.779 135.345H93.5621C94.5619 135.343 95.5202 134.943 96.2272 134.231C96.9341 133.52 97.3319 132.555 97.3333 131.548V54.2882C97.3319 53.2819 96.9341 52.3172 96.2272 51.6056C95.5202 50.894 94.5619 50.4936 93.5621 50.4922ZM96.8896 131.548C96.8886 132.436 96.5376 133.288 95.9138 133.916C95.29 134.544 94.4443 134.897 93.5621 134.898H32.779C31.8968 134.897 31.0511 134.544 30.4273 133.916C29.8035 133.288 29.4526 132.436 29.4515 131.548V54.2882C29.4526 53.4002 29.8035 52.5489 30.4273 51.921C31.0511 51.2931 31.8968 50.9399 32.779 50.9388H93.5621C94.4443 50.9399 95.29 51.2931 95.9138 51.921C96.5376 52.5489 96.8886 53.4002 96.8896 54.2882V131.548Z",fill:"#3F3D56"}),Object(r.createElement)("path",{d:"M15.6527 83.0043C13.6494 83.2327 12.7698 78.5402 12.0039 77.3445L15.9936 75.7852L17.2918 78.1751L28.168 78.4472C28.2326 78.3591 28.3032 78.2755 28.3793 78.1971C30.5362 75.9238 34.0488 79.33 31.9 81.5984C31.663 81.8519 31.3752 82.0521 31.0557 82.1855C30.7362 82.319 30.3923 82.3828 30.0465 82.3727C29.7007 82.3627 29.361 82.2789 29.0497 82.1271C28.7384 81.9753 28.4626 81.7588 28.2405 81.4918C27.9719 81.5111 15.9153 83.0114 15.6527 83.0043Z",fill:"#FFB8B8"}),Object(r.createElement)("path",{d:"M12.6985 80.7664C12.0314 81.4133 6.91061 72.3318 6.5365 72.0751C3.94893 68.0267 10.0405 64.0298 12.6727 68.0562L17.9961 76.2911C18.1402 76.5145 18.1904 76.7864 18.1356 77.047C18.0808 77.3076 17.9255 77.5357 17.7039 77.6812C17.4613 77.7956 12.8697 81.0123 12.6985 80.7664Z",className:"fill-theme-color"}),Object(r.createElement)("path",{d:"M81.583 103.974H44.7583C40.9966 103.955 40.9809 98.1877 44.7584 98.168H81.583C85.3421 98.1857 85.3624 103.954 81.583 103.974Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M81.583 114.692H44.7583C40.9966 114.674 40.9809 108.906 44.7584 108.887H81.583C85.3421 108.904 85.3624 114.673 81.583 114.692Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M81.583 125.411H44.7583C40.9966 125.393 40.9809 119.625 44.7584 119.605H81.583C85.3421 119.623 85.3624 125.391 81.583 125.411Z",fill:"#CCCCCC"}),Object(r.createElement)("path",{d:"M95.3371 57.6387C94.1963 57.6387 93.0812 57.2982 92.1327 56.6603C91.1842 56.0223 90.4449 55.1156 90.0084 54.0548C89.5718 52.9939 89.4576 51.8266 89.6802 50.7004C89.9027 49.5742 90.452 48.5397 91.2587 47.7278C92.0653 46.9159 93.093 46.3629 94.2118 46.1389C95.3307 45.9149 96.4904 46.0299 97.5443 46.4693C98.5982 46.9087 99.499 47.6528 100.133 48.6076C100.767 49.5623 101.105 50.6848 101.105 51.833C101.103 53.3723 100.495 54.8479 99.4136 55.9363C98.3323 57.0247 96.8662 57.637 95.3371 57.6387Z",className:"fill-theme-color"}),Object(r.createElement)("path",{d:"M97.999 51.6121H95.5588V48.821C95.5588 48.7617 95.5355 48.7049 95.4939 48.6631C95.4523 48.6212 95.3958 48.5977 95.337 48.5977C95.2782 48.5977 95.2217 48.6212 95.1801 48.6631C95.1385 48.7049 95.1152 48.7617 95.1152 48.821V51.6121H92.675C92.6161 51.6121 92.5597 51.6357 92.5181 51.6775C92.4765 51.7194 92.4531 51.7762 92.4531 51.8354C92.4531 51.8947 92.4765 51.9515 92.5181 51.9933C92.5597 52.0352 92.6161 52.0587 92.675 52.0587H95.1152V54.8499C95.1152 54.9091 95.1385 54.9659 95.1801 55.0078C95.2217 55.0497 95.2782 55.0732 95.337 55.0732C95.3958 55.0732 95.4523 55.0497 95.4939 55.0078C95.5355 54.9659 95.5588 54.9091 95.5588 54.8499V52.0587H97.999C98.0579 52.0587 98.1143 52.0352 98.1559 51.9933C98.1975 51.9515 98.2209 51.8947 98.2209 51.8354C98.2209 51.7762 98.1975 51.7194 98.1559 51.6775C98.1143 51.6357 98.0579 51.6121 97.999 51.6121Z",fill:"white"}),Object(r.createElement)("path",{d:"M80.9177 91.2002H45.424C44.4535 91.1991 43.5232 90.8105 42.837 90.1198C42.1508 89.4291 41.7648 88.4926 41.7637 87.5158V67.5086C41.7648 66.5318 42.1508 65.5953 42.837 64.9046C43.5232 64.2139 44.4535 63.8253 45.424 63.8242H80.9177C81.8882 63.8253 82.8185 64.2139 83.5047 64.9046C84.1909 65.5953 84.5769 66.5318 84.578 67.5086V87.5158C84.5769 88.4926 84.1909 89.4291 83.5047 90.1198C82.8185 90.8105 81.8882 91.1991 80.9177 91.2002Z",className:"fill-theme-color"})),Object(r.createElement)("defs",null,Object(r.createElement)("clipPath",{id:"clip0"},Object(r.createElement)("rect",{width:"199",height:"148",fill:"white",transform:"translate(0.5)"}))));o(569);var R=o(509),B=o(526);const z=["basics"],F=()=>Object(r.createElement)("div",{className:"woocommerce-admin__business-details__free-badge"},Object(s.__)("Free",'woocommerce')),L=({onChange:e,description:t,isChecked:o})=>Object(r.createElement)("div",{className:"woocommerce-admin__business-details__selective-extensions-bundle__extension"},Object(r.createElement)(_.CheckboxControl,{id:"woocommerce-business-extensions__checkbox",checked:o,onChange:e}),Object(r.createElement)("p",{className:"woocommerce-admin__business-details__selective-extensions-bundle__description",dangerouslySetInnerHTML:Object(R.a)(t),onClick:e=>{const t=e.target.closest("a");t&&e.currentTarget.contains(t)&&t.href.startsWith("https://woocommerce.com/products/")&&Object(d.recordEvent)("storeprofiler_store_business_features_link_click",{extension_name:t.href.split("https://woocommerce.com/products/")[1]})}}),Object(r.createElement)(F,null)),D={install_extensions:!0},V=({isInstallingActivating:e,onSubmit:t})=>{const[o,n]=Object(r.useState)(!1),[c,i]=Object(r.useState)(D),{countryCode:l,freeExtensions:u,isResolving:p,profileItems:b}=Object(a.useSelect)(e=>{const{getFreeExtensions:t,getProfileItems:o,hasFinishedResolution:r}=e(m.ONBOARDING_STORE_NAME),{getSettings:s}=e(m.SETTINGS_STORE_NAME),{general:n={}}=s("general");return{countryCode:Object(f.b)(n.woocommerce_default_country),freeExtensions:t(),isResolving:!r("getFreeExtensions"),profileItems:o()}}),O=Object(r.useMemo)(()=>{const{product_types:e}=b;return u.filter(t=>(window.wcAdminFeatures&&window.wcAdminFeatures.subscriptions&&"US"===l&&e.includes("subscriptions")&&(t.plugins=t.plugins.filter(e=>"woocommerce-payments"!==e.key||"woocommerce-payments"===e.key&&!e.is_activated)),z.includes(t.key)))},[u,b]);Object(r.useEffect)(()=>{if(!e){const e=O.reduce((e,t)=>({...e,...t.plugins.reduce((e,{key:t,selected:o})=>({...e,[t]:null==o||o}),{})}),D);i(e)}},[O]);const g=e=>t=>{const o={...c,[e]:t},r=1===Object.entries(o).filter(([,e])=>e).length&&o.install_extensions;i(r?{...o,install_extensions:!1}:{...c,[e]:t,install_extensions:!0})};return Object(r.createElement)("div",{className:"woocommerce-profile-wizard__business-details__free-features"},Object(r.createElement)(_.Card,null,Object(r.createElement)("div",{className:"woocommerce-profile-wizard__business-details__free-features__illustration"},Object(r.createElement)(A,null)),Object(r.createElement)("div",{className:"woocommerce-admin__business-details__selective-extensions-bundle"},Object(r.createElement)("div",{className:"woocommerce-admin__business-details__selective-extensions-bundle__extension"},Object(r.createElement)(_.CheckboxControl,{checked:c.install_extensions,onChange:e=>{i(Object(B.b)(c,e))}}),Object(r.createElement)("p",{className:"woocommerce-admin__business-details__selective-extensions-bundle__description"},Object(s.__)("Add recommended business features to my site",'woocommerce')),Object(r.createElement)(_.Button,{className:"woocommerce-admin__business-details__selective-extensions-bundle__expand",onClick:()=>{n(!o),o||Object(d.recordEvent)("storeprofiler_store_business_features_accordion_click")}},Object(r.createElement)(T.a,{icon:o?P.a:x.a}))),o&&O.map(({plugins:e,key:t})=>Object(r.createElement)("div",{key:t},p?Object(r.createElement)(_.Spinner,null):e.map(({description:e,key:t})=>Object(r.createElement)(L,{key:t,description:e,isChecked:c[t],onChange:g(t)}))))),Object(r.createElement)("div",{className:"woocommerce-profile-wizard__business-details__free-features__action"},Object(r.createElement)(_.Button,{onClick:()=>{t(c)},isBusy:e,disabled:e,isPrimary:!0},Object(s.__)("Continue",'woocommerce')))),((e,t)=>{const o=Object.keys(e).filter(t=>e[t]&&"install_extensions"!==t);if(0===o.length)return null;const n=o.reduce((e,t)=>{const o=m.pluginNames[t];return e.includes(o)?e:[...e,o]},[]).join(", ");if(t)return Object(r.createElement)("div",{className:"woocommerce-profile-wizard__footnote"},Object(r.createElement)(N.Text,{variant:"caption",as:"p",size:"12",lineHeight:"16px"},Object(s.sprintf)(Object(s._n)("Installing the following plugin: %s","Installing the following plugins: %s",o.length,'woocommerce'),n)));const c=o.includes("jetpack")||o.includes("woocommerce-shipping"),i=Object(s.__)("User accounts are required to use these features.",'woocommerce');return Object(r.createElement)("div",{className:"woocommerce-profile-wizard__footnote"},Object(r.createElement)(N.Text,{variant:"caption",as:"p",size:"12",lineHeight:"16px"},Object(s.sprintf)(Object(s._n)("The following plugin will be installed for free: %1$s. %2$s","The following plugins will be installed for free: %1$s. %2$s",o.length,'woocommerce'),n,i)),c&&Object(r.createElement)(N.Text,{variant:"caption",as:"p",size:"12",lineHeight:"16px"},M()({mixedString:Object(s.__)("By installing Jetpack and WooCommerce Shipping plugins for free you agree to our {{link}}Terms of Service{{/link}}.",'woocommerce'),components:{link:Object(r.createElement)(h.Link,{href:"https://wordpress.com/tos/",target:"_blank",type:"external"})}})))})(c,e))};o(570);class H extends r.Component{constructor(){super(),this.state={isPopoverVisible:!1,isValid:!1,currentTab:"business-details",savedValues:null},this.onContinue=this.onContinue.bind(this),this.validate=this.validate.bind(this)}async onContinue(e){const{createNotice:t,goToNextStep:o,installAndActivatePlugins:r}=this.props,n=(e=>Object.keys(e).filter(t=>e[t]&&"install_extensions"!==t).map(e=>e.split(":")[0]).filter((e,t,o)=>o.indexOf(e)===t))(e);Object(d.recordEvent)("storeprofiler_store_business_features_continue",{all_extensions_installed:Object.values(e).every(e=>e),install_woocommerce_services:e["woocommerce-services:shipping"]||e["woocommerce-services:tax"],install_jetpack:e.jetpack,install_wcpay:e["woocommerce-payments"]});const c=[this.persistProfileItems({business_extensions:n})];n.length&&c.push(r(n).then(e=>{Object(g.a)(e)}).catch(e=>{throw Object(g.a)(e),new Error})),Promise.all(c).then(()=>{o()}).catch(()=>{t("error",Object(s.__)("There was a problem updating your business details",'woocommerce'))})}async persistProfileItems(e={}){const{updateProfileItems:t,createNotice:o}=this.props,{other_platform:r,other_platform_name:n,product_count:c,revenue:i,selling_venues:a,setup_client:l}=this.state.savedValues,m={other_platform:r,other_platform_name:"other"===r?n:"",product_count:c,revenue:i,selling_venues:a,setup_client:l,...e};return t(Object.entries(m).reduce((e,[t,o])=>""!==o?{...e,[t]:o}:e,{})).catch(()=>{o("error",Object(s.__)("There was a problem updating your business details",'woocommerce'))})}validate(e){const t={};return e.product_count.length||(t.product_count=Object(s.__)("This field is required",'woocommerce')),e.selling_venues.length||(t.selling_venues=Object(s.__)("This field is required",'woocommerce')),!e.other_platform.length&&["other","brick-mortar-other"].includes(e.selling_venues)&&(t.other_platform=Object(s.__)("This field is required",'woocommerce')),!e.other_platform_name&&"other"===e.other_platform&&["other","brick-mortar-other"].includes(e.selling_venues)&&(t.other_platform_name=Object(s.__)("This field is required",'woocommerce')),!e.revenue.length&&["other","brick-mortar","brick-mortar-other","other-woocommerce"].includes(e.selling_venues)&&(t.revenue=Object(s.__)("This field is required",'woocommerce')),0===Object.keys(t).length&&this.setState({isValid:!0}),t}trackBusinessDetailsStep({other_platform:e,other_platform_name:t,product_count:o,selling_venues:r,revenue:s,setup_client:n}){const{getCurrencyConfig:c}=this.context;Object(d.recordEvent)("storeprofiler_store_business_details_continue_variant",{already_selling:r,currency:c().code,product_number:o,revenue:s,used_platform:e,used_platform_name:t,setup_client:n})}renderBusinessDetailsStep(){const{goToNextStep:e,isInstallingActivating:t,hasInstallActivateError:o}=this.props,{formatAmount:n,getCurrencyConfig:c}=this.context,i=(a=c(),[{key:"0",label:Object(s.__)("I don't have any products yet.",'woocommerce')},{key:"1-10",label:y(a,1,10)},{key:"11-100",label:y(a,11,100)},{key:"101-1000",label:y(a,101,1e3)},{key:"1000+",label:y(a,1e3)}]);var a;return Object(r.createElement)(h.Form,{initialValues:this.state.savedValues||this.props.initialValues,onSubmit:e=>{this.setState({savedValues:e,currentTab:"free-features"}),this.trackBusinessDetailsStep(e)},onChange:(e,t,o)=>{this.setState({savedValues:t,isValid:o})},validate:this.validate},({getInputProps:a,handleSubmit:l,values:m,isValidForm:d})=>Object(r.createElement)(r.Fragment,null,Object(r.createElement)("div",{className:"woocommerce-profile-wizard__step-header"},Object(r.createElement)(_.__experimentalText,{variant:"title.small",as:"h2",size:"20",lineHeight:"28px"},Object(s.__)("Tell us about your business",'woocommerce')),Object(r.createElement)(_.__experimentalText,{variant:"body",as:"p"},Object(s.__)("We'd love to know if you are just getting started or you already have a business in place.",'woocommerce'))),Object(r.createElement)(_.Card,null,Object(r.createElement)(_.CardBody,null,Object(r.createElement)(h.SelectControl,b()({excludeSelectedOptions:!1,label:Object(s.__)("How many products do you plan to display?",'woocommerce'),options:i,required:!0},a("product_count"))),Object(r.createElement)(h.SelectControl,b()({excludeSelectedOptions:!1,label:Object(s.__)("Currently selling elsewhere?",'woocommerce'),options:w,required:!0},a("selling_venues"))),["other","brick-mortar","brick-mortar-other","other-woocommerce"].includes(m.selling_venues)&&Object(r.createElement)(h.SelectControl,b()({excludeSelectedOptions:!1,label:Object(s.__)("What's your current annual revenue?",'woocommerce'),options:k(c(),this.props.settings.woocommerce_default_country,n),required:!0},a("revenue"))),["other","brick-mortar-other"].includes(m.selling_venues)&&Object(r.createElement)(r.Fragment,null,Object(r.createElement)("div",{className:"business-competitors"},Object(r.createElement)(h.SelectControl,b()({excludeSelectedOptions:!1,label:Object(s.__)("Which platform is the store using?",'woocommerce'),options:j,required:!0},a("other_platform"))),"other"===m.other_platform&&Object(r.createElement)(h.TextControl,b()({label:Object(s.__)("What is the platform name?",'woocommerce'),required:!0},a("other_platform_name")))))),Object(r.createElement)(_.CardFooter,{isBorderless:!0},Object(r.createElement)(_.FlexItem,null,Object(r.createElement)("div",{className:"woocommerce-profile-wizard__client"},Object(r.createElement)(_.CheckboxControl,b()({label:Object(s.__)("I'm setting up a store for a client",'woocommerce')},a("setup_client")))))),Object(r.createElement)(_.CardFooter,{justify:"center"},Object(r.createElement)(_.Button,{isPrimary:!0,onClick:async()=>{await l(),this.persistProfileItems()},disabled:!d,isBusy:t},o?Object(s.__)("Retry",'woocommerce'):Object(s.__)("Continue",'woocommerce')),o&&Object(r.createElement)(_.Button,{onClick:()=>{this.persistProfileItems(),e()}},Object(s.__)("Continue without installing",'woocommerce'))))))}renderFreeFeaturesStep(){const{isInstallingActivating:e,settings:t,profileItems:o}=this.props,n=t.woocommerce_default_country?t.woocommerce_default_country:null;return Object(r.createElement)(r.Fragment,null,Object(r.createElement)("div",{className:"woocommerce-profile-wizard__step-header"},Object(r.createElement)(_.__experimentalText,{variant:"title.small",as:"h2",size:"20",lineHeight:"28px"},Object(s.__)("Included business features",'woocommerce')),Object(r.createElement)(_.__experimentalText,{variant:"body",as:"p"},Object(s.__)("We recommend enhancing your store with these free extensions",'woocommerce')),Object(r.createElement)(_.__experimentalText,{variant:"body",as:"p"},Object(s.__)("No commitment required - you can remove them at any time.",'woocommerce'))),Object(r.createElement)(V,{isInstallingActivating:e,onSubmit:this.onContinue,country:n,industry:o.industry,productTypes:o.product_types}))}render(){const{initialValues:e}=this.props;return Object(r.createElement)(_.TabPanel,{activeClass:"is-active",initialTabName:"current-tab",onSelect:t=>{this.state.currentTab!==t&&this.setState({currentTab:t,savedValues:this.state.savedValues||e})},tabs:[{name:"business-details"===this.state.currentTab?"current-tab":"business-details",id:"business-details",title:Object(s.__)("Business details",'woocommerce')},{name:"free-features"===this.state.currentTab?"current-tab":"free-features",id:"free-features",title:Object(s.__)("Free features",'woocommerce'),className:this.state.isValid?"":"is-disabled"}]},e=>Object(r.createElement)(r.Fragment,null,this.getTab(e.id)))}getTab(e){return"business-details"===e?this.renderBusinessDetailsStep():this.renderFreeFeaturesStep()}}H.contextType=O.a;const U=Object(c.compose)(Object(a.withSelect)(e=>{const{getSettings:t,getSettingsError:o}=e(m.SETTINGS_STORE_NAME),{getProfileItems:r,getOnboardingError:s}=e(m.ONBOARDING_STORE_NAME),{getPluginsError:n,isPluginsRequesting:c}=e(m.PLUGINS_STORE_NAME),{general:i={}}=t("general");return{hasInstallActivateError:n("installPlugins")||n("activatePlugins"),isError:Boolean(s("updateProfileItems")),profileItems:r(),isSettingsError:Boolean(o("general")),settings:i,isInstallingActivating:c("installPlugins")||c("activatePlugins")||c("getJetpackConnectUrl")}}),Object(a.withDispatch)(e=>{const{updateProfileItems:t}=e(m.ONBOARDING_STORE_NAME),{installAndActivatePlugins:o}=e(m.PLUGINS_STORE_NAME),{createNotice:r}=e("core/notices");return{createNotice:r,installAndActivatePlugins:o,updateProfileItems:t}}))(H);o(571);const q=e=>{const{profileItems:t,isLoading:o}=Object(a.useSelect)(e=>({isLoading:!e(m.ONBOARDING_STORE_NAME).hasFinishedResolution("getProfileItems")||!e(m.SETTINGS_STORE_NAME).hasFinishedResolution("getSettings",["general"]),profileItems:e(m.ONBOARDING_STORE_NAME).getProfileItems()}));if(o)return Object(r.createElement)("div",{className:"woocommerce-admin__business-details__spinner"},Object(r.createElement)(h.Spinner,null));const s={other_platform:t.other_platform||"",other_platform_name:t.other_platform_name||"",product_count:t.product_count||"",selling_venues:t.selling_venues||"",revenue:t.revenue||"",setup_client:t.setup_client||!1};return Object(r.createElement)(U,b()({},e,{initialValues:s}))},G=Object(u.f)("onboarding",{});class Z extends r.Component{constructor(e){let t=Object(i.get)(e,"profileItems",{}).industry||[];const{locationSettings:o}=e;if("US"!==Object(f.c)(o.woocommerce_default_country)){const e="cbd-other-hemp-derived-products";t=t.filter(t=>e!==t&&e!==t.slug)}super(),this.state={error:null,selected:t,textInputListContent:{}},this.onContinue=this.onContinue.bind(this),this.onIndustryChange=this.onIndustryChange.bind(this),this.onDetailChange=this.onDetailChange.bind(this)}async onContinue(){if(await this.validateField(),this.state.error)return;const{createNotice:e,goToNextStep:t,isError:o,updateProfileItems:r}=this.props,n=this.state.selected.map(e=>e.slug),c=this.state.selected.map(e=>e.detail).filter(e=>e).join(",");Object(d.recordEvent)("storeprofiler_store_industry_continue",{store_industry:n,industries_with_detail:c}),await r({industry:this.state.selected}),o?e("error",Object(s.__)("There was a problem updating your industries",'woocommerce')):t()}async validateField(){const e=this.state.selected.length?null:Object(s.__)("Please select at least one industry",'woocommerce');this.setState({error:e})}onIndustryChange(e){this.setState(t=>{const o=t.selected,r=Object(i.find)(o,{slug:e});if(r){const o=t.textInputListContent;return o[e]=r.detail,{selected:Object(i.filter)(t.selected,t=>t.slug!==e)||[],textInputListContent:o}}return o.push({slug:e,detail:t.textInputListContent[e]}),{selected:o}},()=>this.validateField())}onDetailChange(e,t){this.setState(o=>{const r=o.selected,s=o.textInputListContent;return r[Object(i.findIndex)(r,{slug:t})].detail=e,s[t]=e,{selected:r,textInputListContent:s}})}renderIndustryLabel(e,t,o){const{textInputListContent:s}=this.state;return Object(r.createElement)(r.Fragment,null,t.label,t.use_description&&o&&Object(r.createElement)(h.TextControl,{key:"text-control-"+e,label:t.description_label,value:o.detail||s[e]||"",onChange:t=>this.onDetailChange(t,e),className:"woocommerce-profile-wizard__text"}))}render(){const{industries:e}=G,{error:t,selected:o}=this.state,{locationSettings:n,isProfileItemsRequesting:c}=this.props,a=Object(f.c)(n.woocommerce_default_country),l=Object.keys(e),m="US"===a?l:l.filter(e=>"cbd-other-hemp-derived-products"!==e);return Object(r.createElement)(r.Fragment,null,Object(r.createElement)("div",{className:"woocommerce-profile-wizard__step-header"},Object(r.createElement)(N.Text,{variant:"title.small",as:"h2",size:"20",lineHeight:"28px"},Object(s.__)("In which industry does the store operate?",'woocommerce')),Object(r.createElement)(N.Text,{variant:"body",as:"p"},Object(s.__)("Choose any that apply",'woocommerce'))),Object(r.createElement)(_.Card,null,Object(r.createElement)(_.CardBody,{size:null},Object(r.createElement)("div",{className:"woocommerce-profile-wizard__checkbox-group"},m.map(t=>{const s=Object(i.find)(o,{slug:t});return Object(r.createElement)(_.CheckboxControl,{key:"checkbox-control-"+t,label:this.renderIndustryLabel(t,e[t],s),onChange:()=>this.onIndustryChange(t),checked:s||!1,className:"woocommerce-profile-wizard__checkbox"})}),t&&Object(r.createElement)("span",{className:"woocommerce-profile-wizard__error"},t))),Object(r.createElement)(_.CardFooter,{isBorderless:!0,justify:"center"},Object(r.createElement)(_.Button,{isPrimary:!0,onClick:this.onContinue,isBusy:c,disabled:!o.length||c},Object(s.__)("Continue",'woocommerce')))))}}var W=Object(c.compose)(Object(a.withSelect)(e=>{const{getProfileItems:t,getOnboardingError:o,isOnboardingRequesting:r}=e(m.ONBOARDING_STORE_NAME),{getSettings:s}=e(m.SETTINGS_STORE_NAME),{general:n={}}=s("general");return{isError:Boolean(o("updateProfileItems")),profileItems:t(),locationSettings:n,isProfileItemsRequesting:r("updateProfileItems")}}),Object(a.withDispatch)(e=>{const{updateProfileItems:t}=e(m.ONBOARDING_STORE_NAME),{createNotice:o}=e("core/notices");return{createNotice:o,updateProfileItems:t}}))(Z),J=o(8),$=Object(r.createElement)(J.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(r.createElement)(J.Path,{d:"M12 3.2c-4.8 0-8.8 3.9-8.8 8.8 0 4.8 3.9 8.8 8.8 8.8 4.8 0 8.8-3.9 8.8-8.8 0-4.8-4-8.8-8.8-8.8zm0 16c-4 0-7.2-3.3-7.2-7.2C4.8 8 8 4.8 12 4.8s7.2 3.3 7.2 7.2c0 4-3.2 7.2-7.2 7.2zM11 17h2v-6h-2v6zm0-8h2V7h-2v2z"}));function Y({annualPrice:e,description:t,isMonthlyPricing:o,label:n,moreUrl:c,slug:i}){const[a,l]=Object(r.useState)("");if(!e)return n;const m=Object(s.__)("This product type requires a paid extension.\nWe'll add this to a cart so that\nyou can purchase and install it later.",'woocommerce');return Object(r.createElement)(r.Fragment,null,Object(r.createElement)("span",{className:"woocommerce-product-wizard__product-types-label"},n),Object(r.createElement)(_.Button,{isTertiary:!0,label:Object(s.__)("Learn more about recommended free business features",'woocommerce'),onClick:()=>{l(!0)}},Object(r.createElement)(T.a,{icon:$})),a&&Object(r.createElement)(_.Popover,{focusOnMount:"container",position:"top center",onClose:()=>l(!1)},M()({mixedString:t+(c?" {{moreLink/}}":""),components:{moreLink:c?Object(r.createElement)(h.Link,{href:c,target:"_blank",type:"external",onClick:()=>Object(d.recordEvent)("storeprofiler_store_product_type_learn_more",{product_type:i})},Object(s.__)("Learn more",'woocommerce')):""}})),Object(r.createElement)(_.Tooltip,{text:m,position:"bottom center"},Object(r.createElement)(h.Pill,null,Object(r.createElement)("span",{className:"screen-reader-text"},m),o?Object(s.sprintf)(Object(s.__)("$%f per month",'woocommerce'),(e/12).toFixed(2)):Object(s.sprintf)(Object(s.__)("$%f per year",'woocommerce'),e))))}o(572);class Q extends r.Component{constructor(){super(),this.state={error:null,isMonthlyPricing:!0,selected:[],isWCPayInstalled:null},this.onContinue=this.onContinue.bind(this),this.onChange=this.onChange.bind(this)}componentDidMount(){const{installedPlugins:e,invalidateResolution:t}=this.props,{isWCPayInstalled:o}=this.state;t("getProductTypes",[]),null===o&&e&&this.setState({isWCPayInstalled:e.includes("woocommerce-payments")})}componentDidUpdate(e){const{profileItems:t,productTypes:o}=this.props;if(e.productTypes!==o){const e=Object.keys(o).filter(e=>!!o[e].default);this.setState({selected:t.product_types||e})}}validateField(){const e=this.state.selected.length?null:Object(s.__)("Please select at least one product type",'woocommerce');return this.setState({error:e}),!e}onContinue(){const{selected:e}=this.state,{installedPlugins:t=[]}=this.props;if(!this.validateField())return;const{countryCode:o,createNotice:r,goToNextStep:n,installAndActivatePlugins:c,updateProfileItems:i}=this.props;Object(d.recordEvent)("storeprofiler_store_product_type_continue",{product_type:e});const a=[i({product_types:e})];window.wcAdminFeatures&&window.wcAdminFeatures.subscriptions&&"US"===o&&!t.includes("woocommerce-payments")&&e.includes("subscriptions")&&a.push(c(["woocommerce-payments"]).then(e=>{Object(g.a)(e)}).catch(e=>{throw Object(g.a)(e),new Error})),Promise.all(a).then(()=>n()).catch(()=>r("error",Object(s.__)("There was a problem updating your product types",'woocommerce')))}onChange(e){this.setState(t=>{if(Object(i.includes)(t.selected,e))return{selected:Object(i.filter)(t.selected,t=>t!==e)||[]};const o=t.selected;return o.push(e),{selected:o}},()=>this.validateField())}render(){const{productTypes:e=[]}=this.props,{error:t,isMonthlyPricing:o,isWCPayInstalled:n,selected:c}=this.state,{countryCode:i,isInstallingActivating:a,isProductTypesRequesting:l,isProfileItemsRequesting:m}=this.props;return l?Object(r.createElement)("div",{className:"woocommerce-profile-wizard__product-types__spinner"},Object(r.createElement)(_.Spinner,null)):Object(r.createElement)("div",{className:"woocommerce-profile-wizard__product-types"},Object(r.createElement)("div",{className:"woocommerce-profile-wizard__step-header"},Object(r.createElement)(N.Text,{variant:"title.small",as:"h2",size:"20",lineHeight:"28px"},Object(s.__)("What type of products will be listed?",'woocommerce')),Object(r.createElement)(N.Text,{variant:"body",as:"p"},Object(s.__)("Choose any that apply",'woocommerce'))),Object(r.createElement)(_.Card,null,Object(r.createElement)(_.CardBody,{size:null},Object.keys(e).map(t=>Object(r.createElement)(_.CheckboxControl,{key:t,label:Object(r.createElement)(Y,{description:e[t].description,label:e[t].label,annualPrice:e[t].yearly_price,isMonthlyPricing:o,moreUrl:e[t].more_url,slug:t}),onChange:()=>this.onChange(t),checked:c.includes(t),className:"woocommerce-profile-wizard__checkbox"})),t&&Object(r.createElement)("span",{className:"woocommerce-profile-wizard__error"},t)),Object(r.createElement)(_.CardFooter,{isBorderless:!0,justify:"center"},Object(r.createElement)(_.Button,{isPrimary:!0,onClick:this.onContinue,isBusy:m||a,disabled:!c.length||m||a},Object(s.__)("Continue",'woocommerce')))),Object(r.createElement)("div",{className:"woocommerce-profile-wizard__card-help-footnote"},Object(r.createElement)("div",{className:"woocommerce-profile-wizard__product-types-pricing-toggle woocommerce-profile-wizard__checkbox"},Object(r.createElement)("label",{htmlFor:"woocommerce-product-types__pricing-toggle"},Object(r.createElement)(N.Text,{variant:"body",as:"p"},Object(s.__)("Display monthly prices",'woocommerce')),Object(r.createElement)(_.FormToggle,{id:"woocommerce-product-types__pricing-toggle",checked:o,onChange:()=>this.setState({isMonthlyPricing:!o})}))),Object(r.createElement)(N.Text,{variant:"caption",size:"12",lineHeight:"16px"},Object(s.__)("Billing is annual. All purchases are covered by our 30 day money back guarantee and include access to support and updates. Extensions will be added to a cart for you to purchase later.",'woocommerce')),window.wcAdminFeatures&&window.wcAdminFeatures.subscriptions&&"US"===i&&!n&&c.includes("subscriptions")&&Object(r.createElement)(N.Text,{variant:"body",size:"12",lineHeight:"16px",as:"p"},Object(s.__)("The following extensions will be added to your site for free: WooCommerce Payments. An account is required to use this feature.",'woocommerce'))))}}var K=Object(c.compose)(Object(a.withSelect)(e=>{const{getProfileItems:t,getProductTypes:o,getOnboardingError:r,hasFinishedResolution:s,isOnboardingRequesting:n}=e(m.ONBOARDING_STORE_NAME),{getSettings:c}=e(m.SETTINGS_STORE_NAME),{getInstalledPlugins:i,isPluginsRequesting:a}=e(m.PLUGINS_STORE_NAME),{general:l={}}=c("general");return{isError:Boolean(r("updateProfileItems")),profileItems:t(),isProfileItemsRequesting:n("updateProfileItems"),installedPlugins:i(),isInstallingActivating:a("installPlugins")||a("activatePlugins"),countryCode:Object(f.b)(l.woocommerce_default_country),productTypes:o(),isProductTypesRequesting:!s("getProductTypes")}}),Object(a.withDispatch)(e=>{const{updateProfileItems:t}=e(m.ONBOARDING_STORE_NAME),{createNotice:o}=e("core/notices"),{installAndActivatePlugins:r}=e(m.PLUGINS_STORE_NAME),{invalidateResolution:s}=e(m.ONBOARDING_STORE_NAME);return{createNotice:o,installAndActivatePlugins:r,invalidateResolution:s,updateProfileItems:t}}))(Q);class X extends r.Component{renderStepper(){const{currentStep:e,steps:t}=this.props,o=Object(i.filter)(t,e=>!!e.label),s=o.findIndex(t=>t.key===e);return o.map((e,t)=>{const r=o[t-1];return tObject(l.updateQueryString)({step:e})),e}),Object(r.createElement)(h.Stepper,{steps:o,currentStep:e})}render(){const e=this.props.steps.find(e=>e.key===this.props.currentStep);return e&&e.label?Object(r.createElement)("div",{className:"woocommerce-profile-wizard__header"},this.renderStepper()):null}}var ee=o(527),te=o(514);o(573);const oe=_.FlexItem||(({children:e,align:t})=>{const o={display:"flex","justify-content":t?"center":"flex-start"};return Object(r.createElement)("div",{style:o},e)}),re=()=>Object(r.createElement)("div",{className:"woocommerce-admin__store-details__spinner"},Object(r.createElement)(_.Spinner,null));class se extends r.Component{constructor(e){super(e),this.state={showUsageModal:!1,skipping:!1,isStoreDetailsPopoverVisible:!1,isSkipSetupPopoverVisible:!1},this.onContinue=this.onContinue.bind(this),this.onSubmit=this.onSubmit.bind(this)}deriveCurrencySettings(e){if(!e)return null;const t=this.context,o=Object(f.b)(e),{currencySymbols:r={},localeInfo:s={}}=Object(u.f)("onboarding",{});return t.getDataForCountry(o,s,r)}onSubmit(){this.setState({showUsageModal:!0,skipping:!1})}async onContinue(e){const{createNotice:t,goToNextStep:o,updateProfileItems:r,updateAndPersistSettingsForGroup:n,profileItems:c,settings:i,errorsRef:a}=this.props,l=this.deriveCurrencySettings(e.countryState);this.context.setCurrency(l),Object(d.recordEvent)("storeprofiler_store_details_continue",{store_country:Object(f.b)(e.countryState),derived_currency:l.currency_code,email_signup:e.isAgreeMarketing}),await n("general",{general:{...i,woocommerce_store_address:e.addressLine1,woocommerce_store_address_2:e.addressLine2,woocommerce_default_country:e.countryState,woocommerce_store_city:e.city,woocommerce_store_postcode:e.postCode,woocommerce_currency:l.code,woocommerce_currency_pos:l.symbolPosition,woocommerce_price_thousand_sep:l.thousandSeparator,woocommerce_price_decimal_sep:l.decimalSeparator,woocommerce_price_num_decimals:l.precision}});const m={is_agree_marketing:e.isAgreeMarketing,store_email:e.storeEmail};if("US"!==Object(f.c)(e.countryState)&&c.industry&&c.industry.length){const e="cbd-other-hemp-derived-products",t=c.industry.filter(t=>e!==t&&e!==t.slug);m.industry=t}let u=[];try{await r(m)}catch(e){var p;null!=e&&null!==(p=e.data)&&void 0!==p&&p.params&&(u=Object.values(e.data.params))}Boolean(a.current.settings)||u.length?(t("error",Object(s.__)("There was a problem saving your store details",'woocommerce')),u.forEach(e=>t("error",e))):o()}validateStoreDetails(e){const t=Object(ee.b)(e);return!e.isAgreeMarketing||e.storeEmail&&e.storeEmail.trim().length||(t.storeEmail=Object(s.__)("Please add an email address",'woocommerce')),e.storeEmail&&e.storeEmail.trim().length&&-1===e.storeEmail.indexOf("@")&&(t.storeEmail=Object(s.__)("Invalid email address",'woocommerce')),t}render(){const{showUsageModal:e,skipping:t,isStoreDetailsPopoverVisible:o,isSkipSetupPopoverVisible:n}=this.state,{skipProfiler:c,isLoading:i,isBusy:a,initialValues:l}=this.props,m=Object(s.__)("Manual setup is only recommended for\n experienced WooCommerce users or developers.",'woocommerce'),d=Object(s.__)("Your store address will help us configure currency\n options and shipping rules automatically.\n This information will not be publicly visible and can\n easily be changed later.",'woocommerce');return i?Object(r.createElement)("div",{className:"woocommerce-profile-wizard__store-details"},Object(r.createElement)(re,null)):Object(r.createElement)("div",{className:"woocommerce-profile-wizard__store-details"},Object(r.createElement)("div",{className:"woocommerce-profile-wizard__step-header"},Object(r.createElement)(N.Text,{variant:"title.small",as:"h2",size:"20",lineHeight:"28px"},Object(s.__)("Welcome to WooCommerce",'woocommerce')),Object(r.createElement)(N.Text,{variant:"body",as:"p"},Object(s.__)("Tell us about your store and we'll get you set up in no time",'woocommerce'),Object(r.createElement)(_.Button,{isTertiary:!0,label:Object(s.__)("Learn more about store details",'woocommerce'),onClick:()=>this.setState({isStoreDetailsPopoverVisible:!0})},Object(r.createElement)(T.a,{icon:$}))),o&&Object(r.createElement)(_.Popover,{focusOnMount:"container",position:"top center",onClose:()=>this.setState({isStoreDetailsPopoverVisible:!1})},d)),Object(r.createElement)(h.Form,{initialValues:l,onSubmit:this.onSubmit,validate:this.validateStoreDetails},({getInputProps:o,handleSubmit:n,values:i,isValidForm:l,setValue:m})=>Object(r.createElement)(_.Card,null,e&&Object(r.createElement)(te.a,{onContinue:()=>{t?c():this.onContinue(i)},onClose:()=>this.setState({showUsageModal:!1,skipping:!1})}),Object(r.createElement)(_.CardBody,null,Object(r.createElement)(ee.a,{getInputProps:o,setValue:m}),Object(r.createElement)(h.TextControl,b()({label:Object(s.__)("Email address",'woocommerce'),required:!0,autoComplete:"email"},o("storeEmail")))),Object(r.createElement)(_.CardFooter,null,Object(r.createElement)(oe,null,Object(r.createElement)("div",{className:"woocommerce-profile-wizard__newsletter-signup"},Object(r.createElement)(_.CheckboxControl,b()({label:Object(r.createElement)(r.Fragment,null,Object(s.__)("Get tips, product updates and inspiration straight to your mailbox.",'woocommerce')," ",Object(r.createElement)("span",{className:"woocommerce-profile-wizard__powered-by-mailchimp"},Object(s.__)("Powered by Mailchimp",'woocommerce')))},o("isAgreeMarketing")))))),Object(r.createElement)(_.CardFooter,{justify:"center"},Object(r.createElement)(_.Button,{isPrimary:!0,onClick:n,isBusy:a,disabled:!l||a},Object(s.__)("Continue",'woocommerce'))))),Object(r.createElement)("div",{className:"woocommerce-profile-wizard__footer"},Object(r.createElement)(_.Button,{isLink:!0,className:"woocommerce-profile-wizard__footer-link",onClick:()=>(this.setState({showUsageModal:!0,skipping:!0}),!1)},Object(s.__)("Skip setup store details",'woocommerce')),Object(r.createElement)(_.Button,{isTertiary:!0,label:m,onClick:()=>this.setState({isSkipSetupPopoverVisible:!0})},Object(r.createElement)(T.a,{icon:$})),n&&Object(r.createElement)(_.Popover,{focusOnMount:"container",position:"top center",onClose:()=>this.setState({isSkipSetupPopoverVisible:!1})},m)))}}se.contextType=O.a;var ne=Object(c.compose)(Object(a.withSelect)(e=>{const{getSettings:t,getSettingsError:o,isUpdateSettingsRequesting:s}=e(m.SETTINGS_STORE_NAME),{getProfileItems:n,isOnboardingRequesting:c,getEmailPrefill:i,hasFinishedResolution:a}=e(m.ONBOARDING_STORE_NAME),{isResolving:l}=e(m.OPTIONS_STORE_NAME),d=n(),u=i(),{general:p={}}=t("general"),b=c("updateProfileItems")||s("general")||l("getOption",["woocommerce_allow_tracking"]),h=!a("getProfileItems")||!a("getEmailPrefill"),_=Object(r.useRef)({settings:null});_.current={settings:o("general")};const O=p.woocommerce_store_address&&p.woocommerce_default_country||"";return{initialValues:{addressLine1:p.woocommerce_store_address||"",addressLine2:p.woocommerce_store_address_2||"",city:p.woocommerce_store_city||"",countryState:O,postCode:p.woocommerce_store_postcode||"",isAgreeMarketing:"boolean"!=typeof d.is_agree_marketing||d.is_agree_marketing,storeEmail:"string"==typeof d.store_email?d.store_email:u},isLoading:h,profileItems:d,isBusy:b,settings:p,errorsRef:_}}),Object(a.withDispatch)(e=>{const{createNotice:t}=e("core/notices"),{updateProfileItems:o}=e(m.ONBOARDING_STORE_NAME),{updateAndPersistSettingsForGroup:r}=e(m.SETTINGS_STORE_NAME);return{createNotice:t,updateProfileItems:o,updateAndPersistSettingsForGroup:r}}))(se),ce=o(17),ie=o.n(ce),ae=o(28),le=o(574),me=o.n(le),de=(o(575),o(6)),ue=o.n(de),pe=o(576),be=o.n(pe),he=o(1),_e=o.n(he),Oe=o(476);const ge=({children:e,className:t})=>Object(Oe.isWpVersion)("5.8",">=")?Object(r.createElement)("div",{className:t},e):Object(r.createElement)(_.DropZoneProvider,null,Object(r.createElement)("div",{className:t},e));class je extends r.Component{constructor(){super(),this.state={isUploading:!1},this.handleFilesUpload=this.handleFilesUpload.bind(this),this.handleFilesDrop=this.handleFilesDrop.bind(this)}handleFilesDrop(e){const t=e[0];this.uploadTheme(t)}handleFilesUpload(e){const t=e.target.files[0];this.uploadTheme(t)}uploadTheme(e){const{createNotice:t,onUploadComplete:o}=this.props;this.setState({isUploading:!0});const r=new window.FormData;return r.append("pluginzip",e),ie()({path:"/wc-admin/themes",method:"POST",body:r}).then(e=>{o(e),this.setState({isUploading:!1}),t(e.status,e.message)}).catch(e=>{this.setState({isUploading:!1}),e&&e.message&&t("error",e.message)})}render(){const{className:e}=this.props,{isUploading:t}=this.state,o=ue()("woocommerce-theme-uploader",e,{"is-uploading":t});return Object(r.createElement)(_.Card,{className:o},Object(r.createElement)(ge,{className:"woocommerce-theme-uploader__dropzone-wrapper"},t?Object(r.createElement)(r.Fragment,null,Object(r.createElement)(h.Spinner,null),Object(r.createElement)(h.H,{className:"woocommerce-theme-uploader__title"},Object(s.__)("Uploading theme",'woocommerce')),Object(r.createElement)("p",null,Object(s.__)("Your theme is being uploaded",'woocommerce'))):Object(r.createElement)(r.Fragment,null,Object(r.createElement)(_.FormFileUpload,{accept:".zip",onChange:this.handleFilesUpload},Object(r.createElement)(be.a,null),Object(r.createElement)(h.H,{className:"woocommerce-theme-uploader__title"},Object(s.__)("Upload a theme",'woocommerce')),Object(r.createElement)("p",null,Object(s.__)("Drop a theme zip file here to upload",'woocommerce'))),Object(r.createElement)(_.DropZone,{label:Object(s.__)("Drop your theme zip file here",'woocommerce'),onFilesDrop:this.handleFilesDrop}))))}}je.propTypes={className:_e.a.string,onUploadComplete:_e.a.func},je.defaultProps={onUploadComplete:i.noop};var we=Object(c.compose)(Object(a.withDispatch)(e=>{const{createNotice:t}=e("core/notices");return{createNotice:t}}))(je),fe=o(497),Ce=o(577),Ee=o.n(Ce),ye=o(578),ve=o.n(ye),Se=o(579),ke=o.n(Se);const Ne=[{key:"mobile",icon:Ee.a},{key:"tablet",icon:ve.a},{key:"desktop",icon:ke.a}];class Te extends r.Component{constructor(){super(...arguments),this.state={device:"desktop"},this.handleDeviceClick=this.handleDeviceClick.bind(this)}handleDeviceClick(e){const{theme:t}=this.props;Object(d.recordEvent)("storeprofiler_store_theme_demo_device",{device:e,theme:t.slug}),this.setState({device:e})}render(){const{isBusy:e,onChoose:t,onClose:o,theme:n}=this.props,{demo_url:c,slug:i,title:a}=n,{device:l}=this.state;return Object(r.createElement)("div",{className:"woocommerce-theme-preview"},Object(r.createElement)("div",{className:"woocommerce-theme-preview__toolbar"},Object(r.createElement)(_.Button,{className:"woocommerce-theme-preview__close",onClick:o},Object(r.createElement)(T.a,{icon:fe.a})),Object(r.createElement)("div",{className:"woocommerce-theme-preview__theme-name"},M()({mixedString:Object(s.sprintf)(Object(s.__)("{{strong}}%s{{/strong}} developed by WooCommerce",'woocommerce'),a),components:{strong:Object(r.createElement)("strong",null)}})),Object(r.createElement)("div",{className:"woocommerce-theme-preview__devices"},Ne.map(e=>{const t=e.icon;return Object(r.createElement)(_.Button,{key:e.key,className:ue()("woocommerce-theme-preview__device",{"is-selected":e.key===l}),onClick:()=>this.handleDeviceClick(e.key)},Object(r.createElement)(t,null))})),Object(r.createElement)(_.Button,{isPrimary:!0,onClick:()=>t(i,"preview"),isBusy:e},Object(s.__)("Choose",'woocommerce'))),Object(r.createElement)(h.WebPreview,{src:c,title:a,className:"is-"+l}))}}var Pe=Te;class xe extends r.Component{constructor(){super(...arguments),this.state={activeTab:"all",chosen:null,demo:null,uploadedThemes:[]},this.handleUploadComplete=this.handleUploadComplete.bind(this),this.onChoose=this.onChoose.bind(this),this.onClosePreview=this.onClosePreview.bind(this),this.onSelectTab=this.onSelectTab.bind(this),this.openDemo=this.openDemo.bind(this),this.skipStep=this.skipStep.bind(this)}componentDidUpdate(e){const{isError:t,isUpdatingProfileItems:o,createNotice:r}=this.props,{chosen:n}=this.state,c=!o&&e.isUpdatingProfileItems&&!t&&n,i=!o&&e.isRequesting&&t;c&&(this.setState({chosen:null}),this.props.goToNextStep()),i&&(this.setState({chosen:null}),r("error",Object(s.__)("There was a problem selecting your store theme",'woocommerce')))}onChoose(e,t=""){const{updateProfileItems:o}=this.props,{is_installed:r,price:s,slug:n}=e,{activeTheme:c=""}=Object(u.f)("onboarding",{});this.setState({chosen:n}),Object(d.recordEvent)("storeprofiler_store_theme_choose",{theme:n,location:t}),n!==c&&Object(f.d)(s)<=0?r?this.activateTheme(n):this.installTheme(n):o({theme:n})}installTheme(e){const{createNotice:t}=this.props;ie()({path:"/wc-admin/onboarding/themes/install?theme="+e,method:"POST"}).then(o=>{t("success",Object(s.sprintf)(Object(s.__)("%s was installed on your site",'woocommerce'),o.name)),this.activateTheme(e)}).catch(e=>{this.setState({chosen:null}),t("error",e.message)})}activateTheme(e){const{createNotice:t,updateProfileItems:o}=this.props;ie()({path:"/wc-admin/onboarding/themes/activate?theme="+e,method:"POST"}).then(r=>{t("success",Object(s.sprintf)(Object(s.__)("%s was activated on your site",'woocommerce'),r.name)),Object(u.g)("onboarding",{...Object(u.f)("onboarding",{}),activeTheme:r.slug}),o({theme:e})}).catch(e=>{this.setState({chosen:null}),t("error",e.message)})}onClosePreview(){const{demo:e}=this.state;Object(d.recordEvent)("storeprofiler_store_theme_demo_close",{theme:e.slug}),document.body.classList.remove("woocommerce-theme-preview-active"),this.setState({demo:null})}openDemo(e){Object(d.recordEvent)("storeprofiler_store_theme_live_demo",{theme:e.slug}),document.body.classList.add("woocommerce-theme-preview-active"),this.setState({demo:e})}skipStep(){const{activeTheme:e=""}=Object(u.f)("onboarding",{});Object(d.recordEvent)("storeprofiler_store_theme_skip_step",{activeTheme:e}),this.props.goToNextStep()}renderTheme(e){const{demo_url:t,has_woocommerce_support:o,image:n,slug:c,title:i}=e,{chosen:a}=this.state,{activeTheme:l=""}=Object(u.f)("onboarding",{});return Object(r.createElement)(_.Card,{className:"woocommerce-profile-wizard__theme",key:c},Object(r.createElement)(_.CardBody,{size:null},n&&Object(r.createElement)("div",{className:"woocommerce-profile-wizard__theme-image",style:{backgroundImage:`url(${n})`},role:"img","aria-label":i})),Object(r.createElement)(_.CardBody,{className:"woocommerce-profile-wizard__theme-details"},Object(r.createElement)(h.H,{className:"woocommerce-profile-wizard__theme-name"},i,!o&&Object(r.createElement)(_.Tooltip,{text:Object(s.__)("This theme does not support WooCommerce.",'woocommerce')},Object(r.createElement)("span",null,Object(r.createElement)(me.a,{role:"img","aria-hidden":"true",focusable:"false"})))),Object(r.createElement)("p",{className:"woocommerce-profile-wizard__theme-status"},this.getThemeStatus(e))),Object(r.createElement)(_.CardFooter,null,c===l?Object(r.createElement)(_.Button,{isPrimary:!0,onClick:()=>this.onChoose(e,"card"),isBusy:a===c,disabled:a===c},Object(s.__)("Continue with my active theme",'woocommerce')):Object(r.createElement)(_.Button,{isSecondary:!0,onClick:()=>this.onChoose(e,"card"),isBusy:a===c,disabled:a===c},Object(s.__)("Choose",'woocommerce')),t&&Object(r.createElement)(_.Button,{isTertiary:!0,onClick:()=>this.openDemo(e)},Object(s.__)("Live demo",'woocommerce'))))}getThemeStatus(e){const{is_installed:t,price:o,slug:r}=e,{activeTheme:n=""}=Object(u.f)("onboarding",{});return n===r?Object(s.__)("Currently active theme",'woocommerce'):t?Object(s.__)("Installed",'woocommerce'):Object(f.d)(o)<=0?Object(s.__)("Free",'woocommerce'):Object(s.sprintf)(Object(s.__)("%s per year",'woocommerce'),Object(ae.decodeEntities)(o))}doesActiveThemeSupportWooCommerce(){const{activeTheme:e=""}=Object(u.f)("onboarding",{}),t=this.getThemes().find(t=>t.slug===e);return t&&t.has_woocommerce_support}onSelectTab(e){Object(d.recordEvent)("storeprofiler_store_theme_navigate",{navigation:e}),this.setState({activeTab:e})}getPriceValue(e){return Number(Object(ae.decodeEntities)(e).replace(/[^0-9.-]+/g,""))}getThemes(e="all"){const{uploadedThemes:t}=this.state,{activeTheme:o="",themes:r=[]}=Object(u.f)("onboarding",{}),s=[...r.filter(e=>e&&(e.has_woocommerce_support||e.slug===o)),...t];switch(e){case"paid":return s.filter(e=>Object(f.d)(e.price)>0);case"free":return s.filter(e=>Object(f.d)(e.price)<=0);case"all":default:return s}}handleUploadComplete(e){"success"===e.status&&e.theme_data&&(this.setState({uploadedThemes:[...this.state.uploadedThemes,e.theme_data]}),Object(d.recordEvent)("storeprofiler_store_theme_upload",{theme:e.theme_data.slug}))}render(){const{activeTab:e,chosen:t,demo:o}=this.state,n=this.getThemes(e),c=this.doesActiveThemeSupportWooCommerce();return Object(r.createElement)(r.Fragment,null,Object(r.createElement)("div",{className:"woocommerce-profile-wizard__step-header"},Object(r.createElement)(N.Text,{variant:"title.small",as:"h2",size:"20",lineHeight:"28px"},Object(s.__)("Choose a theme",'woocommerce')),Object(r.createElement)(N.Text,{variant:"body",as:"p"},Object(s.__)("Choose how your store appears to customers. And don't worry, you can always switch themes and edit them later.",'woocommerce'))),Object(r.createElement)(_.TabPanel,{className:"woocommerce-profile-wizard__themes-tab-panel",activeClass:"is-active",onSelect:this.onSelectTab,tabs:[{name:"all",title:Object(s.__)("All themes",'woocommerce')},{name:"paid",title:Object(s.__)("Paid themes",'woocommerce')},{name:"free",title:Object(s.__)("Free themes",'woocommerce')}]},()=>Object(r.createElement)("div",{className:"woocommerce-profile-wizard__themes"},n&&n.map(e=>this.renderTheme(e)),Object(r.createElement)(we,{onUploadComplete:this.handleUploadComplete}))),o&&Object(r.createElement)(Pe,{theme:o,onChoose:()=>this.onChoose(o,"card"),onClose:this.onClosePreview,isBusy:t===o.slug}),c&&Object(r.createElement)("p",{className:"woocommerce-profile-wizard__themes-skip-this-step"},Object(r.createElement)(_.Button,{isLink:!0,className:"woocommerce-profile-wizard__skip",onClick:()=>this.skipStep()},Object(s.__)("Skip this step",'woocommerce'))))}}var Ie=Object(c.compose)(Object(a.withSelect)(e=>{const{getProfileItems:t,getOnboardingError:o,isOnboardingRequesting:r}=e(m.ONBOARDING_STORE_NAME);return{isError:Boolean(o("updateProfileItems")),isUpdatingProfileItems:r("updateProfileItems"),profileItems:t()}}),Object(a.withDispatch)(e=>{const{updateProfileItems:t}=e(m.ONBOARDING_STORE_NAME),{createNotice:o}=e("core/notices");return{createNotice:o,updateProfileItems:t}}))(xe);o(580);class Me extends r.Component{constructor(e){super(e),this.cachedActivePlugins=e.activePlugins,this.goToNextStep=this.goToNextStep.bind(this)}componentDidUpdate(e){const{step:t}=e.query,{step:o}=this.props.query,{isError:r,isGetProfileItemsRequesting:n,createNotice:c}=this.props;!n&&e.isRequesting&&r&&c("error",Object(s.__)("There was a problem finishing the setup wizard",'woocommerce')),t!==o&&(window.document.documentElement.scrollTop=0,Object(d.recordEvent)("storeprofiler_step_view",{step:this.getCurrentStep().key}))}componentDidMount(){document.body.classList.remove("woocommerce-admin-is-loading"),document.body.classList.add("woocommerce-onboarding"),document.body.classList.add("woocommerce-profile-wizard__body"),document.body.classList.add("woocommerce-admin-full-screen"),document.body.classList.add("is-wp-toolbar-disabled"),Object(d.recordEvent)("storeprofiler_step_view",{step:this.getCurrentStep().key})}componentWillUnmount(){document.body.classList.remove("woocommerce-onboarding"),document.body.classList.remove("woocommerce-profile-wizard__body"),document.body.classList.remove("woocommerce-admin-full-screen"),document.body.classList.remove("is-wp-toolbar-disabled")}getSteps(){const{profileItems:e}=this.props,t=[];return t.push({key:"store-details",container:ne,label:Object(s.__)("Store Details",'woocommerce'),isComplete:e.hasOwnProperty("setup_client")&&null!==e.setup_client}),t.push({key:"industry",container:W,label:Object(s.__)("Industry",'woocommerce'),isComplete:e.hasOwnProperty("industry")&&null!==e.industry}),t.push({key:"product-types",container:K,label:Object(s.__)("Product Types",'woocommerce'),isComplete:e.hasOwnProperty("product_types")&&null!==e.product_types}),t.push({key:"business-features",container:q,label:Object(s.__)("Business Details",'woocommerce'),isComplete:e.hasOwnProperty("product_count")&&null!==e.product_count}),t.push({key:"theme",container:Ie,label:Object(s.__)("Theme",'woocommerce'),isComplete:e.hasOwnProperty("theme")&&null!==e.theme}),Object(n.applyFilters)("woocommerce_admin_profile_wizard_steps",t)}getCurrentStep(){const{step:e}=this.props.query,t=this.getSteps().find(t=>t.key===e);return t||this.getSteps()[0]}async goToNextStep(){const{activePlugins:e,dismissedTasks:t,updateOptions:o}=this.props,r=this.getCurrentStep(),s=this.getSteps().findIndex(e=>e.key===r.key);Object(d.recordEvent)("storeprofiler_step_complete",{step:r.key}),t.length&&o({woocommerce_task_list_dismissed_tasks:[]}),this.cachedActivePlugins=e;const n=this.getSteps()[s+1];if(void 0!==n)return Object(l.updateQueryString)({step:n.key});this.completeProfiler()}completeProfiler(){const{activePlugins:e,isJetpackConnected:t,notes:o,updateNote:r,updateProfileItems:s,connectToJetpack:n}=this.props;Object(d.recordEvent)("storeprofiler_complete");const c=e.includes("jetpack")&&!t,i=o.find(e=>"wc-admin-onboarding-profiler-reminder"===e.name);i&&r(i.id,{status:"actioned"}),s({completed:!0}).then(()=>{const e=new URL(Object(l.getNewPath)({},"/",{}),window.location.href).href;c?(document.body.classList.add("woocommerce-admin-is-loading"),n(()=>e)):window.location.href=e})}skipProfiler(){const{createNotice:e,updateProfileItems:t}=this.props;t({skipped:!0}).then(()=>{Object(d.recordEvent)("storeprofiler_store_details_skip"),Object(l.getHistory)().push(Object(l.getNewPath)({},"/",{}))}).catch(()=>{e("error",Object(s.__)("There was a problem skipping the setup wizard",'woocommerce'))})}render(){const{query:e}=this.props,t=this.getCurrentStep(),o=t.key,s=Object(r.createElement)(t.container,{query:e,step:t,goToNextStep:this.goToNextStep,skipProfiler:()=>{this.skipProfiler()}}),n=this.getSteps().map(e=>Object(i.pick)(e,["key","label","isComplete"])),c="woocommerce-profile-wizard__container "+o;return Object(r.createElement)(r.Fragment,null,Object(r.createElement)(X,{currentStep:o,steps:n}),Object(r.createElement)("div",{className:c},s))}}t.default=Object(c.compose)(Object(a.withSelect)(e=>{const{getNotes:t}=e(m.NOTES_STORE_NAME),{getOption:o}=e(m.OPTIONS_STORE_NAME),{getProfileItems:r,getOnboardingError:s}=e(m.ONBOARDING_STORE_NAME),{getActivePlugins:n,getPluginsError:c,isJetpackConnected:i}=e(m.PLUGINS_STORE_NAME),a=r(),l=t({page:1,per_page:m.QUERY_DEFAULTS.pageSize,type:"update",status:"unactioned"}),d=n();return{dismissedTasks:o("woocommerce_task_list_dismissed_tasks")||[],getPluginsError:c,isError:Boolean(s("updateProfileItems")),isJetpackConnected:i(),notes:l,profileItems:a,activePlugins:d}}),Object(a.withDispatch)(e=>{const{connectToJetpackWithFailureRedirect:t,createErrorNotice:o}=e(m.PLUGINS_STORE_NAME),{updateNote:r}=e(m.NOTES_STORE_NAME),{updateOptions:s}=e(m.OPTIONS_STORE_NAME),{updateProfileItems:n}=e(m.ONBOARDING_STORE_NAME),{createNotice:c}=e("core/notices");return{connectToJetpack:e=>{t(e,o,u.e)},createNotice:c,updateNote:r,updateOptions:s,updateProfileItems:n}}),Object(u.f)("plugins")?Object(m.withPluginsHydration)({...Object(u.f)("plugins"),jetpackStatus:Object(u.f)("dataEndpoints",{}).jetpackStatus}):i.identity)(Me)}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/shipping-recommendations.js b/packages/woocommerce-admin/dist/chunks/shipping-recommendations.js new file mode 100644 index 0000000..9f3f6e9 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/shipping-recommendations.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[48],{507:function(e,t,c){"use strict";c.d(t,"a",(function(){return o}));var i=c(7);function o(e){const{createNotice:t}=Object(i.dispatch)("core/notices");e.error_data&&e.errors&&Object.keys(e.errors).length?Object.keys(e.errors).forEach(c=>{t("error",e.errors[c].join(" "))}):e.message&&t(e.code?"error":"success",e.message)}},537:function(e,t,c){"use strict";var i=Object.assign||function(e){for(var t,c=1;cnull)})=>{const{updateOptions:c}=Object(n.useDispatch)(j.OPTIONS_STORE_NAME),s=Object(i.useContext)(N),M=()=>{t(),c({[s]:"yes"})};return Object(i.createElement)(r.CardHeader,null,Object(i.createElement)("div",{className:"woocommerce-dismissable-list__header"},e),Object(i.createElement)("div",null,Object(i.createElement)(a.EllipsisMenu,{label:Object(o.__)("Task List Options",'woocommerce'),renderContent:()=>Object(i.createElement)("div",{className:"woocommerce-dismissable-list__controls"},Object(i.createElement)(r.Button,{onClick:M},Object(o.__)("Hide this",'woocommerce')))})))},g=({children:e,className:t,dismissOptionName:c})=>Object(n.useSelect)(e=>{const{getOption:t,hasFinishedResolution:i}=e(j.OPTIONS_STORE_NAME),o=i("getOption",[c]),n="yes"===t(c);return o&&!n})?Object(i.createElement)(r.Card,{size:"medium",className:l()("woocommerce-dismissable-list",t)},Object(i.createElement)(N.Provider,{value:c},e)):null;var S=c(13),y=(c(588),c(589)),E=c.n(y);var T=({onSetupClick:e,pluginsBeingSetup:t})=>{Object(S.f)("wcAdminAssetUrl","");const{createSuccessNotice:c}=Object(n.useDispatch)("core/notices"),s=Object(n.useSelect)(e=>e(j.PLUGINS_STORE_NAME).isJetpackConnected());return Object(i.createElement)("div",{className:"woocommerce-list__item-inner woocommerce-services-item"},Object(i.createElement)("div",{className:"woocommerce-list__item-before"},Object(i.createElement)("img",{className:"woocommerce-services-item__logo",src:E.a,alt:""})),Object(i.createElement)("div",{className:"woocommerce-list__item-text"},Object(i.createElement)("span",{className:"woocommerce-list__item-title"},Object(o.__)("Woocommerce Shipping",'woocommerce'),Object(i.createElement)(a.Pill,null,Object(o.__)("Recommended",'woocommerce'))),Object(i.createElement)("span",{className:"woocommerce-list__item-content"},Object(o.__)("Print USPS and DHL Express labels straight from your WooCommerce dashboard and save on shipping.",'woocommerce'),Object(i.createElement)("br",null),Object(i.createElement)(r.ExternalLink,{href:"https://woocommerce.com/woocommerce-shipping/"},Object(o.__)("Learn more",'woocommerce')))),Object(i.createElement)("div",{className:"woocommerce-list__item-after"},Object(i.createElement)(r.Button,{isSecondary:!0,onClick:()=>{e(["woocommerce-services"]).then(()=>{const e=[];s||e.push({url:Object(S.e)("plugins.php"),label:Object(o.__)("Finish the setup by connecting your store to Jetpack.",'woocommerce')}),c(Object(o.__)("🎉 WooCommerce Shipping is installed!",'woocommerce'),{actions:e})})},isBusy:t.includes("woocommerce-services"),disabled:t.length>0},Object(o.__)("Get started",'woocommerce'))))};c(590);const d=({children:e})=>Object(i.createElement)(g,{className:"woocommerce-recommended-shipping-extensions",dismissOptionName:"woocommerce_settings_shipping_recommendations_hidden"},Object(i.createElement)(O,null,Object(i.createElement)(s.Text,{variant:"title.small",as:"p",size:"20",lineHeight:"28px"},Object(o.__)("Recommended shipping solutions",'woocommerce')),Object(i.createElement)(s.Text,{className:"woocommerce-recommended-shipping__header-heading",variant:"caption",as:"p",size:"12",lineHeight:"16px"},Object(o.__)('We recommend adding one of the following shipping extensions to your store. The extension will be installed and activated for you when you click "Get started".','woocommerce'))),Object(i.createElement)("ul",{className:"woocommerce-list"},i.Children.map(e,e=>Object(i.createElement)("li",{className:"woocommerce-list__item"},e))),Object(i.createElement)(r.CardFooter,null,Object(i.createElement)(r.Button,{className:"woocommerce-recommended-shipping-extensions__more_options_cta",href:"https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/?utm_source=shipping_recommendations",target:"_blank",isTertiary:!0},Object(o.__)("See more options",'woocommerce'),Object(i.createElement)(r.VisuallyHidden,null,Object(o.__)("(opens in a new tab)",'woocommerce')),Object(i.createElement)(u.a,{size:18}))));t.default=()=>{const[e,t]=(()=>{const[e,t]=Object(i.useState)([]),{installAndActivatePlugins:c}=Object(n.useDispatch)(j.PLUGINS_STORE_NAME);return[e,i=>e.length>0?Promise.resolve():(t(i),c(i).then(()=>{t([])}).catch(e=>(Object(m.a)(e),t([]),Promise.reject())))]})();return Object(n.useSelect)(e=>e(j.PLUGINS_STORE_NAME).getActivePlugins()).includes("woocommerce-services")?null:Object(i.createElement)(d,null,Object(i.createElement)(T,{pluginsBeingSetup:e,onSetupClick:t}))}}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/store-alerts.js b/packages/woocommerce-admin/dist/chunks/store-alerts.js new file mode 100644 index 0000000..bfcf7c6 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/store-alerts.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[49],{473:function(e,t,r){"use strict";var a=r(0),n=r(8),o=Object(a.createElement)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(a.createElement)(n.Path,{d:"M10.6 6L9.4 7l4.6 5-4.6 5 1.2 1 5.4-6z"}));t.a=o},509:function(e,t,r){"use strict";var a=r(53);const n=["a","b","em","i","strong","p","br"],o=["target","href","rel","name","download"];t.a=e=>({__html:Object(a.sanitize)(e,{ALLOWED_TAGS:n,ALLOWED_ATTR:o})})},544:function(e,t,r){},618:function(e,t,r){"use strict";r.r(t),r.d(t,"StoreAlerts",(function(){return S}));var a=r(0),n=r(2),o=r(3),s=r(6),l=r.n(s),c=r(18),i=r.n(c),m=r(14),d=r(7),u=r(9),p=r.n(u),b=r(116),h=r(291),_=r(473),O=r(13),j=r(11),g=r(16),E=r(20),w=r(509),A=r(1),N=r.n(A);class v extends a.Component{render(){const{hasMultipleAlerts:e}=this.props;return Object(a.createElement)(o.Card,{className:"woocommerce-store-alerts is-loading","aria-hidden":!0,size:null},Object(a.createElement)(o.CardHeader,{isBorderless:!0},Object(a.createElement)("span",{className:"is-placeholder"}),e&&Object(a.createElement)("span",{className:"is-placeholder"})),Object(a.createElement)(o.CardBody,null,Object(a.createElement)("div",{className:"woocommerce-store-alerts__message"},Object(a.createElement)("span",{className:"is-placeholder"}),Object(a.createElement)("span",{className:"is-placeholder"}))),Object(a.createElement)(o.CardFooter,{isBorderless:!0},Object(a.createElement)("span",{className:"is-placeholder"})))}}var x=v;v.propTypes={hasMultipleAlerts:N.a.bool},v.defaultProps={hasMultipleAlerts:!1};r(544);class S extends a.Component{constructor(e){super(e),this.state={currentIndex:0},this.previousAlert=this.previousAlert.bind(this),this.nextAlert=this.nextAlert.bind(this)}previousAlert(e){e.stopPropagation();const{currentIndex:t}=this.state;t>0&&this.setState({currentIndex:t-1})}nextAlert(e){e.stopPropagation();const t=this.getAlerts(),{currentIndex:r}=this.state;rObject(a.createElement)(o.Button,{key:r.name,isPrimary:r.primary,isSecondary:!r.primary,href:r.url||void 0,onClick:()=>t(e.id,r.id)},r.label)),l=[{value:p()().add(4,"hours").unix().toString(),label:Object(n.__)("Later Today",'woocommerce')},{value:p()().add(1,"day").hour(9).minute(0).second(0).millisecond(0).unix().toString(),label:Object(n.__)("Tomorrow",'woocommerce')},{value:p()().add(1,"week").hour(9).minute(0).second(0).millisecond(0).unix().toString(),label:Object(n.__)("Next Week",'woocommerce')},{value:p()().add(1,"month").hour(9).minute(0).second(0).millisecond(0).unix().toString(),label:Object(n.__)("Next Month",'woocommerce')}],c=e.is_snoozable&&Object(a.createElement)(o.SelectControl,{className:"woocommerce-store-alerts__snooze",options:[{label:Object(n.__)("Remind Me Later",'woocommerce'),value:"0"},...l],onChange:t=>{if("0"===t)return;const a=l.find(e=>e.value===t);(t=>{r(e.id,{status:"snoozed",date_reminder:t.value});const a={alert_name:e.name,alert_title:e.title,snooze_duration:t.value,snooze_label:t.label};Object(g.recordEvent)("store_alert_snooze",a)})({value:t,label:a&&a.label})}});if(s||c)return Object(a.createElement)("div",{className:"woocommerce-store-alerts__actions"},s,c)}getAlerts(){return(this.props.alerts||[]).filter(e=>"unactioned"===e.status)}render(){const e=this.getAlerts(),t=Object(O.f)("alertCount",0,e=>parseInt(e,10));if(t>0&&this.props.isLoading)return Object(a.createElement)(x,{hasMultipleAlerts:t>1});if(0===e.length)return null;const{currentIndex:r}=this.state,s=e.length,c=e[r],m=c.type,d=l()("woocommerce-store-alerts",{"is-alert-error":"error"===m,"is-alert-update":"update"===m});return Object(a.createElement)(o.Card,{className:d,size:null},Object(a.createElement)(o.CardHeader,{isBorderless:!0},Object(a.createElement)(E.Text,{variant:"title.medium",as:"h2",size:"24",lineHeight:"32px"},c.title),s>1&&Object(a.createElement)("div",{className:"woocommerce-store-alerts__pagination"},Object(a.createElement)(o.Button,{onClick:this.previousAlert,disabled:0===r,label:Object(n.__)("Previous Alert",'woocommerce')},Object(a.createElement)(b.a,{icon:h.a,className:"arrow-left-icon"})),Object(a.createElement)("span",{className:"woocommerce-store-alerts__pagination-label",role:"status","aria-live":"polite"},i()({mixedString:Object(n.__)("{{current /}} of {{total /}}",'woocommerce'),components:{current:Object(a.createElement)(a.Fragment,null,r+1),total:Object(a.createElement)(a.Fragment,null,s)}})),Object(a.createElement)(o.Button,{onClick:this.nextAlert,disabled:s-1===r,label:Object(n.__)("Next Alert",'woocommerce')},Object(a.createElement)(b.a,{icon:_.a,className:"arrow-right-icon"})))),Object(a.createElement)(o.CardBody,null,Object(a.createElement)("div",{className:"woocommerce-store-alerts__message",dangerouslySetInnerHTML:Object(w.a)(c.content)})),Object(a.createElement)(o.CardFooter,{isBorderless:!0},this.renderActions(c)))}}const f={page:1,per_page:j.QUERY_DEFAULTS.pageSize,type:"error,update",status:"unactioned"};t.default=Object(m.compose)(Object(d.withSelect)(e=>{const{getNotes:t,isResolving:r}=e(j.NOTES_STORE_NAME);return{alerts:t(f),isLoading:r("getNotes",[f])}}),Object(d.withDispatch)(e=>{const{triggerNoteAction:t,updateNote:r}=e(j.NOTES_STORE_NAME);return{triggerNoteAction:t,updateNote:r}}))(S)}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/store-performance.js b/packages/woocommerce-admin/dist/chunks/store-performance.js new file mode 100644 index 0000000..7a80fa8 --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/store-performance.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[50],{525:function(e,t,r){"use strict";r.d(t,"b",(function(){return d})),r.d(t,"a",(function(){return u}));var a=r(9),n=r.n(a),o=r(4),c=r(19),s=r(11),i=r(12),l=r(120),m=r(13);const d=({indicator:e,primaryData:t,secondaryData:r,currency:a,formatAmount:n,persistedQuery:c})=>{const s=Object(o.find)(t.data,t=>t.stat===e.stat),d=Object(o.find)(r.data,t=>t.stat===e.stat);if(!s||!d)return{};const u=s._links&&s._links.report[0]&&s._links.report[0].href||"",p=function(e,t,r){return e?"/jetpack"===e?Object(m.e)("admin.php?page=jetpack#/dashboard"):Object(i.getNewPath)(t,e,{chart:r.chart}):""}(u,c,s),b="/jetpack"===u?"wp-admin":"wc-admin",f="currency"===s.format,y=Object(l.calculateDelta)(s.value,d.value);return{primaryValue:f?n(s.value):Object(l.formatValue)(a,s.format,s.value),secondaryValue:f?n(d.value):Object(l.formatValue)(a,d.format,d.value),delta:y,reportUrl:p,reportUrlType:b}},u=(e,t,r,a)=>{const{getReportItems:o,getReportItemsError:i,isResolving:l}=e(s.REPORTS_STORE_NAME),{woocommerce_default_date_range:m}=e(s.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings"),d=Object(c.getCurrentDates)(r,m),u=d.primary.before,p=d.secondary.before,b=t.map(e=>e.stat).join(","),f=Object(s.getFilterQuery)({filters:a,query:r}),y={...f,after:Object(c.appendTimestamp)(d.primary.after,"start"),before:Object(c.appendTimestamp)(u,u.isSame(n()(),"day")?"now":"end"),stats:b},g={...f,after:Object(c.appendTimestamp)(d.secondary.after,"start"),before:Object(c.appendTimestamp)(p,p.isSame(n()(),"day")?"now":"end"),stats:b};return{primaryData:o("performance-indicators",y),primaryError:i("performance-indicators",y)||null,primaryRequesting:l("getReportItems",["performance-indicators",y]),secondaryData:o("performance-indicators",g),secondaryError:i("performance-indicators",g)||null,secondaryRequesting:l("getReportItems",["performance-indicators",g]),defaultDateRange:m}}},603:function(e,t,r){},610:function(e,t,r){"use strict";r.r(t);var a=r(0),n=r(2),o=r(14),c=r(12),s=r(13),i=r(7),l=r(11),m=r(21),d=r(19),u=r(16),p=(r(603),r(501)),b=r(525);const{performanceIndicators:f}=Object(s.f)("dataEndpoints",{performanceIndicators:[]});class y extends a.Component{renderMenu(){const{hiddenBlocks:e,isFirst:t,isLast:r,onMove:o,onRemove:c,onTitleBlur:s,onTitleChange:i,onToggleHiddenBlock:l,titleInput:d,controls:p}=this.props;return Object(a.createElement)(m.EllipsisMenu,{label:Object(n.__)("Choose which analytics to display and the section name",'woocommerce'),renderContent:({onToggle:b})=>Object(a.createElement)(a.Fragment,null,Object(a.createElement)(m.MenuTitle,null,Object(n.__)("Display stats:",'woocommerce')),f.map((t,r)=>{const n=!e.includes(t.stat);return Object(a.createElement)(m.MenuItem,{checked:n,isCheckbox:!0,isClickable:!0,key:r,onInvoke:()=>{l(t.stat)(),Object(u.recordEvent)("dash_indicators_toggle",{status:n?"off":"on",key:t.stat})}},t.label)}),Object(a.createElement)(p,{onToggle:b,onMove:o,onRemove:c,isFirst:t,isLast:r,onTitleBlur:s,onTitleChange:i,titleInput:d}))})}renderList(){const{query:e,primaryRequesting:t,secondaryRequesting:r,primaryError:o,secondaryError:s,primaryData:i,secondaryData:l,userIndicators:p,defaultDateRange:f}=this.props;if(t||r)return Object(a.createElement)(m.SummaryListPlaceholder,{numberOfItems:p.length});if(o||s)return null;const y=Object(c.getPersistedQuery)(e),{compare:g}=Object(d.getDateParamsFromQuery)(e,f),O="previous_period"===g?Object(n.__)("Previous period:",'woocommerce'):Object(n.__)("Previous year:",'woocommerce'),{formatAmount:j,getCurrencyConfig:_}=this.context,h=_();return Object(a.createElement)(m.SummaryList,null,()=>p.map((e,t)=>{const{primaryValue:r,secondaryValue:n,delta:o,reportUrl:c,reportUrlType:s}=Object(b.b)({indicator:e,primaryData:i,secondaryData:l,currency:h,formatAmount:j,persistedQuery:y});return Object(a.createElement)(m.SummaryNumber,{key:t,href:c,hrefType:s,label:e.label,value:r,prevLabel:O,prevValue:n,delta:o,onLinkClickCallback:()=>{Object(u.recordEvent)("dash_indicators_click",{key:e.stat})}})}))}render(){const{userIndicators:e,title:t}=this.props;return Object(a.createElement)(a.Fragment,null,Object(a.createElement)(m.SectionHeader,{title:t||Object(n.__)("Store Performance",'woocommerce'),menu:this.renderMenu()}),e.length>0&&Object(a.createElement)("div",{className:"woocommerce-dashboard__store-performance"},this.renderList()))}}y.contextType=p.a,t.default=Object(o.compose)(Object(i.withSelect)((e,t)=>{const{hiddenBlocks:r,query:a,filters:n}=t,o=f.filter(e=>!r.includes(e.stat)),{woocommerce_default_date_range:c}=e(l.SETTINGS_STORE_NAME).getSetting("wc_admin","wcAdminSettings"),s={hiddenBlocks:r,userIndicators:o,indicators:f,defaultDateRange:c};if(0===o.length)return s;const i=Object(b.a)(e,o,a,n);return{...s,...i}}))(y)}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/task-list.js b/packages/woocommerce-admin/dist/chunks/task-list.js new file mode 100644 index 0000000..86819ec --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/task-list.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[51],{162:function(e,t,o){"use strict";o.d(t,"a",(function(){return s}));var c=o(15),n=o(13);const s=(e,t={})=>{const{pathname:o,search:s}=window.location,a=Object(n.f)("connectNonce","");return t={"wccom-site":Object(n.f)("siteUrl"),"wccom-back":o+s,"wccom-woo-version":Object(n.f)("wcVersion"),"wccom-connect-nonce":a,...t},Object(c.addQueryArgs)(e,t)}},473:function(e,t,o){"use strict";var c=o(0),n=o(8),s=Object(c.createElement)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(n.Path,{d:"M10.6 6L9.4 7l4.6 5-4.6 5 1.2 1 5.4-6z"}));t.a=s},475:function(e,t,o){"use strict";var c=o(0),n=o(8),s=Object(c.createElement)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(n.Path,{d:"M18.3 5.6L9.9 16.9l-4.6-3.4-.9 1.2 5.8 4.3 9.3-12.6z"}));t.a=s},526:function(e,t,o){"use strict";o.d(t,"a",(function(){return c})),o.d(t,"b",(function(){return n}));const c=(e,t,o="undefined")=>e&&Array.isArray(e)&&e.length?t?e.reduce((e,c)=>(c[t]||(c[t]=o),(e[c[t]]=e[c[t]]||[]).push(c),e),{}):e:{},n=(e,t)=>Object.entries(e).reduce((e,[o])=>({...e,[o]:t}),{})},527:function(e,t,o){"use strict";o.d(t,"b",(function(){return p})),o.d(t,"a",(function(){return u}));var c=o(35),n=o.n(c),s=o(0),a=o(2),i=o(28),r=o(4),l=o(13),m=o(21);const{countries:d}=Object(l.f)("dataEndpoints",{countries:{}});function p(e){const t={};return e.addressLine1.trim().length||(t.addressLine1=Object(a.__)("Please add an address",'woocommerce')),e.countryState.trim().length||(t.countryState=Object(a.__)("Please select a country / region",'woocommerce')),e.city.trim().length||(t.city=Object(a.__)("Please add a city",'woocommerce')),e.postCode.trim().length||(t.postCode=Object(a.__)("Please add a post code",'woocommerce')),t}function u(e){const{getInputProps:t,setValue:o}=e,c=Object(s.useMemo)(()=>d.reduce((e,t)=>{if(!t.states.length)return e.push({key:t.code,label:Object(i.decodeEntities)(t.name)}),e;const o=t.states.map(e=>({key:t.code+":"+e.code,label:Object(i.decodeEntities)(t.name)+" — "+Object(i.decodeEntities)(e.name)}));return e.push(...o),e},[]),[]),l=function(e,t,o){const[c,n]=Object(s.useState)(""),[a,i]=Object(s.useState)(""),l=Object(s.useRef)();return Object(s.useEffect)(()=>{const o=e.find(e=>e.key===t),s=o?o.label.split(/\u2013|\u2014|\-/):[],r=(s[0]||"").trim(),m=(s[1]||"").trim();l.current||r===c&&m===a||(n(r),i(m)),l.current=!1},[t]),Object(s.useEffect)(()=>{c||a||!t||(l.current=!0,o("countryState",""));let n=[];const s=new RegExp(Object(r.escapeRegExp)(c),"i"),i=new RegExp(Object(r.escapeRegExp)(a.replace(/\s/g,""))+"$","i");if((a.length||c.length)&&(n=e.filter(e=>(c.length?s:i).test(e.label))),c.length&&a.length){const e=a.length<3;n=n.filter(t=>i.test((e?t.key:t.label).replace("-","").replace(/\s/g,"")));const t=c.length<3;if(n.length>1){let e=[];e=n.filter(e=>s.test(t?e.key:e.label)),e.length>0&&(n=e)}if(n.length>1){let t=[];t=n.filter(t=>i.test((e?t.key:t.label).replace("-","").replace(/\s/g,""))),1===t.length&&(n=t)}}1===n.length&&t!==n[0].key&&(l.current=!0,o("countryState",n[0].key))},[c,a,e,o]),Object(s.createElement)(s.Fragment,null,Object(s.createElement)("input",{onChange:e=>n(e.target.value),value:c,name:"country",type:"text",className:"woocommerce-select-control__autofill-input",tabIndex:"-1",autoComplete:"country"}),Object(s.createElement)("input",{onChange:e=>i(e.target.value),value:a,name:"state",type:"text",className:"woocommerce-select-control__autofill-input",tabIndex:"-1",autoComplete:"address-level1"}))}(c,t("countryState").value,o);return Object(s.createElement)("div",{className:"woocommerce-store-address-fields"},Object(s.createElement)(m.TextControl,n()({label:Object(a.__)("Address line 1",'woocommerce'),required:!0,autoComplete:"address-line1"},t("addressLine1"))),Object(s.createElement)(m.TextControl,n()({label:Object(a.__)("Address line 2 (optional)",'woocommerce'),required:!0,autoComplete:"address-line2"},t("addressLine2"))),Object(s.createElement)(m.SelectControl,n()({label:Object(a.__)("Country / Region",'woocommerce'),required:!0,autoComplete:"new-password",options:c,excludeSelectedOptions:!1,showAllOnFocus:!0,isSearchable:!0},t("countryState"),{controlClassName:t("countryState").className}),l),Object(s.createElement)(m.TextControl,n()({label:Object(a.__)("City",'woocommerce'),required:!0},t("city"),{autoComplete:"address-level2"})),Object(s.createElement)(m.TextControl,n()({label:Object(a.__)("Post code",'woocommerce'),required:!0,autoComplete:"postal-code"},t("postCode"))))}},538:function(e,t,o){},593:function(e,t,o){},594:function(e,t,o){},595:function(e,t,o){},596:function(e,t,o){},597:function(e,t,o){},598:function(e,t,o){},611:function(e,t,o){"use strict";o.r(t);var c=o(0),n=o(2),s=o(3),a=o(475),i=o(7),r=o(11),l=o(122),m=o(16),d=o(21),p=(o(541),o(14)),u=o(4),_=o(28),b=o(13),g=o(60),h=o(509),O=o(162);class j extends c.Component{constructor(e){super(e),this.state={purchaseNowButtonBusy:!1,purchaseLaterButtonBusy:!1}}onClickPurchaseNow(){const{productIds:e,onClickPurchaseNow:t}=this.props;if(this.setState({purchaseNowButtonBusy:!0}),!e.length)return;Object(m.recordEvent)("tasklist_modal_proceed_checkout",{product_ids:e,purchase_install:!0});const o=Object(O.a)("https://woocommerce.com/cart?utm_medium=product",{"wccom-replace-with":e.join(",")});t?t(o):window.location=o}onClickPurchaseLater(){const{productIds:e}=this.props;Object(m.recordEvent)("tasklist_modal_proceed_checkout",{product_ids:e,purchase_install:!1}),this.setState({purchaseLaterButtonBusy:!0}),this.props.onClickPurchaseLater()}onClose(){const{onClose:e,productIds:t}=this.props;Object(m.recordEvent)("tasklist_modal_proceed_checkout",{product_ids:t,purchase_install:!1}),e()}renderProducts(){const{productIds:e,productTypes:t}=this.props,{themes:o=[]}=Object(b.f)("onboarding",{}),s=[];return e.forEach(e=>{const a=Object(u.find)(t,t=>t.product===e);a&&s.push({title:a.label,content:a.description});const i=Object(u.find)(o,t=>t.id===e);i&&s.push({title:Object(n.sprintf)(Object(n.__)("%s — %s per year",'woocommerce'),i.title,Object(_.decodeEntities)(i.price)),content:Object(c.createElement)("span",{dangerouslySetInnerHTML:Object(h.a)(i.excerpt)})})}),Object(c.createElement)(d.List,{items:s})}render(){const{purchaseNowButtonBusy:e,purchaseLaterButtonBusy:t}=this.state;return Object(c.createElement)(s.Modal,{title:Object(n.__)("Would you like to add the following paid features to your store now?",'woocommerce'),onRequestClose:()=>this.onClose(),className:"woocommerce-cart-modal"},this.renderProducts(),Object(c.createElement)("p",{className:"woocommerce-cart-modal__help-text"},Object(n.__)("You won't have access to this functionality until the extensions have been purchased and installed.",'woocommerce')),Object(c.createElement)("div",{className:"woocommerce-cart-modal__actions"},Object(c.createElement)(s.Button,{isLink:!0,isBusy:t,onClick:()=>this.onClickPurchaseLater()},Object(n.__)("I'll do it later",'woocommerce')),Object(c.createElement)(s.Button,{isPrimary:!0,isBusy:e,onClick:()=>this.onClickPurchaseNow()},Object(n.__)("Buy now",'woocommerce'))))}}var w=Object(p.compose)(Object(i.withSelect)(e=>{const{getInstalledPlugins:t}=e(r.PLUGINS_STORE_NAME),{getProductTypes:o,getProfileItems:c}=e(r.ONBOARDING_STORE_NAME),n=c(),s=t(),a=o();return{profileItems:n,productIds:Object(g.e)(a,n,!1,s),productTypes:a}}))(j),y=o(30),k=o(12),E=o(17),S=o.n(E);class v extends c.Component{constructor(e){super(e);const{hasHomepage:t,hasProducts:o}=e.tasksStatus;this.stepVisibility={homepage:!t,import:!o},this.state={isDirty:!1,isPending:!1,logo:null,stepIndex:0,isUpdatingLogo:!1,isUpdatingNotice:!1,storeNoticeText:e.demoStoreNotice||""},this.completeStep=this.completeStep.bind(this),this.createHomepage=this.createHomepage.bind(this),this.importProducts=this.importProducts.bind(this),this.updateLogo=this.updateLogo.bind(this),this.updateNotice=this.updateNotice.bind(this)}componentDidMount(){const{themeMods:e}=this.props.tasksStatus;e&&e.custom_logo&&this.setState({logo:{id:e.custom_logo}})}componentDidUpdate(e){const{isPending:t,logo:o}=this.state,{demoStoreNotice:c}=this.props;!o||o.url||t||(this.setState({isPending:!0}),wp.media.attachment(o.id).fetch().then(()=>{const e=wp.media.attachment(o.id).get("url");this.setState({isPending:!1,logo:{id:o.id,url:e}})})),c&&e.demoStoreNotice!==c&&this.setState({storeNoticeText:c})}completeStep(){const{stepIndex:e}=this.state;this.getSteps()[e+1]?this.setState({stepIndex:e+1}):Object(k.getHistory)().push(Object(k.getNewPath)({},"/",{}))}importProducts(){const{clearTaskStatusCache:e,createNotice:t}=this.props;this.setState({isPending:!0}),Object(m.recordEvent)("tasklist_appearance_import_demo",{}),S()({path:r.WC_ADMIN_NAMESPACE+"/onboarding/tasks/import_sample_products",method:"POST"}).then(o=>{o.failed&&o.failed.length?t("error",Object(n.__)("There was an error importing some of the sample products",'woocommerce')):(t("success",Object(n.__)("All sample products have been imported",'woocommerce')),e()),this.setState({isPending:!1}),this.completeStep()}).catch(e=>{t("error",e.message),this.setState({isPending:!1})})}createHomepage(){const{clearTaskStatusCache:e,createNotice:t}=this.props;this.setState({isPending:!0}),Object(m.recordEvent)("tasklist_appearance_create_homepage",{create_homepage:!0}),S()({path:"/wc-admin/onboarding/tasks/create_homepage",method:"POST"}).then(o=>{e(),t(o.status,o.message,{actions:o.edit_post_link?[{label:Object(n.__)("Customize",'woocommerce'),onClick:()=>{Object(m.queueRecordEvent)("tasklist_appearance_customize_homepage",{}),window.location=o.edit_post_link+"&wc_onboarding_active_task=homepage"}}]:null}),this.setState({isPending:!1}),this.completeStep()}).catch(e=>{t("error",e.message),this.setState({isPending:!1})})}async updateLogo(){const{clearTaskStatusCache:e,createNotice:t,stylesheet:o,themeMods:c,updateOptions:s}=this.props,{logo:a}=this.state,i={...c,custom_logo:a?a.id:null};Object(m.recordEvent)("tasklist_appearance_upload_logo"),this.setState({isUpdatingLogo:!0});const r=await s({["theme_mods_"+o]:i});e(),r.success?(this.setState({isUpdatingLogo:!1}),t("success",Object(n.__)("Store logo updated sucessfully",'woocommerce')),this.completeStep()):t("error",r.message)}async updateNotice(){const{clearTaskStatusCache:e,createNotice:t,updateOptions:o}=this.props,{storeNoticeText:c}=this.state;Object(m.recordEvent)("tasklist_appearance_set_store_notice",{added_text:Boolean(c.length)}),this.setState({isUpdatingNotice:!0});const s=await o({woocommerce_task_list_appearance_complete:!0,woocommerce_demo_store:c.length?"yes":"no",woocommerce_demo_store_notice:c});e(),s.success?(this.setState({isUpdatingNotice:!1}),t("success",Object(n.__)("🎨 Your store is looking great! Don't forget to continue personalizing it",'woocommerce')),this.completeStep()):t("error",s.message)}getSteps(){const{isDirty:e,isPending:t,logo:o,storeNoticeText:a,isUpdatingLogo:i}=this.state,r=[{key:"import",label:Object(n.__)("Import sample products",'woocommerce'),description:Object(n.__)("We’ll add some products that will make it easier to see what your store looks like",'woocommerce'),content:Object(c.createElement)(c.Fragment,null,Object(c.createElement)(s.Button,{onClick:this.importProducts,isBusy:t,isPrimary:!0},Object(n.__)("Import products",'woocommerce')),Object(c.createElement)(s.Button,{onClick:()=>this.completeStep()},Object(n.__)("Skip",'woocommerce'))),visible:this.stepVisibility.import},{key:"homepage",label:Object(n.__)("Create a custom homepage",'woocommerce'),description:Object(n.__)("Create a new homepage and customize it to suit your needs",'woocommerce'),content:Object(c.createElement)(c.Fragment,null,Object(c.createElement)(s.Button,{isPrimary:!0,isBusy:t,onClick:this.createHomepage},Object(n.__)("Create homepage",'woocommerce')),Object(c.createElement)(s.Button,{isTertiary:!0,onClick:()=>{Object(m.recordEvent)("tasklist_appearance_create_homepage",{create_homepage:!1}),this.completeStep()}},Object(n.__)("Skip",'woocommerce'))),visible:this.stepVisibility.homepage},{key:"logo",label:Object(n.__)("Upload a logo",'woocommerce'),description:Object(n.__)("Ensure your store is on-brand by adding your logo",'woocommerce'),content:t?null:Object(c.createElement)(c.Fragment,null,Object(c.createElement)(d.ImageUpload,{image:o,onChange:e=>this.setState({isDirty:!0,logo:e})}),Object(c.createElement)(s.Button,{disabled:!o&&!e,onClick:this.updateLogo,isBusy:i,isPrimary:!0},Object(n.__)("Proceed",'woocommerce')),Object(c.createElement)(s.Button,{isTertiary:!0,onClick:()=>this.completeStep()},Object(n.__)("Skip",'woocommerce'))),visible:!0},{key:"notice",label:Object(n.__)("Set a store notice",'woocommerce'),description:Object(n.__)("Optionally display a prominent notice across all pages of your store",'woocommerce'),content:Object(c.createElement)(c.Fragment,null,Object(c.createElement)(d.TextControl,{label:Object(n.__)("Store notice text",'woocommerce'),placeholder:Object(n.__)("Store notice text",'woocommerce'),value:a,onChange:e=>this.setState({storeNoticeText:e})}),Object(c.createElement)(s.Button,{onClick:this.updateNotice,isPrimary:!0},Object(n.__)("Complete task",'woocommerce'))),visible:!0}];return Object(u.filter)(r,e=>e.visible)}render(){const{isPending:e,stepIndex:t,isUpdatingLogo:o,isUpdatingNotice:n}=this.state,a=this.getSteps()[t].key;return Object(c.createElement)("div",{className:"woocommerce-task-appearance"},Object(c.createElement)(s.Card,{className:"woocommerce-task-card"},Object(c.createElement)(s.CardBody,null,Object(c.createElement)(d.Stepper,{isPending:n||o||e,isVertical:!0,currentStep:a,steps:this.getSteps()}))))}}var f=Object(p.compose)(Object(i.withSelect)(e=>{const{getOption:t}=e(r.OPTIONS_STORE_NAME),{getTasksStatus:o}=e(r.ONBOARDING_STORE_NAME),c=o();return{demoStoreNotice:t("woocommerce_demo_store_notice"),stylesheet:t("stylesheet"),tasksStatus:c}}),Object(i.withDispatch)(e=>{const{createNotice:t}=e("core/notices"),{updateOptions:o}=e(r.OPTIONS_STORE_NAME),{invalidateResolutionForStoreSelector:c}=e(r.ONBOARDING_STORE_NAME);return{clearTaskStatusCache:()=>c("getTasksStatus"),createNotice:t,updateOptions:o}}))(v),C=o(20),T=(o(593),o(507));o(594);const N=({description:e,imageUrl:t,installAndActivate:o=(()=>{}),isActive:a,isBusy:i,isDisabled:r,isInstalled:l,manageUrl:d,name:p,slug:u})=>Object(c.createElement)("div",{className:"woocommerce-plugin-list__plugin"},t&&Object(c.createElement)("div",{className:"woocommerce-plugin-list__plugin-logo"},Object(c.createElement)("img",{src:t,alt:Object(n.sprintf)(Object(n.__)("%s logo",'woocommerce'),p)})),Object(c.createElement)("div",{className:"woocommerce-plugin-list__plugin-text"},Object(c.createElement)(C.Text,{variant:"subtitle.small",as:"h4"},p),Object(c.createElement)(C.Text,{variant:"subtitle.small"},e)),Object(c.createElement)("div",{className:"woocommerce-plugin-list__plugin-action"},a&&d&&Object(c.createElement)(s.Button,{disabled:r,isBusy:i,isSecondary:!0,href:Object(b.e)(d),onClick:()=>Object(m.recordEvent)("marketing_manage",{extension_name:u})},Object(n.__)("Manage","woocommmerce-admin")),l&&!a&&Object(c.createElement)(s.Button,{disabled:r,isBusy:i,isSecondary:!0,onClick:()=>o(u)},Object(n.__)("Activate","woocommmerce-admin")),!l&&Object(c.createElement)(s.Button,{disabled:r,isBusy:i,isSecondary:!0,onClick:()=>o(u)},Object(n.__)("Get started","woocommmerce-admin"))));o(595);const x=({currentPlugin:e,installAndActivate:t=(()=>{}),plugins:o=[],title:n})=>Object(c.createElement)("div",{className:"woocommerce-plugin-list"},n&&Object(c.createElement)("div",{className:"woocommerce-plugin-list__title"},Object(c.createElement)(C.Text,{variant:"sectionheading",as:"h3"},n)),o.map(o=>{const{description:n,imageUrl:s,isActive:a,isInstalled:i,manageUrl:r,slug:l,name:m}=o;return Object(c.createElement)(N,{key:l,description:n,manageUrl:r,name:m,imageUrl:s,installAndActivate:t,isActive:a,isBusy:e===l,isDisabled:!!e,isInstalled:i,slug:l})})),P=["reach","grow"],A=(e,t,o)=>{const c=[],n=[];return e.forEach(e=>{if(!P.includes(e.key))return;const s=[];if(e.plugins.forEach(e=>{const n=((e,t,o)=>{const{description:c,image_url:n,key:s,manage_url:a,name:i}=e,r=s.split(":")[0];return{description:c,slug:r,imageUrl:n,isActive:t.includes(r),isInstalled:o.includes(r),manageUrl:a,name:i}})(e,t,o);n.isInstalled?c.push(n):s.push(n)}),!s.length)return;const a={...e,plugins:s};n.push(a)}),[c,n]},I=({trackedCompletedActions:e})=>{const[t,o]=Object(c.useState)(null),{installAndActivatePlugins:a}=Object(i.useDispatch)(r.PLUGINS_STORE_NAME),{updateOptions:l}=Object(i.useDispatch)(r.OPTIONS_STORE_NAME),{activePlugins:d,freeExtensions:p,installedPlugins:u,isResolving:_}=Object(i.useSelect)(e=>{const{getActivePlugins:t,getInstalledPlugins:o}=e(r.PLUGINS_STORE_NAME),{getFreeExtensions:c,hasFinishedResolution:n}=e(r.ONBOARDING_STORE_NAME);return{activePlugins:t(),freeExtensions:c(),installedPlugins:o(),isResolving:!n("getFreeExtensions")}}),[b,g]=Object(c.useMemo)(()=>A(p,d,u),[u,d,p]),h=t=>{o(t),a([t]).then(c=>{Object(m.recordEvent)("tasklist_marketing_install",{selected_extension:t,installed_extensions:b.map(e=>e.slug)}),e.includes("marketing")||l({woocommerce_task_list_tracked_completed_actions:[...e,"marketing"]}),Object(T.a)(c),o(null)}).catch(e=>{Object(T.a)(e),o(null)})};return _?Object(c.createElement)(s.Spinner,null):Object(c.createElement)("div",{className:"woocommerce-task-marketing"},!!b.length&&Object(c.createElement)(s.Card,{className:"woocommerce-task-card"},Object(c.createElement)(s.CardHeader,null,Object(c.createElement)(C.Text,{variant:"title.small",as:"h2",className:"woocommerce-task-card__title"},Object(n.__)("Installed marketing extensions",'woocommerce'))),Object(c.createElement)(x,{currentPlugin:t,installAndActivate:h,plugins:b})),!!g.length&&Object(c.createElement)(s.Card,{className:"woocommerce-task-card"},Object(c.createElement)(s.CardHeader,null,Object(c.createElement)(C.Text,{variant:"title.small",as:"h2",className:"woocommerce-task-card__title"},Object(n.__)("Recommended marketing extensions",'woocommerce')),Object(c.createElement)(C.Text,{as:"span"},Object(n.__)('We recommend adding one of the following marketing tools for your store. The extension will be installed and activated for you when you click "Get started".','woocommerce'))),g.map(e=>{const{key:o,title:n,plugins:s}=e;return Object(c.createElement)(x,{currentPlugin:t,installAndActivate:h,key:o,plugins:s,title:n})})))};var R=o(116),M=o(8),B=Object(c.createElement)(M.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(M.Path,{d:"M18 5.5H6a.5.5 0 00-.5.5v3h13V6a.5.5 0 00-.5-.5zm.5 5H10v8h8a.5.5 0 00.5-.5v-7.5zM6 4h12a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2z"})),G=o(473),L=Object(c.createElement)(M.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 24 24"},Object(c.createElement)(M.Path,{d:"M10 1c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9zm0 16c-3.9 0-7-3.1-7-7s3.1-7 7-7 7 3.1 7 7-3.1 7-7 7zm1-11H9v3H6v2h3v3h2v-3h3V9h-3V6zM10 1c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9zm0 16c-3.9 0-7-3.1-7-7s3.1-7 7-7 7 3.1 7 7-3.1 7-7 7zm1-11H9v3H6v2h3v3h2v-3h3V9h-3V6z"})),F=Object(c.createElement)(M.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},Object(c.createElement)(M.Path,{d:"M19 6.2h-5.9l-.6-1.1c-.3-.7-1-1.1-1.8-1.1H5c-1.1 0-2 .9-2 2v11.8c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V8.2c0-1.1-.9-2-2-2zm.5 11.6c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V6c0-.3.2-.5.5-.5h5.8c.2 0 .4.1.4.3l1 2H19c.3 0 .5.2.5.5v9.5zM8 12.8h8v-1.5H8v1.5zm0 3h8v-1.5H8v1.5z"})),U=Object(c.createElement)(M.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Object(c.createElement)(M.Path,{d:"M18 11.3l-1-1.1-4 4V3h-1.5v11.3L7 10.2l-1 1.1 6.2 5.8 5.8-5.8zm.5 3.7v3.5h-13V15H4v5h16v-5h-1.5z"}));o(596);const D=()=>[{key:"physical",title:Object(n.__)("Physical product",'woocommerce'),subtitle:Object(n.__)("Tangible items that get delivered to customers",'woocommerce')},{key:"digital",title:Object(n.__)("Digital product",'woocommerce'),subtitle:Object(n.__)("Items that customers download or access through your website",'woocommerce')},{key:"variable",title:Object(n.__)("Variable product",'woocommerce'),subtitle:Object(n.__)("Products with several versions that customers can choose from",'woocommerce')},{key:"subscription",title:Object(n.__)("Subscription product",'woocommerce'),subtitle:Object(n.__)("Products that customers receive or gain access to regularly by paying in advance",'woocommerce')}];function z({onClose:e}){const[t,o]=Object(c.useState)(null),[a,l]=Object(c.useState)(!1),{createProductFromTemplate:d}=Object(i.useDispatch)(r.ITEMS_STORE_NAME),{countryCode:p,profileItems:u}=Object(i.useSelect)(e=>{const{getProfileItems:t}=e(r.ONBOARDING_STORE_NAME),{getSettings:o}=e(r.SETTINGS_STORE_NAME),{general:c={}}=o("general");return{countryCode:Object(g.b)(c.woocommerce_default_country),profileItems:t()}}),{installedPlugins:_}=Object(i.useSelect)(e=>{const{getInstalledPlugins:t}=e(r.PLUGINS_STORE_NAME);return{installedPlugins:t()}}),h=window.wcAdminFeatures&&!window.wcAdminFeatures.subscriptions||"US"!==p||!u.product_types.includes("subscriptions")||!_.includes("woocommerce-payments")?D().filter(e=>"subscription"!==e.key):D(),O=Object(y.applyFilters)("woocommerce_admin_onboarding_product_templates",h);return Object(c.createElement)(s.Modal,{title:Object(n.__)("Start with a template"),isDismissible:!0,onRequestClose:()=>e(),className:"woocommerce-product-template-modal"},Object(c.createElement)("div",{className:"woocommerce-product-template-modal__wrapper"},Object(c.createElement)("div",{className:"woocommerce-product-template-modal__list"},Object(c.createElement)(s.RadioControl,{selected:t,options:O.map(e=>({label:Object(c.createElement)(c.Fragment,null,Object(c.createElement)("span",{className:"woocommerce-product-template-modal__list-title"},e.title),Object(c.createElement)("span",{className:"woocommerce-product-template-modal__list-subtitle"},e.subtitle)),value:e.key})),onChange:o})),Object(c.createElement)("div",{className:"woocommerce-product-template-modal__actions"},Object(c.createElement)(s.Button,{isPrimary:!0,isBusy:a,disabled:!t||a,onClick:()=>{l(!0),Object(m.recordEvent)("tasklist_product_template_selection",{product_type:t}),"subscription"!==t?t?d({template_name:t,status:"draft"},{_fields:["id"]}).then(e=>{if(e&&e.id){const t=Object(b.e)(`post.php?post=${e.id}&action=edit&wc_onboarding_active_task=products&tutorial=true`);window.location=t}},e=>{Object(T.a)(e),l(!1)}):e&&(Object(m.recordEvent)("tasklist_product_template_dismiss"),e()):window.location=Object(b.e)("post-new.php?post_type=product&subscription_pointers=true")}},Object(n.__)("Go")))))}function H(){const[e,t]=Object(c.useState)(null),{countryCode:o,profileItems:a}=Object(i.useSelect)(e=>{const{getProfileItems:t}=e(r.ONBOARDING_STORE_NAME),{getSettings:o}=e(r.SETTINGS_STORE_NAME),{general:c={}}=o("general");return{countryCode:Object(g.b)(c.woocommerce_default_country),profileItems:t()}}),{installedPlugins:l}=Object(i.useSelect)(e=>{const{getInstalledPlugins:t}=e(r.PLUGINS_STORE_NAME);return{installedPlugins:t()}}),p=[{key:"addProductTemplate",title:Object(c.createElement)(c.Fragment,null,Object(n.__)("Start with a template",'woocommerce'),Object(c.createElement)(d.Pill,null,Object(n.__)("Recommended",'woocommerce'))),content:Object(n.__)("Use a template to add physical, digital, and variable products",'woocommerce'),before:Object(c.createElement)(R.a,{icon:B}),after:Object(c.createElement)(R.a,{icon:G.a}),onClick:()=>Object(m.recordEvent)("tasklist_add_product",{method:"product_template"})},{key:"addProductManually",title:Object(n.__)("Add manually",'woocommerce'),content:Object(n.__)("For small stores we recommend adding products manually",'woocommerce'),before:Object(c.createElement)(R.a,{icon:L}),after:Object(c.createElement)(R.a,{icon:G.a}),onClick:()=>Object(m.recordEvent)("tasklist_add_product",{method:"manually"}),href:Object(b.e)("post-new.php?post_type=product&wc_onboarding_active_task=products&tutorial=true")},{key:"importProducts",title:Object(n.__)("Import via CSV",'woocommerce'),content:Object(n.__)("For larger stores we recommend importing all products at once via CSV file",'woocommerce'),before:Object(c.createElement)(R.a,{icon:F}),after:Object(c.createElement)(R.a,{icon:G.a}),onClick:()=>Object(m.recordEvent)("tasklist_add_product",{method:"import"}),href:Object(b.e)("edit.php?post_type=product&page=product_importer&wc_onboarding_active_task=product-import")},{key:"migrateProducts",title:Object(n.__)("Import from another service",'woocommerce'),content:Object(n.__)("For stores currently selling elsewhere we suggest using a product migration service",'woocommerce'),before:Object(c.createElement)(R.a,{icon:U}),after:Object(c.createElement)(R.a,{icon:G.a}),onClick:()=>Object(m.recordEvent)("tasklist_add_product",{method:"migrate"}),href:"https://woocommerce.com/products/cart2cart/?utm_medium=product",target:"_blank"}];if(window.wcAdminFeatures&&window.wcAdminFeatures.subscriptions&&"US"===o&&a.product_types.includes("subscriptions")&&l.includes("woocommerce-payments")){p.find(({key:e})=>"addProductTemplate"===e).content=Object(n.__)("Use a template to add physical, digital, variable, and subscription products",'woocommerce')}const u=p.map(e=>({...e,onClick:()=>(e=>{e.onClick(),"addProductTemplate"===e.key&&t(!0)})(e)}));return Object(c.createElement)(c.Fragment,null,Object(c.createElement)(s.Card,{className:"woocommerce-task-card"},Object(c.createElement)(s.CardBody,{size:null},Object(c.createElement)(d.List,{items:u}))),e?Object(c.createElement)(z,{onClose:()=>t(null)}):null)}var q=o(35),V=o.n(q),J=o(18),W=o.n(J),Y=o(1),Z=o.n(Y);class $ extends c.Component{constructor(e){super(e),this.state={isConnecting:!1},this.connectJetpack=this.connectJetpack.bind(this),e.setIsPending(!0)}componentDidUpdate(e){const{createNotice:t,error:o,isRequesting:c,onError:n,setIsPending:s}=this.props;e.isRequesting&&!c&&s(!1),o&&o!==e.error&&(n&&n(),t("error",o))}async connectJetpack(){const{jetpackConnectUrl:e,onConnect:t}=this.props;this.setState({isConnecting:!0},()=>{t&&t(),window.location=e})}render(){const{hasErrors:e,isRequesting:t,onSkip:o,skipText:a,onAbort:i,abortText:r}=this.props;return Object(c.createElement)(c.Fragment,null,e?Object(c.createElement)(s.Button,{isPrimary:!0,onClick:()=>window.location.reload()},Object(n.__)("Retry",'woocommerce')):Object(c.createElement)(s.Button,{disabled:t,isBusy:this.state.isConnecting,isPrimary:!0,onClick:this.connectJetpack},Object(n.__)("Connect",'woocommerce')),o&&Object(c.createElement)(s.Button,{onClick:o},a||Object(n.__)("No thanks",'woocommerce')),i&&Object(c.createElement)(s.Button,{onClick:i},r||Object(n.__)("Abort",'woocommerce')))}}$.propTypes={createNotice:Z.a.func.isRequired,error:Z.a.string,hasErrors:Z.a.bool,isRequesting:Z.a.bool,jetpackConnectUrl:Z.a.string,onConnect:Z.a.func,onError:Z.a.func,onSkip:Z.a.func,redirectUrl:Z.a.string,skipText:Z.a.string,setIsPending:Z.a.func,onAbort:Z.a.func,abortText:Z.a.string},$.defaultProps={setIsPending:()=>{}};var Q=Object(p.compose)(Object(i.withSelect)((e,t)=>{const{getJetpackConnectUrl:o,isPluginsRequesting:c,getPluginsError:n}=e(r.PLUGINS_STORE_NAME),s={redirect_url:t.redirectUrl||window.location.href},a=c("getJetpackConnectUrl");return{error:n("getJetpackConnectUrl")||"",isRequesting:a,jetpackConnectUrl:o(s)}}),Object(i.withDispatch)(e=>{const{createNotice:t}=e("core/notices");return{createNotice:t}}))($),K=o(527);class X extends c.Component{constructor(){super(...arguments),this.onSubmit=this.onSubmit.bind(this)}async onSubmit(e){const{onComplete:t,createNotice:o,isSettingsError:c,updateAndPersistSettingsForGroup:s,settings:a}=this.props;await s("general",{general:{...a,woocommerce_store_address:e.addressLine1,woocommerce_store_address_2:e.addressLine2,woocommerce_default_country:e.countryState,woocommerce_store_city:e.city,woocommerce_store_postcode:e.postCode}}),c?o("error",Object(n.__)("There was a problem saving your store location",'woocommerce')):t(e)}getInitialValues(){const{settings:e}=this.props,{woocommerce_store_address:t,woocommerce_store_address_2:o,woocommerce_store_city:c,woocommerce_default_country:n,woocommerce_store_postcode:s}=e;return{addressLine1:t||"",addressLine2:o||"",city:c||"",countryState:n||"",postCode:s||""}}render(){const{isSettingsRequesting:e}=this.props;return e?null:Object(c.createElement)(d.Form,{initialValues:this.getInitialValues(),onSubmit:this.onSubmit,validate:K.b},({getInputProps:e,handleSubmit:t,setValue:o})=>Object(c.createElement)(c.Fragment,null,Object(c.createElement)(K.a,{getInputProps:e,setValue:o}),Object(c.createElement)(s.Button,{isPrimary:!0,onClick:t},Object(n.__)("Continue",'woocommerce'))))}}var ee=Object(c.createElement)(M.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"-2 -2 24 24"},Object(c.createElement)(M.Path,{d:"M9 0C4.03 0 0 4.03 0 9s4.03 9 9 9 9-4.03 9-9-4.03-9-9-9zM1.11 9.68h2.51c.04.91.167 1.814.38 2.7H1.84c-.403-.85-.65-1.764-.73-2.7zm8.57-5.4V1.19c.964.366 1.756 1.08 2.22 2 .205.347.386.708.54 1.08l-2.76.01zm3.22 1.35c.232.883.37 1.788.41 2.7H9.68v-2.7h3.22zM8.32 1.19v3.09H5.56c.154-.372.335-.733.54-1.08.462-.924 1.255-1.64 2.22-2.01zm0 4.44v2.7H4.7c.04-.912.178-1.817.41-2.7h3.21zm-4.7 2.69H1.11c.08-.936.327-1.85.73-2.7H4c-.213.886-.34 1.79-.38 2.7zM4.7 9.68h3.62v2.7H5.11c-.232-.883-.37-1.788-.41-2.7zm3.63 4v3.09c-.964-.366-1.756-1.08-2.22-2-.205-.347-.386-.708-.54-1.08l2.76-.01zm1.35 3.09v-3.04h2.76c-.154.372-.335.733-.54 1.08-.464.92-1.256 1.634-2.22 2v-.04zm0-4.44v-2.7h3.62c-.04.912-.178 1.817-.41 2.7H9.68zm4.71-2.7h2.51c-.08.936-.327 1.85-.73 2.7H14c.21-.87.337-1.757.38-2.65l.01-.05zm0-1.35c-.046-.894-.176-1.78-.39-2.65h2.16c.403.85.65 1.764.73 2.7l-2.5-.05zm1-4H13.6c-.324-.91-.793-1.76-1.39-2.52 1.244.56 2.325 1.426 3.14 2.52h.04zm-9.6-2.52c-.597.76-1.066 1.61-1.39 2.52H2.65c.815-1.094 1.896-1.96 3.14-2.52zm-3.15 12H4.4c.324.91.793 1.76 1.39 2.52-1.248-.567-2.33-1.445-3.14-2.55l-.01.03zm9.56 2.52c.597-.76 1.066-1.61 1.39-2.52h1.76c-.82 1.08-1.9 1.933-3.14 2.48l-.01.04z"})),te=o(501);class oe extends c.Component{constructor(){super(...arguments),this.updateShippingZones=this.updateShippingZones.bind(this)}getShippingMethods(e,t=null){return e&&e.methods&&Array.isArray(e.methods)?t?e.methods?e.methods.filter(e=>e.method_id===t):[]:e.methods:[]}disableShippingMethods(e,t){t.length&&t.forEach(t=>{S()({method:"POST",path:`/wc/v3/shipping/zones/${e.id}/methods/${t.instance_id}`,data:{enabled:!1}})})}async updateShippingZones(e){const{clearTaskStatusCache:t,createNotice:o,shippingZones:c}=this.props;let s=!1,a=!1;c.forEach(t=>{0===t.id?s=t.toggleable&&e[t.id+"_enabled"]:a=""!==e[t.id+"_rate"]&&parseFloat(e[t.id+"_rate"])!==parseFloat(0);const o=this.getShippingMethods(t),c=parseFloat(e[t.id+"_rate"])===parseFloat(0)?"free_shipping":"flat_rate",n=this.getShippingMethods(t,c).length?this.getShippingMethods(t,c)[0]:null;if(!t.toggleable||e[t.id+"_enabled"]){if(n){const e=o.filter(e=>e.instance_id!==n.instance_id);this.disableShippingMethods(t,e)}S()({method:"POST",path:n?`/wc/v3/shipping/zones/${t.id}/methods/${n.instance_id}`:`/wc/v3/shipping/zones/${t.id}/methods`,data:{method_id:c,enabled:!0,settings:{cost:e[t.id+"_rate"]}}})}else this.disableShippingMethods(t,o)}),Object(m.recordEvent)("tasklist_shipping_set_costs",{shipping_cost:a,rest_world:s}),t(),o("success",Object(n.__)("Your shipping rates have been updated",'woocommerce')),this.props.onComplete()}renderInputPrefix(){const{symbolPosition:e,symbol:t}=this.context.getCurrencyConfig();return 0===e.indexOf("right")?null:Object(c.createElement)("span",{className:"woocommerce-shipping-rate__control-prefix"},t)}renderInputSuffix(e){const{symbolPosition:t,symbol:o}=this.context.getCurrencyConfig();return 0===t.indexOf("right")?Object(c.createElement)("span",{className:"woocommerce-shipping-rate__control-suffix"},o):parseFloat(e)===parseFloat(0)?Object(c.createElement)("span",{className:"woocommerce-shipping-rate__control-suffix"},Object(n.__)("Free shipping",'woocommerce')):null}getFormattedRate(e){const{formatDecimalString:t}=this.context,o=t(e);return e.length&&o.length?t(e):t(0)}getInitialValues(){const{formatDecimalString:e}=this.context,t={};return this.props.shippingZones.forEach(o=>{const c=this.getShippingMethods(o),n=c.length&&c[0].settings.cost?this.getFormattedRate(c[0].settings.cost.value):e(0);t[o.id+"_rate"]=n,c.length&&c[0].enabled?t[o.id+"_enabled"]=!0:t[o.id+"_enabled"]=!1}),t}validate(e){const t={};return Object.keys(e).filter(e=>e.endsWith("_rate")).forEach(o=>{e[o]<0&&(t[o]=Object(n.__)("Shipping rates can not be negative numbers.",'woocommerce'))}),t}render(){const{buttonText:e,shippingZones:t}=this.props;return t.length?Object(c.createElement)(d.Form,{initialValues:this.getInitialValues(),onSubmit:this.updateShippingZones,validate:this.validate},({getInputProps:o,handleSubmit:a,setTouched:i,setValue:r,values:l})=>Object(c.createElement)(c.Fragment,null,Object(c.createElement)("div",{className:"woocommerce-shipping-rates"},t.map(e=>Object(c.createElement)("div",{className:"woocommerce-shipping-rate",key:e.id},Object(c.createElement)("div",{className:"woocommerce-shipping-rate__icon"},e.locations?e.locations.map(e=>Object(c.createElement)(d.Flag,{size:24,code:e.code,key:e.code})):Object(c.createElement)(R.a,{icon:ee})),Object(c.createElement)("div",{className:"woocommerce-shipping-rate__main"},e.toggleable?Object(c.createElement)("label",{htmlFor:"woocommerce-shipping-rate__toggle-"+e.id,className:"woocommerce-shipping-rate__name"},e.name,Object(c.createElement)(s.FormToggle,V()({id:"woocommerce-shipping-rate__toggle-"+e.id},o(e.id+"_enabled")))):Object(c.createElement)("div",{className:"woocommerce-shipping-rate__name"},e.name),(!e.toggleable||l[e.id+"_enabled"])&&Object(c.createElement)(d.TextControlWithAffixes,V()({label:Object(n.__)("Shipping cost",'woocommerce'),required:!0},o(e.id+"_rate"),{onBlur:()=>{i(e.id+"_rate"),r(e.id+"_rate",this.getFormattedRate(l[e.id+"_rate"]))},prefix:this.renderInputPrefix(),suffix:this.renderInputSuffix(l[e.id+"_rate"]),className:"muriel-input-text woocommerce-shipping-rate__control-wrapper"})))))),Object(c.createElement)(s.Button,{isPrimary:!0,onClick:a},e||Object(n.__)("Update",'woocommerce')))):null}}oe.propTypes={buttonText:Z.a.string,onComplete:Z.a.func.isRequired,createNotice:Z.a.func.isRequired,shippingZones:Z.a.array},oe.defaultProps={shippingZones:[]},oe.contextType=te.a;var ce=Object(p.compose)(Object(i.withDispatch)(e=>{const{invalidateResolutionForStoreSelector:t}=e(r.ONBOARDING_STORE_NAME);return{clearTaskStatusCache:()=>t("getTasksStatus")}}))(oe);class ne extends c.Component{constructor(e){super(e),this.initialState={isPending:!1,step:"store_location",shippingZones:[]},this.activePlugins=e.activePlugins,this.state=this.initialState,this.completeStep=this.completeStep.bind(this)}componentDidMount(){this.reset()}reset(){this.setState(this.initialState)}async fetchShippingZones(){this.setState({isPending:!0});const{countryCode:e,countryName:t}=this.props,o=[],c=await S()({path:"/wc/v3/shipping/zones"});let s=!1;if(await Promise.all(c.map(async t=>{if(0===t.id)return t.methods=await S()({path:`/wc/v3/shipping/zones/${t.id}/methods`}),t.name=Object(n.__)("Rest of the world",'woocommerce'),t.toggleable=!0,void o.push(t);t.locations=await S()({path:`/wc/v3/shipping/zones/${t.id}/locations`});t.locations.find(t=>e===t.code)&&(t.methods=await S()({path:`/wc/v3/shipping/zones/${t.id}/methods`}),o.push(t),s=!0)})),!s){const c=await S()({method:"POST",path:"/wc/v3/shipping/zones",data:{name:t}});c.locations=await S()({method:"POST",path:`/wc/v3/shipping/zones/${c.id}/locations`,data:[{code:e,type:"country"}]}),o.push(c)}o.reverse(),this.setState({isPending:!1,shippingZones:o})}componentDidUpdate(e,t){const{countryCode:o,settings:c}=this.props,{woocommerce_store_address:n,woocommerce_default_country:s,woocommerce_store_postcode:a}=c,{step:i}=this.state;"rates"!==i||e.countryCode===o&&"rates"===t.step||this.fetchShippingZones();const r=Boolean(n&&s&&a);"store_location"===i&&r&&this.completeStep()}completeStep(){const{createNotice:e}=this.props,{step:t}=this.state,o=this.getSteps(),c=o.findIndex(e=>e.key===t),s=o[c+1];s?this.setState({step:s.key}):(e("success",Object(n.__)("📦 Shipping is done! Don't worry, you can always change it later",'woocommerce')),Object(k.getHistory)().push(Object(k.getNewPath)({},"/",{})))}getPluginsToActivate(){const{countryCode:e}=this.props,t=[];return["GB","CA","AU"].includes(e)?t.push("woocommerce-shipstation-integration"):"US"===e&&(t.push("woocommerce-services"),t.push("jetpack")),Object(u.difference)(t,this.activePlugins)}getSteps(){const{countryCode:e,isJetpackConnected:t,settings:o}=this.props,s=this.getPluginsToActivate(),a=!t&&"US"===e,i=[{key:"store_location",label:Object(n.__)("Set store location",'woocommerce'),description:Object(n.__)("The address from which your business operates",'woocommerce'),content:Object(c.createElement)(X,V()({},this.props,{onComplete:e=>{const t=Object(g.b)(e.countryState);Object(m.recordEvent)("tasklist_shipping_set_location",{country:t}),this.completeStep()}})),visible:!0},{key:"rates",label:Object(n.__)("Set shipping costs",'woocommerce'),description:Object(n.__)("Define how much customers pay to ship to different destinations",'woocommerce'),content:Object(c.createElement)(ce,V()({buttonText:s.length||a?Object(n.__)("Proceed",'woocommerce'):Object(n.__)("Complete task",'woocommerce'),shippingZones:this.state.shippingZones,onComplete:this.completeStep},this.props)),visible:"disabled"!==o.woocommerce_ship_to_countries},{key:"label_printing",label:Object(n.__)("Enable shipping label printing",'woocommerce'),description:s.includes("woocommerce-shipstation-integration")?W()({mixedString:Object(n.__)("We recommend using ShipStation to save time at the post office by printing your shipping labels at home. Try ShipStation free for 30 days. {{link}}Learn more{{/link}}.",'woocommerce'),components:{link:Object(c.createElement)(d.Link,{href:"https://woocommerce.com/products/shipstation-integration?utm_medium=product",target:"_blank",type:"external"})}}):Object(n.__)("With WooCommerce Shipping you can save time by printing your USPS and DHL Express shipping labels at home",'woocommerce'),content:Object(c.createElement)(d.Plugins,V()({onComplete:(e,t)=>{Object(T.a)(t),Object(m.recordEvent)("tasklist_shipping_label_printing",{install:!0,plugins_to_activate:s}),this.completeStep()},onError:(e,t)=>Object(T.a)(t),onSkip:()=>{Object(m.recordEvent)("tasklist_shipping_label_printing",{install:!1,plugins_to_activate:s}),Object(k.getHistory)().push(Object(k.getNewPath)({},"/",{}))},pluginSlugs:s},this.props)),visible:s.length},{key:"connect",label:Object(n.__)("Connect your store",'woocommerce'),description:Object(n.__)("Connect your store to WordPress.com to enable label printing",'woocommerce'),content:Object(c.createElement)(Q,V()({redirectUrl:Object(b.e)("admin.php?page=wc-admin"),completeStep:this.completeStep},this.props,{onConnect:()=>{Object(m.recordEvent)("tasklist_shipping_connect_store")}})),visible:a}];return Object(u.filter)(i,e=>e.visible)}render(){const{isPending:e,step:t}=this.state,{isUpdateSettingsRequesting:o}=this.props;return Object(c.createElement)("div",{className:"woocommerce-task-shipping"},Object(c.createElement)(s.Card,{className:"woocommerce-task-card"},Object(c.createElement)(s.CardBody,null,Object(c.createElement)(d.Stepper,{isPending:e||o,isVertical:!0,currentStep:t,steps:this.getSteps()}))))}}var se=Object(p.compose)(Object(i.withSelect)(e=>{const{getSettings:t,isUpdateSettingsRequesting:o}=e(r.SETTINGS_STORE_NAME),{getActivePlugins:c,isJetpackConnected:n}=e(r.PLUGINS_STORE_NAME),{general:s={}}=t("general"),a=Object(g.b)(s.woocommerce_default_country),{countries:i=[]}=Object(b.f)("dataEndpoints",{}),l=a?i.find(e=>e.code===a):null,m=l?l.name:null,d=c();return{countryCode:a,countryName:m,isUpdateSettingsRequesting:o("general"),settings:s,activePlugins:d,isJetpackConnected:n()}}),Object(i.withDispatch)(e=>{const{createNotice:t}=e("core/notices"),{updateAndPersistSettingsForGroup:o}=e(r.SETTINGS_STORE_NAME);return{createNotice:t,updateAndPersistSettingsForGroup:o}}))(ne);class ae extends c.Component{constructor(e){super(e);const{hasCompleteAddress:t,pluginsToActivate:o}=e;this.initialState={isPending:!1,stepIndex:t?1:0,cachedPluginsToActivate:o},this.state=this.initialState,this.completeStep=this.completeStep.bind(this)}componentDidMount(){const{query:e}=this.props,{auto:t}=e;this.reset(),"true"===t&&this.enableAutomatedTax()}reset(){this.setState(this.initialState)}shouldShowSuccessScreen(){const{isJetpackConnected:e,hasCompleteAddress:t,pluginsToActivate:o}=this.props;return t&&!o.length&&e&&this.isTaxJarSupported()}isTaxJarSupported(){const{countryCode:e,tasksStatus:t}=this.props,{automatedTaxSupportedCountries:o=[],taxJarActivated:c}=t;return!c&&o.includes(e)}completeStep(){const{stepIndex:e}=this.state;this.getSteps()[e+1]&&this.setState({stepIndex:e+1})}async manuallyConfigureTaxRates(){const{generalSettings:e,updateAndPersistSettingsForGroup:t}=this.props;"yes"!==e.woocommerce_calc_taxes?(this.setState({isPending:!0}),t("general",{general:{...e,woocommerce_calc_taxes:"yes"}}).then(()=>this.redirectToTaxSettings()).catch(e=>Object(T.a)(e))):this.redirectToTaxSettings()}updateAutomatedTax(e){const{clearTaskStatusCache:t,createNotice:o,updateAndPersistSettingsForGroup:c,generalSettings:s,taxSettings:a}=this.props;Promise.all([c("tax",{tax:{...a,wc_connect_taxes_enabled:e?"yes":"no"}}),c("general",{general:{...s,woocommerce_calc_taxes:"yes"}})]).then(()=>{t(),e?(o("success",Object(n.__)("You're awesome! One less item on your to-do list ✅",'woocommerce')),Object(k.getHistory)().push(Object(k.getNewPath)({},"/",{}))):this.redirectToTaxSettings()}).catch(()=>{o("error",Object(n.__)("There was a problem updating your tax settings",'woocommerce'))})}redirectToTaxSettings(){window.location=Object(b.e)("admin.php?page=wc-settings&tab=tax§ion=standard&wc_onboarding_active_task=tax")}doNotChargeSalesTax(){const{updateOptions:e}=this.props;Object(m.queueRecordEvent)("tasklist_tax_connect_store",{connect:!1,no_tax:!0}),e({woocommerce_no_sales_tax:!0,woocommerce_calc_taxes:"no"}).then(()=>{window.location=Object(b.e)("admin.php?page=wc-admin")})}getSteps(){const{generalSettings:e,isJetpackConnected:t,isPending:o,tosAccepted:a,updateOptions:i}=this.props,{cachedPluginsToActivate:r}=this.state;let l,p;r.includes("woocommerce-services")?(l=Object(n.__)("Install Jetpack and WooCommerce Tax",'woocommerce'),p=Object(n.__)("By installing Jetpack and WooCommerce Tax you agree to the {{link}}Terms of Service{{/link}}.",'woocommerce')):(l=Object(n.__)("Install Jetpack",'woocommerce'),p=Object(n.__)("By installing Jetpack you agree to the {{link}}Terms of Service{{/link}}.",'woocommerce'));const _=[{key:"store_location",label:Object(n.__)("Set store location",'woocommerce'),description:Object(n.__)("The address from which your business operates",'woocommerce'),content:Object(c.createElement)(X,V()({},this.props,{onComplete:e=>{const t=Object(g.b)(e.countryState);Object(m.recordEvent)("tasklist_tax_set_location",{country:t}),this.completeStep()},isSettingsRequesting:!1,settings:e})),visible:!0},{key:"plugins",label:l,description:Object(n.__)("Jetpack and WooCommerce Tax allow you to automate sales tax calculations",'woocommerce'),content:Object(c.createElement)(c.Fragment,null,Object(c.createElement)(d.Plugins,{onComplete:(e,t)=>{Object(T.a)(t),Object(m.recordEvent)("tasklist_tax_install_extensions",{install_extensions:!0}),i({woocommerce_setup_jetpack_opted_in:!0}),this.completeStep()},onError:(e,t)=>Object(T.a)(t),onSkip:()=>{Object(m.queueRecordEvent)("tasklist_tax_install_extensions",{install_extensions:!1}),this.manuallyConfigureTaxRates()},skipText:Object(n.__)("Set up manually",'woocommerce'),onAbort:()=>this.doNotChargeSalesTax(),abortText:Object(n.__)("I don't charge sales tax",'woocommerce')}),!a&&Object(c.createElement)(C.Text,{variant:"caption",className:"woocommerce-task__caption",size:"12",lineHeight:"16px"},W()({mixedString:p,components:{link:Object(c.createElement)(d.Link,{href:"https://wordpress.com/tos/",target:"_blank",type:"external"})}}))),visible:(r.length||!a)&&this.isTaxJarSupported()},{key:"connect",label:Object(n.__)("Connect your store",'woocommerce'),description:Object(n.__)("Connect your store to WordPress.com to enable automated sales tax calculations",'woocommerce'),content:Object(c.createElement)(Q,V()({},this.props,{onConnect:()=>{Object(m.recordEvent)("tasklist_tax_connect_store",{connect:!0,no_tax:!1})},onSkip:()=>{Object(m.queueRecordEvent)("tasklist_tax_connect_store",{connect:!1,no_tax:!1}),this.manuallyConfigureTaxRates()},skipText:Object(n.__)("Set up tax rates manually",'woocommerce'),onAbort:()=>this.doNotChargeSalesTax(),abortText:Object(n.__)("My business doesn't charge sales tax",'woocommerce')})),visible:!t&&this.isTaxJarSupported()},{key:"manual_configuration",label:Object(n.__)("Configure tax rates",'woocommerce'),description:Object(n.__)("Head over to the tax rate settings screen to configure your tax rates",'woocommerce'),content:Object(c.createElement)(c.Fragment,null,Object(c.createElement)(s.Button,{disabled:o,isPrimary:!0,isBusy:o,onClick:()=>{Object(m.recordEvent)("tasklist_tax_config_rates"),this.manuallyConfigureTaxRates()}},Object(n.__)("Configure",'woocommerce')),Object(c.createElement)("p",null,"yes"!==e.woocommerce_calc_taxes&&W()({mixedString:Object(n.__)('By clicking "Configure" you\'re enabling tax rates and calculations. More info {{link}}here{{/link}}.','woocommerce'),components:{link:Object(c.createElement)(d.Link,{href:"https://docs.woocommerce.com/document/setting-up-taxes-in-woocommerce/?utm_medium=product#section-1",target:"_blank",type:"external"})}}))),visible:!this.isTaxJarSupported()}];return Object(u.filter)(_,e=>e.visible)}enableAutomatedTax(){Object(m.recordEvent)("tasklist_tax_setup_automated_proceed",{setup_automatically:!0}),this.updateAutomatedTax(!0)}renderSuccessScreen(){const{isPending:e}=this.props;return Object(c.createElement)("div",{className:"woocommerce-task-tax__success"},Object(c.createElement)("span",{className:"woocommerce-task-tax__success-icon",role:"img","aria-labelledby":"woocommerce-task-tax__success-message"},"🎊"),Object(c.createElement)(d.H,{id:"woocommerce-task-tax__success-message"},Object(n.__)("Good news!",'woocommerce')),Object(c.createElement)("p",null,W()({mixedString:Object(n.__)("{{strong}}Jetpack{{/strong}} and {{strong}}WooCommerce Tax{{/strong}} can automate your sales tax calculations for you.",'woocommerce'),components:{strong:Object(c.createElement)("strong",null)}})),Object(c.createElement)(s.Button,{disabled:e,isPrimary:!0,isBusy:e,onClick:this.enableAutomatedTax.bind(this)},Object(n.__)("Yes please",'woocommerce')),Object(c.createElement)(s.Button,{disabled:e,isTertiary:!0,onClick:()=>{Object(m.recordEvent)("tasklist_tax_setup_automated_proceed",{setup_automatically:!1}),this.updateAutomatedTax(!1)}},Object(n.__)("No thanks, I'll set up manually",'woocommerce')),Object(c.createElement)(s.Button,{disabled:e,isTertiary:!0,onClick:()=>this.doNotChargeSalesTax()},Object(n.__)("I don't charge sales tax",'woocommerce')))}render(){const{stepIndex:e}=this.state,{isPending:t,isResolving:o}=this.props;if(o)return Object(c.createElement)(d.Spinner,null);const n=this.getSteps()[e];return Object(c.createElement)("div",{className:"woocommerce-task-tax"},Object(c.createElement)(s.Card,{className:"woocommerce-task-card"},Object(c.createElement)(s.CardBody,null,this.shouldShowSuccessScreen()?this.renderSuccessScreen():Object(c.createElement)(d.Stepper,{isPending:t||o,isVertical:!0,currentStep:n.key,steps:this.getSteps()}))))}}var ie=Object(p.compose)(Object(i.withSelect)(e=>{const{getSettings:t,isUpdateSettingsRequesting:o}=e(r.SETTINGS_STORE_NAME),{getOption:c,hasFinishedResolution:n}=e(r.OPTIONS_STORE_NAME),{getActivePlugins:s,isJetpackConnected:a,isPluginsRequesting:i}=e(r.PLUGINS_STORE_NAME),{getTasksStatus:l}=e(r.ONBOARDING_STORE_NAME),{general:m={}}=t("general"),d=Object(g.b)(m.woocommerce_default_country),{woocommerce_store_address:p,woocommerce_default_country:_,woocommerce_store_postcode:b}=m,h=Boolean(p&&_&&b),{tax:O={}}=t("tax"),j=s(),w=Object(u.difference)(["jetpack","woocommerce-services"],j),y=c("wc_connect_options")||{},k=c("woocommerce_setup_jetpack_opted_in"),E=y.tos_accepted||"1"===k,S=l(),v=o("tax")||o("general"),f=i("getJetpackConnectUrl")||!n("getOption",["woocommerce_setup_jetpack_opted_in"])||!n("getOption",["wc_connect_options"])||void 0===k||void 0===y;return{countryCode:d,generalSettings:m,hasCompleteAddress:h,isJetpackConnected:a(),isPending:v,isResolving:f,pluginsToActivate:w,tasksStatus:S,taxSettings:O,tosAccepted:E}}),Object(i.withDispatch)(e=>{const{createNotice:t}=e("core/notices"),{updateOptions:o}=e(r.OPTIONS_STORE_NAME),{updateAndPersistSettingsForGroup:c}=e(r.SETTINGS_STORE_NAME),{invalidateResolutionForStoreSelector:n}=e(r.ONBOARDING_STORE_NAME);return{clearTaskStatusCache:()=>n("getTasksStatus"),createNotice:t,updateAndPersistSettingsForGroup:c,updateOptions:o}}))(ae),re=o(6),le=o.n(re),me=o(271),de=o(542);o(538);const pe=({isRecommended:e,markConfigured:t,paymentGateway:o})=>{var n,a;const{image:i,content:r,id:l,plugins:d=[],title:p,loading:u,enabled:_=!1,installed:b=!1,needsSetup:g=!0,requiredSettings:h,settingsUrl:O,is_local_partner:j}=o,w=Object(C.useSlot)("woocommerce_payment_gateway_configure_"+l),y=Object(C.useSlot)("woocommerce_payment_gateway_setup_"+l),k=Boolean(null==w||null===(n=w.fills)||void 0===n?void 0:n.length)||Boolean(null==y||null===(a=y.fills)||void 0===a?void 0:a.length),E=Boolean(d.length||h.length||k),S=e&&g,v=le()("woocommerce-task-payment","woocommerce-task-card",g&&"woocommerce-task-payment-not-configured","woocommerce-task-payment-"+l);return Object(c.createElement)(c.Fragment,{key:l},Object(c.createElement)(s.CardBody,{style:{paddingLeft:0,marginBottom:0},className:v},Object(c.createElement)(s.CardMedia,{isBorderless:!0},Object(c.createElement)("img",{src:i,alt:p})),Object(c.createElement)("div",{className:"woocommerce-task-payment__description"},S&&Object(c.createElement)(me.RecommendedRibbon,{isLocalPartner:j}),Object(c.createElement)(C.Text,{as:"h3",className:"woocommerce-task-payment__title"},p,b&&g&&!!d.length&&Object(c.createElement)(me.SetupRequired,null)),Object(c.createElement)("div",{className:"woocommerce-task-payment__content"},r)),Object(c.createElement)("div",{className:"woocommerce-task-payment__footer"},Object(c.createElement)(de.a,{manageUrl:O,id:l,hasSetup:E,needsSetup:g,isEnabled:_,isInstalled:b,hasPlugins:Boolean(d.length),isRecommended:e,isLoading:u,markConfigured:t,onSetUp:()=>Object(m.recordEvent)("tasklist_payment_setup",{selected:l})}))),Object(c.createElement)(s.CardDivider,null))},ue=({heading:e,markConfigured:t,recommendation:o,paymentGateways:n})=>Object(c.createElement)(s.Card,null,Object(c.createElement)(s.CardHeader,{as:"h2"},e),n.map(e=>{const{id:n}=e;return Object(c.createElement)(pe,{key:n,isRecommended:o===n,markConfigured:t,paymentGateway:e})})),_e=()=>{const e=le()("woocommerce-task-payment","woocommerce-task-card");return Object(c.createElement)(c.Fragment,null,Object(c.createElement)(s.CardBody,{style:{paddingLeft:0,marginBottom:0},className:e},Object(c.createElement)(s.CardMedia,{isBorderless:!0},Object(c.createElement)("span",{className:"is-placeholder"})),Object(c.createElement)("div",{className:"woocommerce-task-payment__description"},Object(c.createElement)(C.Text,{as:"h3",className:"woocommerce-task-payment__title"},Object(c.createElement)("span",{className:"is-placeholder"})),Object(c.createElement)("div",{className:"woocommerce-task-payment__content"},Object(c.createElement)("span",{className:"is-placeholder"}))),Object(c.createElement)("div",{className:"woocommerce-task-payment__footer"},Object(c.createElement)("span",{className:"is-placeholder"}))),Object(c.createElement)(s.CardDivider,null))},be=()=>Object(c.createElement)(s.Card,{"aria-hidden":"true",className:"is-loading woocommerce-payment-gateway-suggestions-list-placeholder"},Object(c.createElement)(s.CardHeader,{as:"h2"},Object(c.createElement)("span",{className:"is-placeholder"})),Object(c.createElement)(_e,null),Object(c.createElement)(_e,null),Object(c.createElement)(_e,null)),ge=({markConfigured:e,paymentGateway:t})=>{var o;const{id:a,connectionUrl:l,setupHelpText:p,settingsUrl:u,title:_,requiredSettings:b}=t,{createNotice:g}=Object(i.useDispatch)("core/notices"),{updatePaymentGateway:O}=Object(i.useDispatch)(r.PAYMENT_GATEWAYS_STORE_NAME),j=Object(C.useSlot)("woocommerce_payment_gateway_configure_"+a),w=Boolean(null==j||null===(o=j.fills)||void 0===o?void 0:o.length),{isUpdating:y}=Object(i.useSelect)(e=>{const{isPaymentGatewayUpdating:t}=e(r.PAYMENT_GATEWAYS_STORE_NAME);return{isUpdating:t()}}),k=t=>{O(a,{enabled:!0,settings:t}).then(t=>{t&&t.id===a&&(e(a),g("success",Object(n.sprintf)(Object(n.__)("%s configured successfully",'woocommerce'),_)))}).catch(()=>{g("error",Object(n.__)("There was a problem saving your payment settings",'woocommerce'))})},E=p&&Object(c.createElement)("p",{dangerouslySetInnerHTML:Object(h.a)(p)}),S=Object(c.createElement)(d.DynamicForm,{fields:b,isBusy:y,onSubmit:k,submitLabel:Object(n.__)("Proceed",'woocommerce'),validate:e=>((e,t)=>{const o={},c=e=>t.find(t=>t.id===e);for(const[t,n]of Object.entries(e)){const e=c(t),s=e.label.replace(/([A-Z][a-z]+)/g,e=>e.toLowerCase());n||"checkbox"===e.type||(o[t]="Please enter your "+s)}return o})(e,b)});return w?Object(c.createElement)(me.WooPaymentGatewayConfigure.Slot,{fillProps:{defaultForm:S,defaultSubmit:k,defaultFields:b,markConfigured:()=>e(a),paymentGateway:t},id:a}):l?Object(c.createElement)(c.Fragment,null,E,Object(c.createElement)(s.Button,{isPrimary:!0,onClick:()=>Object(m.recordEvent)("tasklist_payment_connect_start",{payment_method:a}),href:l},Object(n.__)("Connect",'woocommerce'))):b.length?Object(c.createElement)(c.Fragment,null,E,S):Object(c.createElement)(c.Fragment,null,E||Object(c.createElement)("p",null,Object(n.__)("You can manage this payment gateway's settings by clicking the button below",'woocommerce')),Object(c.createElement)(s.Button,{isPrimary:!0,href:u},Object(n.__)("Set up",'woocommerce')))};o(597);const he=({markConfigured:e,paymentGateway:t})=>{var o;const{id:a,plugins:l=[],title:p,postInstallScripts:u,installed:_}=t,g=Object(C.useSlot)("woocommerce_payment_gateway_setup_"+a),h=Boolean(null==g||null===(o=g.fills)||void 0===o?void 0:o.length),[O,j]=Object(c.useState)(!1);Object(c.useEffect)(()=>{Object(m.recordEvent)("payments_task_stepper_view",{payment_method:a})},[]);const{invalidateResolutionForStoreSelector:w}=Object(i.useDispatch)(r.PAYMENT_GATEWAYS_STORE_NAME),{isOptionUpdating:y,isPaymentGatewayResolving:k,needsPluginInstall:E}=Object(i.useSelect)(e=>{const{isOptionsUpdating:t}=e(r.OPTIONS_STORE_NAME),{isResolving:o}=e(r.PAYMENT_GATEWAYS_STORE_NAME),c=e(r.PLUGINS_STORE_NAME).getActivePlugins(),n=l.filter(e=>!c.includes(e));return{isOptionUpdating:t(),isPaymentGatewayResolving:o("getPaymentGateways"),needsPluginInstall:!!n.length}});Object(c.useEffect)(()=>{if(!E)if(u&&u.length){const e=u.map(e=>Object(b.d)(e));Promise.all(e).then(()=>{j(!0)})}else j(!0)},[u,E]);const S=Object(c.useMemo)(()=>l&&l.length?{key:"install",label:Object(n.sprintf)(Object(n.__)("Install %s",'woocommerce'),p),content:Object(c.createElement)(d.Plugins,{onComplete:(e,t)=>{Object(T.a)(t),w("getPaymentGateways"),Object(m.recordEvent)("tasklist_payment_install_method",{plugins:l})},onError:(e,t)=>Object(T.a)(t),autoInstall:!0,pluginSlugs:l})}:null,[]),v=Object(c.useMemo)(()=>({key:"configure",label:Object(n.sprintf)(Object(n.__)("Configure your %(title)s account",'woocommerce'),{title:p}),content:_?Object(c.createElement)(ge,{markConfigured:e,paymentGateway:t}):null}),[_]),f=E||y||k||!O,N=Object(c.createElement)(d.Stepper,{isVertical:!0,isPending:f,currentStep:E?"install":"configure",steps:[S,v].filter(Boolean)});return Object(c.createElement)(s.Card,{className:"woocommerce-task-payment-method woocommerce-task-card"},Object(c.createElement)(s.CardBody,null,h?Object(c.createElement)(me.WooPaymentGatewaySetup.Slot,{fillProps:{defaultStepper:N,defaultInstallStep:S,defaultConfigureStep:v,markConfigured:()=>e(a),paymentGateway:t},id:a}):N))},Oe=()=>{const e=le()("is-loading","woocommerce-task-payment-method","woocommerce-task-card");return Object(c.createElement)(s.Card,{"aria-hidden":"true",className:e},Object(c.createElement)(s.CardBody,null,Object(c.createElement)(d.Stepper,{isVertical:!0,currentStep:"none",steps:[{key:"first",label:""},{key:"second",label:""}]})))};var je=o(539),we=o(88);const ye={account_name:"",account_number:"",bank_name:"",sort_code:"",iban:"",bic:""};Object(we.registerPlugin)("wc-admin-payment-gateway-setup-bacs",{render:()=>{const e=Object(i.useSelect)(e=>e(r.OPTIONS_STORE_NAME).isOptionsUpdating()),{createNotice:t}=Object(i.useDispatch)("core/notices"),{updateOptions:o}=Object(i.useDispatch)(r.OPTIONS_STORE_NAME),a=e=>{const t={};return e.account_number||e.iban||(t.account_number=t.iban=Object(n.__)("Please enter an account number or IBAN",'woocommerce')),t};return Object(c.createElement)(c.Fragment,null,Object(c.createElement)(me.WooPaymentGatewaySetup,{id:"bacs"},({markConfigured:i})=>Object(c.createElement)(d.Form,{initialValues:ye,onSubmit:e=>(async(e,c)=>{if((await o({woocommerce_bacs_settings:{enabled:"yes"},woocommerce_bacs_accounts:[e]})).success)return c(),void t("success",Object(n.__)("Direct bank transfer details added successfully",'woocommerce'));t("error",Object(n.__)("There was a problem saving your payment settings",'woocommerce'))})(e,i),validate:a},({getInputProps:t,handleSubmit:o})=>Object(c.createElement)(c.Fragment,null,Object(c.createElement)(d.H,null,Object(n.__)("Add your bank details",'woocommerce')),Object(c.createElement)("p",null,Object(n.__)("These details are required to receive payments via bank transfer",'woocommerce')),Object(c.createElement)("div",{className:"woocommerce-task-payment-method__fields"},Object(c.createElement)(d.TextControl,V()({label:Object(n.__)("Account name",'woocommerce'),required:!0},t("account_name"))),Object(c.createElement)(d.TextControl,V()({label:Object(n.__)("Account number",'woocommerce'),required:!0},t("account_number"))),Object(c.createElement)(d.TextControl,V()({label:Object(n.__)("Bank name",'woocommerce'),required:!0},t("bank_name"))),Object(c.createElement)(d.TextControl,V()({label:Object(n.__)("Sort code",'woocommerce'),required:!0},t("sort_code"))),Object(c.createElement)(d.TextControl,V()({label:Object(n.__)("IBAN",'woocommerce'),required:!0},t("iban"))),Object(c.createElement)(d.TextControl,V()({label:Object(n.__)("BIC / Swift",'woocommerce'),required:!0},t("bic")))),Object(c.createElement)(s.Button,{isPrimary:!0,isBusy:e,onClick:o},Object(n.__)("Save",'woocommerce'))))))},scope:'woocommerce'});const ke=({query:e})=>{const{invalidateResolutionForStoreSelector:t}=Object(i.useDispatch)(r.ONBOARDING_STORE_NAME),{updatePaymentGateway:o}=Object(i.useDispatch)(r.PAYMENT_GATEWAYS_STORE_NAME),{getPaymentGateway:s,paymentGatewaySuggestions:a,installedPaymentGateways:l,isResolving:d}=Object(i.useSelect)(e=>({getPaymentGateway:e(r.PAYMENT_GATEWAYS_STORE_NAME).getPaymentGateway,getOption:e(r.OPTIONS_STORE_NAME).getOption,installedPaymentGateways:e(r.PAYMENT_GATEWAYS_STORE_NAME).getPaymentGateways(),isResolving:e(r.ONBOARDING_STORE_NAME).isResolving("getPaymentGatewaySuggestions"),paymentGatewaySuggestions:e(r.ONBOARDING_STORE_NAME).getPaymentGatewaySuggestions()}),[]),p=Object(c.useMemo)(()=>{const e=l.reduce((e,t)=>(e[t.id]=t,e),{});return a.reduce((t,o)=>{const{id:c}=o,n=e[o.id]?e[c]:{},s={installed:!!e[c],postInstallScripts:n.post_install_scripts,enabled:n.enabled||!1,needsSetup:n.needs_setup,settingsUrl:n.settings_url,connectionUrl:n.connection_url,setupHelpText:n.setup_help_text,title:n.title,requiredSettings:n.required_settings_keys?n.required_settings_keys.map(e=>n.settings[e]).filter(Boolean):[],...o};return t.set(c,s),t},new Map)},[l,a]);Object(c.useEffect)(()=>{p.size&&Object(m.recordEvent)("tasklist_payments_options",{options:Array.from(p.values()).map(e=>e.id)})},[p]);const u=Object(c.useCallback)(async(e,c={})=>{if(!p.get(e))throw`Payment gateway ${e} not found in available gateways list`;(e=>{if(!e)return;const c=s(e);c&&!c.enabled&&o(e,{enabled:!0}).then(()=>{t("getTasksStatus")})})(e),Object(m.recordEvent)("tasklist_payment_connect_method",{payment_method:e}),Object(k.getHistory)().push(Object(k.getNewPath)({...c},"/",{}))},[p]),_=Object(c.useMemo)(()=>Array.from(p.values()).filter(e=>e.recommendation_priority).sort((e,t)=>e.recommendation_priority-t.recommendation_priority).map(e=>e.id).shift(),[p]),b=Object(c.useMemo)(()=>{if(!e.id||d||!p.size)return null;const t=p.get(e.id);if(!t)throw`Current gateway ${e.id} not found in available gateways list`;return t},[d,e,p]),[g,h,O]=Object(c.useMemo)(()=>Array.from(p.values()).reduce((e,t)=>{const[o,c,n]=e;return"woocommerce_payments"!==t.id||t.installed&&!t.needsSetup?t.enabled?c.push(t):n.push(t):o.push(t),e},[[],[],[]]),[p]);return e.id&&!b?Object(c.createElement)(Oe,null):b?Object(c.createElement)(he,{paymentGateway:b,markConfigured:u}):Object(c.createElement)("div",{className:"woocommerce-task-payments"},!p.size&&Object(c.createElement)(be,null),!!g.length&&Object(c.createElement)(je.a,{paymentGateway:g[0]}),!!h.length&&Object(c.createElement)(ue,{heading:Object(n.__)("Enabled payment gateways",'woocommerce'),recommendation:_,paymentGateways:h}),!!O.length&&Object(c.createElement)(ue,{heading:Object(n.__)("Additional payment gateways",'woocommerce'),recommendation:_,paymentGateways:O,markConfigured:u}))};var Ee=o(526),Se=o(543);function ve(e,t,o,c){Object(m.recordEvent)("task_view",{task_name:e,wcs_installed:c.includes("woocommerce-services"),wcs_active:o.includes("woocommerce-services"),jetpack_installed:c.includes("jetpack"),jetpack_active:o.includes("jetpack"),jetpack_connected:t})}function fe(e,t){if(e.completed||t.completed)return e.completed?1:-1;const o=e.level||3,c=t.level||3;return o===c?0:o>c?1:-1}o(598);function Ce(e,t,o){return[...new Set([...e,...t])].filter(e=>!o.includes(e))}var Te=({query:e,name:t,eventName:o,isComplete:a,dismissedTasks:l,remindMeLaterTasks:p,tasks:u,trackedCompletedTasks:_,title:b,collapsible:g=!1,onComplete:h,onHide:O,expandingItems:j=!1})=>{const{createNotice:w}=Object(i.useDispatch)("core/notices"),{updateOptions:y}=Object(i.useDispatch)(r.OPTIONS_STORE_NAME),{profileItems:E}=Object(i.useSelect)(e=>{const{getProfileItems:t}=e(r.ONBOARDING_STORE_NAME);return{profileItems:t()}}),S=Object(c.useRef)(e);Object(c.useEffect)(()=>{R()},[]),Object(c.useEffect)(()=>{const{task:t}=S.current,{task:o}=e;t!==o&&(window.document.documentElement.scrollTop=0,S.current=e),x(),P()},[e]);const v=Date.now(),f=u.filter(e=>e.visible&&!l.includes(e.key)&&(!p[e.key]||p[e.key]e.completed).map(e=>e.key),N=u.filter(e=>e.visible&&!e.completed&&!l.includes(e.key)),x=()=>{const e=`woocommerce_${t}_complete`,o=a?{[e]:"no"}:{[e]:"yes"};(!N.length&&!a||N.length&&a)&&(y({...o}),"function"==typeof h&&h())},P=()=>{const e=function(e,t){if(!t)return[];return e.filter(e=>t.includes(e))}(T,_),t=(o=e,c=_,f.filter(e=>c.includes(e.key)&&!o.includes(e.key)).map(e=>e.key));var o,c;(function(e,t,o){if(t.length>0)return!0;if(0===o.length)return!1;return!o.every(t=>e.indexOf(t)>=0)})(e,t,T)&&y({woocommerce_task_list_tracked_completed_tasks:Ce(T,_,t)})},A=e=>{const t=l.filter(t=>t!==e);y({woocommerce_task_list_dismissed_tasks:t}),Object(m.recordEvent)(o+"_undo_dismiss_task",{task_name:e})},I=e=>{const{[e]:t,...c}=p;y({woocommerce_task_list_remind_me_later_tasks:c}),Object(m.recordEvent)(o+"_undo_remindmelater_task",{task_name:e})},R=()=>{e.task||Object(m.recordEvent)(o+"_view",{number_tasks:f.length,store_connected:E.wccom_connected})},M=f.map(e=>(e.onClick||(e.onClick=t=>{if(Object(m.recordEvent)(o+"_click",{task_name:e.key}),"A"===t.target.nodeName)return!1;Object(k.updateQueryString)({task:e.key})}),e));if(!M.length)return Object(c.createElement)("div",{className:"woocommerce-task-dashboard__container"});const B=Object(n.sprintf)(Object(n._n)("Show %i more task.","Show %i more tasks.",M.length-2,'woocommerce'),M.length-2),G=Object(n.__)("Show less",'woocommerce'),L=g?C.CollapsibleList:C.List,F=g?{collapseLabel:G,expandLabel:B,show:2,onCollapse:()=>Object(m.recordEvent)(o+"_collapse"),onExpand:()=>Object(m.recordEvent)(o+"_expand")}:{};return Object(c.createElement)(c.Fragment,null,Object(c.createElement)("div",{className:"woocommerce-task-dashboard__container"},Object(c.createElement)(s.Card,{size:"large",className:"woocommerce-task-card woocommerce-homescreen-card"},Object(c.createElement)(s.CardHeader,{size:"medium"},Object(c.createElement)("div",{className:"wooocommerce-task-card__header"},Object(c.createElement)(C.Text,{size:"20",lineHeight:"28px",variant:"title.small"},b),Object(c.createElement)(d.Badge,{count:N.length})),Object(c.createElement)("div",{className:"woocommerce-card__menu woocommerce-card__header-item"},Object(c.createElement)(d.EllipsisMenu,{label:Object(n.__)("Task List Options",'woocommerce'),renderContent:()=>Object(c.createElement)("div",{className:"woocommerce-task-card__section-controls"},Object(c.createElement)(s.Button,{onClick:()=>(e=>{const c={[`woocommerce_${t}_hidden`]:"yes"};Object(m.recordEvent)(o+"_completed",{action:e,completed_task_count:T.length,incomplete_task_count:N.length}),y({...c}),"function"==typeof O&&O()})("remove_card")},Object(n.__)("Hide this",'woocommerce')))}))),Object(c.createElement)(L,V()({animation:"custom"},F),M.map(e=>Object(c.createElement)(C.TaskItem,{key:e.key,title:e.title,completed:e.completed,content:e.content,expandable:j&&e.expandable,expanded:j&&e.expanded,onClick:e.onClick,onDismiss:e.isDismissable?()=>(({key:e,onDismiss:t})=>{w("success",Object(n.__)("Task dismissed"),{actions:[{label:Object(n.__)("Undo",'woocommerce'),onClick:()=>A(e)}]}),Object(m.recordEvent)(o+"_dismiss_task",{task_name:e}),y({woocommerce_task_list_dismissed_tasks:[...l,e]}),t&&t()})(e):void 0,onSnooze:e.allowRemindMeLater?()=>(({key:e,onDismiss:t})=>{w("success",Object(n.__)("Task postponed until tomorrow",'woocommerce'),{actions:[{label:Object(n.__)("Undo",'woocommerce'),onClick:()=>I(e)}]}),Object(m.recordEvent)(o+"_remindmelater_task",{task_name:e});const c=Date.now()+864e5;y({woocommerce_task_list_remind_me_later_tasks:{...p,[e]:c}}),t&&t()})(e):void 0,time:e.time,level:e.level,action:e.action,actionLabel:e.actionLabel,additionalInfo:e.additionalInfo,showActionButton:e.showActionButton,onExpand:e.onExpand,onCollapse:e.onCollapse}))))))},Ne=o(253);const xe=({taskContainer:e,query:t})=>{const o=Object(c.useRef)(),{isJetpackConnected:n,activePlugins:s,installedPlugins:a}=Object(i.useSelect)(e=>{const{getActivePlugins:t,getInstalledPlugins:o,isJetpackConnected:c}=e(r.PLUGINS_STORE_NAME);return{activePlugins:t(),isJetpackConnected:c(),installedPlugins:o()}});return Object(c.useEffect)(()=>{const{task:e}=t;o.current!==e&&(window.document.documentElement.scrollTop=0),o.current=e,(()=>{const{task:e}=t;e&&ve(e,n,s,a)})()},[t]),e&&t.task?Object(c.createElement)("div",{className:"woocommerce-task-dashboard__container"},Object(c.cloneElement)(e,{query:t})):null};var Pe=o(540);const Ae=[],Ie=e=>{const{getFreeExtensions:t,getProductTypes:o,getProfileItems:c,getTasksStatus:n,hasFinishedResolution:s}=e(r.ONBOARDING_STORE_NAME),{getSettings:a}=e(r.SETTINGS_STORE_NAME),{getOption:i,hasFinishedResolution:l}=e(r.OPTIONS_STORE_NAME),{getActivePlugins:m,getInstalledPlugins:d,isJetpackConnected:p}=e(r.PLUGINS_STORE_NAME),u=c(),_=i("woocommerce_task_list_tracked_completed_tasks")||Ae,b=i("woocommerce_task_list_tracked_completed_actions")||Ae,{general:h={}}=a("general"),O=Object(g.b)(h.woocommerce_default_country),{woocommerce_store_address:j,woocommerce_default_country:w,woocommerce_store_postcode:y}=h,k=Boolean(j&&w&&y),E=m(),S=d(),v=n(),f=o();return{activePlugins:E,countryCode:O,dismissedTasks:i("woocommerce_task_list_dismissed_tasks"),freeExtensions:t(),remindMeLaterTasks:i("woocommerce_task_list_remind_me_later_tasks"),isExtendedTaskListComplete:"yes"===i("woocommerce_extended_task_list_complete"),isExtendedTaskListHidden:"yes"===i("woocommerce_extended_task_list_hidden"),isJetpackConnected:p(),isSetupTaskListHidden:"yes"===i("woocommerce_task_list_hidden"),isTaskListComplete:"yes"===i("woocommerce_task_list_complete"),installedPlugins:S,productTypes:f,trackedCompletedActions:b,onboardingStatus:v,profileItems:u,trackedCompletedTasks:_,hasCompleteAddress:k,isResolving:!(l("getOption",["woocommerce_task_list_complete"])&&l("getOption",["woocommerce_task_list_hidden"])&&l("getOption",["woocommerce_extended_task_list_complete"])&&l("getOption",["woocommerce_extended_task_list_hidden"])&&l("getOption",["woocommerce_task_list_remind_me_later_tasks"])&&l("getOption",["woocommerce_task_list_tracked_completed_tasks"])&&l("getOption",["woocommerce_task_list_dismissed_tasks"])&&s("getProductTypes"))}};t.default=({userPreferences:e,query:t})=>{const{createNotice:o}=Object(i.useDispatch)("core/notices"),{updateOptions:p}=Object(i.useDispatch)(r.OPTIONS_STORE_NAME),{invalidateResolutionForStoreSelector:u}=Object(i.useDispatch)(r.ONBOARDING_STORE_NAME),{installAndActivatePlugins:_}=Object(i.useDispatch)(r.PLUGINS_STORE_NAME),{trackedCompletedTasks:b,activePlugins:h,countryCode:O,freeExtensions:j,installedPlugins:E,productTypes:S,isJetpackConnected:v,onboardingStatus:C,profileItems:T,isSetupTaskListHidden:N,dismissedTasks:x,remindMeLaterTasks:P,isTaskListComplete:R,isExtendedTaskListHidden:M,isExtendedTaskListComplete:B,hasCompleteAddress:G,trackedCompletedActions:L,isResolving:F}=Object(i.useSelect)(Ie),[U,D]=Object(c.useState)(!1),[z,q]=Object(l.useExperiment)("woocommerce_tasklist_progression");Object(c.useEffect)(()=>{document.body.classList.add("woocommerce-onboarding"),document.body.classList.add("woocommerce-task-dashboard__body"),u("getProductTypes")},[]);const V=()=>{U||Object(m.recordEvent)("tasklist_purchase_extensions"),D(!U)},J=e=>Object(c.createElement)(Te,{name:"extended_task_list",eventName:"extended_tasklist",collapsible:!0,dismissedTasks:x||[],remindMeLaterTasks:P||[],isComplete:B,query:t,tasks:e,title:Object(n.__)("Things to do next",'woocommerce'),trackedCompletedTasks:b||[]}),W=function({activePlugins:e,countryCode:t,createNotice:o,freeExtensions:s,installAndActivatePlugins:a,installedPlugins:i,isJetpackConnected:r,onboardingStatus:l,profileItems:m,query:d,toggleCartModal:p,onTaskSelect:u,hasCompleteAddress:_,trackedCompletedActions:b,productTypes:h}){const{hasPaymentGateway:O,hasPhysicalProducts:j,hasProducts:w,isAppearanceComplete:E,isTaxComplete:S,shippingZonesCount:v,wcPayIsConnected:C}={hasPaymentGateway:!1,hasPhysicalProducts:!1,hasProducts:!1,isAppearanceComplete:!1,isTaxComplete:!1,shippingZonesCount:0,wcPayIsConnected:!1,...l},T=Object(g.a)(h,m,i),{products:N,remainingProducts:x,uniqueItemsList:P}=T,R=-1!==i.indexOf("woocommerce-payments"),M=-1!==e.indexOf("woocommerce-services"),{completed:B,product_types:G,business_extensions:L}=m,F=(L||[]).includes("woocommerce-payments");let U,D=Object(n.__)("Add paid extensions to my store",'woocommerce');if(1===P.length){var z;const{name:e}=P[0],t=Object(n.__)("Add %s to my store",'woocommerce');D=Object(n.sprintf)(t,e),U=null===(z=N.find(({label:t})=>t===e))||void 0===z?void 0:z.description}else{const e=P.map(({name:e})=>e),t=e.pop();let o=e.join(", ");e.length>1&&(o+=","),U=Object(n.sprintf)(Object(n.__)("Good choice! You chose to add %1$s and %2$s to your store.",'woocommerce'),o,t)}const{automatedTaxSupportedCountries:q=[],taxJarActivated:V}=l,J=!V&&q.includes(t),W=_&&M&&J;let Y=Object(n.__)("Let's go",'woocommerce'),Z=Object(n.__)("Set your store location and configure tax rate settings.",'woocommerce');W&&(Y=Object(n.__)("Yes please",'woocommerce'),Z=Object(n.__)("Good news! WooCommerce Services and Jetpack can automate your sales tax calculations for you.",'woocommerce'));const[$,Q]=A(s,e,i),K=[{key:"store_details",title:Object(n.__)("Store details",'woocommerce'),content:Object(n.__)("Your store address is required to set the origin country for shipping, currencies, and payment options.",'woocommerce'),container:null,action:Object(n.__)("Let's go",'woocommerce'),onClick:()=>{u("store_details"),Object(k.getHistory)().push(Object(k.getNewPath)({},"/setup-wizard",{}))},completed:B,visible:!0,time:Object(n.__)("4 minutes",'woocommerce'),type:"setup"},{key:"purchase",title:D,content:U,container:null,action:Object(n.__)("Purchase & install now",'woocommerce'),onClick:()=>(u("purchase"),x.length?p():null),visible:N.length,completed:N.length&&!x.length,time:Object(n.__)("2 minutes",'woocommerce'),isDismissable:!0,type:"setup"},{key:"products",title:Object(n.__)("Add my products",'woocommerce'),content:Object(n.__)("Start by adding the first product to your store. You can add your products manually, via CSV, or import them from another service.",'woocommerce'),container:Object(c.createElement)(H,null),onClick:()=>{u("products"),Object(k.updateQueryString)({task:"products"})},completed:w,visible:!0,time:Object(n.__)("1 minute per product",'woocommerce'),type:"setup"},{key:"woocommerce-payments",title:Object(n.__)("Get paid with WooCommerce Payments",'woocommerce'),content:Object(n.__)("You're only one step away from getting paid. Verify your business details to start managing transactions with WooCommerce Payments.",'woocommerce'),action:Object(n.__)("Finish setup","woocommmerce-admin"),expanded:!0,container:Object(c.createElement)(c.Fragment,null),completed:C,onClick:async t=>{if("A"===t.target.nodeName)return!1;await new Promise((t,c)=>(ve("wcpay",r,e,i),u("woocommerce-payments"),Object(je.b)(c,o,a)))},visible:F&&R&&Object(je.c)(t),additionalInfo:Object(n.__)('By setting up, you are agreeing to the Terms of Service','woocommerce'),time:Object(n.__)("2 minutes",'woocommerce'),type:"setup"},{key:"payments",title:Object(n.__)("Set up payments",'woocommerce'),content:Object(n.__)("Choose payment providers and enable payment methods at checkout.",'woocommerce'),container:Object(c.createElement)(ke,{query:d}),completed:O,onClick:()=>{u("payments"),Object(k.updateQueryString)({task:"payments"})},visible:window.wcAdminFeatures["payment-gateway-suggestions"]&&(!R||!F||!Object(je.c)(t)),time:Object(n.__)("2 minutes",'woocommerce'),type:"setup"},{key:"tax",title:Object(n.__)("Set up tax",'woocommerce'),content:Z,container:Object(c.createElement)(ie,null),action:Y,onClick:(e,t={})=>{const{isExpanded:o}=t;u("tax"),Object(k.updateQueryString)({task:"tax",auto:W&&o})},completed:S,visible:!0,time:Object(n.__)("1 minute",'woocommerce'),type:"setup"},{key:"shipping",title:Object(n.__)("Set up shipping",'woocommerce'),content:Object(n.__)("Set your store location and where you'll ship to.",'woocommerce'),container:Object(c.createElement)(se,null),action:Object(n.__)("Let's go",'woocommerce'),onClick:()=>{v>0?window.location=Object(Se.b)({type:"wc-settings",tab:"shipping"}).href:(u("shipping"),Object(k.updateQueryString)({task:"shipping"}))},completed:v>0,visible:G&&G.includes("physical")||j,time:Object(n.__)("1 minute",'woocommerce'),type:"setup"},{key:"marketing",title:Object(n.__)("Set up marketing tools",'woocommerce'),content:Object(n.__)("Add recommended marketing tools to reach new customers and grow your business",'woocommerce'),container:Object(c.createElement)(I,{trackedCompletedActions:b}),onClick:()=>{u("marketing"),Object(k.updateQueryString)({task:"marketing"})},completed:!!$.length&&b.includes("marketing")||!Q.length,visible:window.wcAdminFeatures&&window.wcAdminFeatures["remote-free-extensions"]&&(!!Q.length||!!$.length),time:Object(n.__)("1 minute",'woocommerce'),type:"setup"},{key:"appearance",title:Object(n.__)("Personalize my store",'woocommerce'),content:Object(n.__)("Add your logo, create a homepage, and start designing your store.",'woocommerce'),container:Object(c.createElement)(f,null),action:Object(n.__)("Let's go",'woocommerce'),onClick:()=>{u("appearance"),Object(k.updateQueryString)({task:"appearance"})},completed:E,visible:!0,time:Object(n.__)("2 minutes",'woocommerce'),type:"setup"}],X=Object(y.applyFilters)("woocommerce_admin_onboarding_task_list",K,d);for(const e of X)e.level=e.level?parseInt(e.level,10):3;return Object(Ee.a)(X,"type","extension")}({activePlugins:h,countryCode:O,createNotice:o,freeExtensions:j,installAndActivatePlugins:_,installedPlugins:E,isJetpackConnected:v,onboardingStatus:C,profileItems:T,query:t,toggleCartModal:V,onTaskSelect:t=>{const o=(t=>{const o=e.task_list_tracked_started_tasks;return o&&o[t]?o[t]:0})(t);Object(m.recordEvent)("tasklist_click",{task_name:t}),(e=>!!b&&b.includes(e))(t)||((t,o)=>{const c=e.task_list_tracked_started_tasks||{};e.updateUserPreferences({task_list_tracked_started_tasks:{...c||{},[t]:o}})})(t,o+1)},hasCompleteAddress:G,trackedCompletedActions:L,productTypes:S}),{extension:Y,setup:Z}=W,{task:$}=t,Q=Array.isArray(Y)&&Y.sort(fe),K=(e=>{const{task:o}=t,c=e.find(e=>e.key===o);return c||null})([...Q||[],...Z||[]]);if($&&!K)return null;if(K)return Object(c.createElement)(xe,{taskContainer:K.container,query:t});if(F)return Object(c.createElement)(Pe.a,null);const X="extended_task_list"===window.location.hash.substr(1);return Object(c.createElement)(c.Fragment,null,Z&&(!N||$)&&(z?Object(c.createElement)(Pe.a,null):Object(c.createElement)(Te,{name:"task_list",eventName:"tasklist",expandingItems:"treatment"===(null==q?void 0:q.variationName),dismissedTasks:x||[],remindMeLaterTasks:P||[],isComplete:R,query:t,tasks:Z,title:Object(n.__)("Get ready to start selling",'woocommerce'),trackedCompletedTasks:b||[],onComplete:()=>p({woocommerce_default_homepage_layout:"two_columns"}),onHide:()=>p({woocommerce_default_homepage_layout:"two_columns"})})),Q&&Object(c.createElement)(Ne.a,null,Object(c.createElement)(s.MenuGroup,{className:"woocommerce-layout__homescreen-display-options",label:Object(n.__)("Display",'woocommerce')},Object(c.createElement)(s.MenuItem,{className:"woocommerce-layout__homescreen-extension-tasklist-toggle",icon:!M&&a.a,isSelected:!M,role:"menuitemcheckbox",onClick:()=>{const e=!M;Object(m.recordEvent)(e?"extended_tasklist_hide":"extended_tasklist_show"),p({woocommerce_extended_task_list_hidden:e?"yes":"no"})}},Object(n.__)("Show things to do next",'woocommerce')))),Q&&!M&&(X?Object(c.createElement)(d.ScrollTo,{offset:"-20"},J(Q)):J(Q)),U&&Object(c.createElement)(w,{onClose:()=>V(),onClickPurchaseLater:()=>V()}))}}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/chunks/wcpay-usage-modal.js b/packages/woocommerce-admin/dist/chunks/wcpay-usage-modal.js new file mode 100644 index 0000000..52c390f --- /dev/null +++ b/packages/woocommerce-admin/dist/chunks/wcpay-usage-modal.js @@ -0,0 +1 @@ +(window.__wcAdmin_webpackJsonp=window.__wcAdmin_webpackJsonp||[]).push([[53],{514:function(e,t,o){"use strict";var s=o(0),i=o(2),n=o(14),c=o(7),a=o(18),r=o.n(a),m=o(3),l=o(21),u=o(11),d=o(122);class p extends s.Component{constructor(e){super(e),this.state={isLoadingScripts:!1,isRequestStarted:!1}}async componentDidUpdate(e,t){const{hasErrors:o,isRequesting:s,onClose:n,onContinue:c,createNotice:a}=this.props,{isLoadingScripts:r,isRequestStarted:m}=this.state;if(!m)return;const l=!s&&!r&&(e.isRequesting||t.isLoadingScripts)&&!o,u=!s&&e.isRequesting&&o;l&&(n(),c()),u&&(a("error",Object(i.__)("There was a problem updating your preferences",'woocommerce')),n())}updateTracking({allowTracking:e}){const{updateOptions:t}=this.props;e&&"function"==typeof window.wcTracks.enable?(this.setState({isLoadingScripts:!0}),window.wcTracks.enable(()=>{this._isMounted&&(Object(d.initializeExPlat)(),this.setState({isLoadingScripts:!1}))})):e||(window.wcTracks.isEnabled=!1);const o=e?"yes":"no";this.setState({isRequestStarted:!0}),t({woocommerce_allow_tracking:o})}componentDidMount(){this._isMounted=!0}componentWillUnmount(){this._isMounted=!1}render(){const{allowTracking:e,isResolving:t,onClose:o,onContinue:n}=this.props;if(t)return null;if(e)return o(),n(),null;const{isRequesting:c,title:a=Object(i.__)("Build a better WooCommerce",'woocommerce'),message:u=r()({mixedString:Object(i.__)("Get improved features and faster fixes by sharing non-sensitive data via {{link}}usage tracking{{/link}} that shows us how WooCommerce is used. No personal data is tracked or stored.",'woocommerce'),components:{link:Object(s.createElement)(l.Link,{href:"https://woocommerce.com/usage-tracking?utm_medium=product",target:"_blank",type:"external"})}}),dismissActionText:d=Object(i.__)("No thanks",'woocommerce'),acceptActionText:p=Object(i.__)("Yes, count me in!",'woocommerce')}=this.props,{isRequestStarted:g}=this.state,w=g&&c;return Object(s.createElement)(m.Modal,{title:a,isDismissible:this.props.isDismissible,onRequestClose:()=>this.props.onClose(),className:"woocommerce-usage-modal"},Object(s.createElement)("div",{className:"woocommerce-usage-modal__wrapper"},Object(s.createElement)("div",{className:"woocommerce-usage-modal__message"},u),Object(s.createElement)("div",{className:"woocommerce-usage-modal__actions"},Object(s.createElement)(m.Button,{isSecondary:!0,isBusy:w,onClick:()=>this.updateTracking({allowTracking:!1})},d),Object(s.createElement)(m.Button,{isPrimary:!0,isBusy:w,onClick:()=>this.updateTracking({allowTracking:!0})},p))))}}t.a=Object(n.compose)(Object(c.withSelect)(e=>{const{getOption:t,getOptionsUpdatingError:o,isOptionsUpdating:s,hasFinishedResolution:i}=e(u.OPTIONS_STORE_NAME);return{allowTracking:"yes"===t("woocommerce_allow_tracking"),isRequesting:Boolean(s()),isResolving:!i("getOption",["woocommerce_allow_tracking"])||void 0===t("woocommerce_allow_tracking"),hasErrors:Boolean(o())}}),Object(c.withDispatch)(e=>{const{createNotice:t}=e("core/notices"),{updateOptions:o}=e(u.OPTIONS_STORE_NAME);return{createNotice:t,updateOptions:o}}))(p)},519:function(e,t,o){"use strict";o.r(t),o.d(t,"UsageModal",(function(){return l}));var s=o(0),i=o(2),n=o(12),c=o(18),a=o.n(c),r=o(21),m=o(514);const l=()=>{const e="1"===Object(n.getQuery)()["wcpay-connection-success"],[t,o]=Object(s.useState)(e);if(!t)return null;const c=()=>{o(!1),Object(n.updateQueryString)({"wcpay-connection-success":void 0})},l=Object(i.__)("Help us build a better WooCommerce Payments experience",'woocommerce'),u=a()({mixedString:Object(i.__)("By agreeing to share non-sensitive {{link}}usage data{{/link}}, you’ll help us improve features and optimize the WooCommerce Payments experience. You can opt out at any time.",'woocommerce'),components:{link:Object(s.createElement)(r.Link,{href:"https://woocommerce.com/usage-tracking?utm_medium=product",target:"_blank",type:"external"})}});return Object(s.createElement)(m.a,{isDismissible:!1,title:l,message:u,acceptActionText:Object(i.__)("I agree",'woocommerce'),dismissActionText:Object(i.__)("No thanks",'woocommerce'),onContinue:c,onClose:c})};t.default=l}}]); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/components/index.asset.php b/packages/woocommerce-admin/dist/components/index.asset.php new file mode 100644 index 0000000..2e9c818 --- /dev/null +++ b/packages/woocommerce-admin/dist/components/index.asset.php @@ -0,0 +1 @@ + array('lodash', 'moment', 'react', 'react-dom', 'wc-currency', 'wc-date', 'wc-navigation', 'wc-store-data', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-deprecated', 'wp-dom', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-keycodes', 'wp-primitives', 'wp-url', 'wp-viewport'), 'version' => 'a1fceacf677e4fa8bac4dd89559b9947'); \ No newline at end of file diff --git a/packages/woocommerce-admin/dist/components/index.js b/packages/woocommerce-admin/dist/components/index.js new file mode 100644 index 0000000..f3770f9 --- /dev/null +++ b/packages/woocommerce-admin/dist/components/index.js @@ -0,0 +1,2 @@ +/*! For license information please see index.js.LICENSE.txt */ +this.wc=this.wc||{},this.wc.components=function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=461)}([function(e,t){e.exports=window.wp.element},function(e,t,n){e.exports=n(47)()},function(e,t){e.exports=window.wp.i18n},function(e,t){e.exports=window.wp.components},function(e,t){e.exports=window.lodash},function(e,t){e.exports=window.React},function(e,t,n){!function(){"use strict";var t={}.hasOwnProperty;function n(){for(var e=[],r=0;r "+c);if("componentClose"===d.type)throw new Error("Missing opening component token: `"+d.value+"`");if("componentOpen"===d.type){n=t[d.value],s=f;break}m.push(t[d.value])}else m.push(d.value);return n&&(u=function(e,t){var n,r,o=t[e],a=0;for(r=e+1;r=0||(o[n]=e[n]);return o}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";function r(){return(r=Object.assign||function(e){for(var t=1;t1&&"boolean"!=typeof t)throw new a('"allowMissing" argument must be a boolean');var n=S(e),o=n.length>0?n[0]:"",i=D("%"+o+"%",t),c=i.name,l=i.value,u=!1,d=i.alias;d&&(o=d[0],v(n,y([0,1],d)));for(var f=1,p=!0;f=n.length){var w=s(l,m);l=(p=!!w)&&"get"in w&&!("originalValue"in w.get)?w.get:l[m]}else p=g(l,m),l=l[m];p&&!u&&(h[c]=l)}}return l}},function(e,t){e.exports=window.wp.keycodes},function(e,t){e.exports=window.wp.htmlEntities},function(e,t){e.exports=window.ReactDOM},,function(e,t,n){e.exports=n(354)},,function(e,t,n){"use strict";var r=n(5),o=n.n(r);t.a=o.a.createContext(null)},,,function(e,t,n){"use strict";var r=n(57),o=n(95),a=n(197),i=n(198),s=n(351),c=o.apply(i()),l=function(e,t){return c(Object,arguments)};r(l,{getPolyfill:i,implementation:a,shim:s}),e.exports=l},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r="Interact with the calendar and add the check-in date for your trip.",o="Move backward to switch to the previous month.",a="Move forward to switch to the next month.",i="page up and page down keys",s="Home and end keys",c="Escape key",l="Select the date in focus.",u="Move backward (left) and forward (right) by one day.",d="Move backward (up) and forward (down) by one week.",f="Return to the date input field.",p="Press the down arrow key to interact with the calendar and\n select a date. Press the question mark key to get the keyboard shortcuts for changing dates.",h=function(e){var t=e.date;return"Choose "+String(t)+" as your check-in date. It’s available."},m=function(e){var t=e.date;return"Choose "+String(t)+" as your check-out date. It’s available."},b=function(e){return e.date},g=function(e){var t=e.date;return"Not available. "+String(t)},y=function(e){var t=e.date;return"Selected. "+String(t)};t.default={calendarLabel:"Calendar",closeDatePicker:"Close",focusStartDate:r,clearDate:"Clear Date",clearDates:"Clear Dates",jumpToPrevMonth:o,jumpToNextMonth:a,keyboardShortcuts:"Keyboard Shortcuts",showKeyboardShortcutsPanel:"Open the keyboard shortcuts panel.",hideKeyboardShortcutsPanel:"Close the shortcuts panel.",openThisPanel:"Open this panel.",enterKey:"Enter key",leftArrowRightArrow:"Right and left arrow keys",upArrowDownArrow:"up and down arrow keys",pageUpPageDown:i,homeEnd:s,escape:c,questionMark:"Question mark",selectFocusedDate:l,moveFocusByOneDay:u,moveFocusByOneWeek:d,moveFocusByOneMonth:"Switch months.",moveFocustoStartAndEndOfWeek:"Go to the first or last day of a week.",returnFocusToInput:f,keyboardNavigationInstructions:p,chooseAvailableStartDate:h,chooseAvailableEndDate:m,dateIsUnavailable:g,dateIsSelected:y};t.DateRangePickerPhrases={calendarLabel:"Calendar",closeDatePicker:"Close",clearDates:"Clear Dates",focusStartDate:r,jumpToPrevMonth:o,jumpToNextMonth:a,keyboardShortcuts:"Keyboard Shortcuts",showKeyboardShortcutsPanel:"Open the keyboard shortcuts panel.",hideKeyboardShortcutsPanel:"Close the shortcuts panel.",openThisPanel:"Open this panel.",enterKey:"Enter key",leftArrowRightArrow:"Right and left arrow keys",upArrowDownArrow:"up and down arrow keys",pageUpPageDown:i,homeEnd:s,escape:c,questionMark:"Question mark",selectFocusedDate:l,moveFocusByOneDay:u,moveFocusByOneWeek:d,moveFocusByOneMonth:"Switch months.",moveFocustoStartAndEndOfWeek:"Go to the first or last day of a week.",returnFocusToInput:f,keyboardNavigationInstructions:p,chooseAvailableStartDate:h,chooseAvailableEndDate:m,dateIsUnavailable:g,dateIsSelected:y},t.DateRangePickerInputPhrases={focusStartDate:r,clearDates:"Clear Dates",keyboardNavigationInstructions:p},t.SingleDatePickerPhrases={calendarLabel:"Calendar",closeDatePicker:"Close",clearDate:"Clear Date",jumpToPrevMonth:o,jumpToNextMonth:a,keyboardShortcuts:"Keyboard Shortcuts",showKeyboardShortcutsPanel:"Open the keyboard shortcuts panel.",hideKeyboardShortcutsPanel:"Close the shortcuts panel.",openThisPanel:"Open this panel.",enterKey:"Enter key",leftArrowRightArrow:"Right and left arrow keys",upArrowDownArrow:"up and down arrow keys",pageUpPageDown:i,homeEnd:s,escape:c,questionMark:"Question mark",selectFocusedDate:l,moveFocusByOneDay:u,moveFocusByOneWeek:d,moveFocusByOneMonth:"Switch months.",moveFocustoStartAndEndOfWeek:"Go to the first or last day of a week.",returnFocusToInput:f,keyboardNavigationInstructions:p,chooseAvailableDate:b,dateIsUnavailable:g,dateIsSelected:y},t.SingleDatePickerInputPhrases={clearDate:"Clear Date",keyboardNavigationInstructions:p},t.DayPickerPhrases={calendarLabel:"Calendar",jumpToPrevMonth:o,jumpToNextMonth:a,keyboardShortcuts:"Keyboard Shortcuts",showKeyboardShortcutsPanel:"Open the keyboard shortcuts panel.",hideKeyboardShortcutsPanel:"Close the shortcuts panel.",openThisPanel:"Open this panel.",enterKey:"Enter key",leftArrowRightArrow:"Right and left arrow keys",upArrowDownArrow:"up and down arrow keys",pageUpPageDown:i,homeEnd:s,escape:c,questionMark:"Question mark",selectFocusedDate:l,moveFocusByOneDay:u,moveFocusByOneWeek:d,moveFocusByOneMonth:"Switch months.",moveFocustoStartAndEndOfWeek:"Go to the first or last day of a week.",returnFocusToInput:f,chooseAvailableStartDate:h,chooseAvailableEndDate:m,chooseAvailableDate:b,dateIsUnavailable:g,dateIsSelected:y},t.DayPickerKeyboardShortcutsPhrases={keyboardShortcuts:"Keyboard Shortcuts",showKeyboardShortcutsPanel:"Open the keyboard shortcuts panel.",hideKeyboardShortcutsPanel:"Close the shortcuts panel.",openThisPanel:"Open this panel.",enterKey:"Enter key",leftArrowRightArrow:"Right and left arrow keys",upArrowDownArrow:"up and down arrow keys",pageUpPageDown:i,homeEnd:s,escape:c,questionMark:"Question mark",selectFocusedDate:l,moveFocusByOneDay:u,moveFocusByOneWeek:d,moveFocusByOneMonth:"Switch months.",moveFocustoStartAndEndOfWeek:"Go to the first or last day of a week.",returnFocusToInput:f},t.DayPickerNavigationPhrases={jumpToPrevMonth:o,jumpToNextMonth:a},t.CalendarDayPhrases={chooseAvailableDate:b,dateIsUnavailable:g,dateIsSelected:y}},,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e){return Object.keys(e).reduce((function(e,t){return(0,r.default)({},e,function(e,t,n){t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n;return e}({},t,o.default.oneOfType([o.default.string,o.default.func,o.default.node])))}),{})};var r=a(n(36)),o=a(n(1));function a(e){return e&&e.__esModule?e:{default:e}}},,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.withStylesPropTypes=t.css=void 0;var r=Object.assign||function(e){for(var t=1;t1&&void 0!==arguments[1]?arguments[1]:{},n=t.stylesPropName,s=void 0===n?"styles":n,u=t.themePropName,f=void 0===u?"theme":u,g=t.cssPropName,w=void 0===g?"css":g,k=t.flushBefore,S=void 0!==k&&k,D=t.pureComponent,C=void 0!==D&&D,E=void 0,j=void 0,F=void 0,P=void 0,M=v(C);function x(e){return e===l.DIRECTIONS.LTR?d.default.resolveLTR:d.default.resolveRTL}function T(e){return e===l.DIRECTIONS.LTR?F:P}function N(t,n){var r=T(t),o=t===l.DIRECTIONS.LTR?E:j,a=d.default.get();return o&&r===a||(t===l.DIRECTIONS.RTL?(j=e?d.default.createRTL(e):y,P=a,o=j):(E=e?d.default.createLTR(e):y,F=a,o=E)),o}function I(e,t){return{resolveMethod:x(e),styleDef:N(e)}}return function(e){var t=e.displayName||e.name||"Component",n=function(t){function n(e,t){p(this,n);var r=h(this,(n.__proto__||Object.getPrototypeOf(n)).call(this,e,t)),o=r.context[l.CHANNEL]?r.context[l.CHANNEL].getState():O;return r.state=I(o),r}return m(n,t),o(n,[{key:"componentDidMount",value:function(){var e=this;this.context[l.CHANNEL]&&(this.channelUnsubscribe=this.context[l.CHANNEL].subscribe((function(t){e.setState(I(t))})))}},{key:"componentWillUnmount",value:function(){this.channelUnsubscribe&&this.channelUnsubscribe()}},{key:"render",value:function(){var t;S&&d.default.flush();var n=this.state,o=n.resolveMethod,a=n.styleDef;return i.default.createElement(e,r({},this.props,(b(t={},f,d.default.get()),b(t,s,a()),b(t,w,o),t)))}}]),n}(M);return n.WrappedComponent=e,n.displayName="withStyles("+String(t)+")",n.contextTypes=_,e.propTypes&&(n.propTypes=(0,a.default)({},e.propTypes),delete n.propTypes[s],delete n.propTypes[f],delete n.propTypes[w]),e.defaultProps&&(n.defaultProps=(0,a.default)({},e.defaultProps)),(0,c.default)(n,e)}};var a=f(n(36)),i=f(n(5)),s=f(n(1)),c=f(n(118)),l=n(355),u=f(n(356)),d=f(n(199));function f(e){return e&&e.__esModule?e:{default:e}}function p(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function h(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function m(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}function b(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}t.css=d.default.resolveLTR,t.withStylesPropTypes={styles:s.default.object.isRequired,theme:s.default.object.isRequired,css:s.default.func.isRequired};var g={},y=function(){return g};function v(e){if(e){if(!i.default.PureComponent)throw new ReferenceError("withStyles() pureComponent option requires React 15.3.0 or later");return i.default.PureComponent}return i.default.Component}var _=b({},l.CHANNEL,u.default),O=l.DIRECTIONS.LTR},function(e,t){e.exports=window.wp.deprecated},function(e,t,n){(function(t){var n=function(e){return e&&e.Math==Math&&e};e.exports=n("object"==typeof globalThis&&globalThis)||n("object"==typeof window&&window)||n("object"==typeof self&&self)||n("object"==typeof t&&t)||function(){return this}()||Function("return this")()}).call(this,n(78))},function(e,t,n){"use strict";var r=n(401);e.exports=function(e){return"symbol"==typeof e?"Symbol":"bigint"==typeof e?"BigInt":r(e)}},,function(e,t,n){"use strict";var r=n(48);function o(){}function a(){}a.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,a,i){if(i!==r){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:o};return n.PropTypes=n,n}},function(e,t,n){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},,function(e,t,n){"use strict";function r(e){return function(){return e}}var o=function(){};o.thatReturns=r,o.thatReturnsFalse=r(!1),o.thatReturnsTrue=r(!0),o.thatReturnsNull=r(null),o.thatReturnsThis=function(){return this},o.thatReturnsArgument=function(e){return e},e.exports=o},,,function(e,t,n){e.exports=function(){"use strict";var e=Object.hasOwnProperty,t=Object.setPrototypeOf,n=Object.isFrozen,r=Object.getPrototypeOf,o=Object.getOwnPropertyDescriptor,a=Object.freeze,i=Object.seal,s=Object.create,c="undefined"!=typeof Reflect&&Reflect,l=c.apply,u=c.construct;l||(l=function(e,t,n){return e.apply(t,n)}),a||(a=function(e){return e}),i||(i=function(e){return e}),u||(u=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t1?n-1:0),o=1;o/gm),U=i(/^data-[\-\w.\u00B7-\uFFFF]/),B=i(/^aria-[\-\w]+$/),H=i(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),K=i(/^(?:\w+script|data):/i),z=i(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function V(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t0&&void 0!==arguments[0]?arguments[0]:W(),n=function(t){return e(t)};if(n.version="2.2.9",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,o=t.document,i=t.DocumentFragment,s=t.HTMLTemplateElement,c=t.Node,l=t.Element,u=t.NodeFilter,d=t.NamedNodeMap,w=void 0===d?t.NamedNodeMap||t.MozNamedAttrMap:d,Y=t.Text,$=t.Comment,Q=t.DOMParser,Z=t.trustedTypes,X=l.prototype,J=D(X,"cloneNode"),ee=D(X,"nextSibling"),te=D(X,"childNodes"),ne=D(X,"parentNode");if("function"==typeof s){var re=o.createElement("template");re.content&&re.content.ownerDocument&&(o=re.content.ownerDocument)}var oe=G(Z,r),ae=oe&&Re?oe.createHTML(""):"",ie=o,se=ie.implementation,ce=ie.createNodeIterator,le=ie.createDocumentFragment,ue=r.importNode,de={};try{de=S(o).documentMode?o.documentMode:{}}catch(e){}var fe={};n.isSupported="function"==typeof ne&&se&&void 0!==se.createHTMLDocument&&9!==de;var pe=A,he=L,me=U,be=B,ge=K,ye=z,ve=H,_e=null,Oe=k({},[].concat(V(C),V(E),V(j),V(P),V(x))),we=null,ke=k({},[].concat(V(T),V(N),V(I),V(R))),Se=null,De=null,Ce=!0,Ee=!0,je=!1,Fe=!1,Pe=!1,Me=!1,xe=!1,Te=!1,Ne=!1,Ie=!0,Re=!1,Ae=!0,Le=!0,Ue=!1,Be={},He=k({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ke=null,ze=k({},["audio","video","img","source","image","track"]),qe=null,Ve=k({},["alt","class","for","id","label","name","pattern","placeholder","summary","title","value","style","xmlns"]),We="http://www.w3.org/1998/Math/MathML",Ge="http://www.w3.org/2000/svg",Ye="http://www.w3.org/1999/xhtml",$e=Ye,Qe=!1,Ze=null,Xe=o.createElement("form"),Je=function(e){Ze&&Ze===e||(e&&"object"===(void 0===e?"undefined":q(e))||(e={}),e=S(e),_e="ALLOWED_TAGS"in e?k({},e.ALLOWED_TAGS):Oe,we="ALLOWED_ATTR"in e?k({},e.ALLOWED_ATTR):ke,qe="ADD_URI_SAFE_ATTR"in e?k(S(Ve),e.ADD_URI_SAFE_ATTR):Ve,Ke="ADD_DATA_URI_TAGS"in e?k(S(ze),e.ADD_DATA_URI_TAGS):ze,Se="FORBID_TAGS"in e?k({},e.FORBID_TAGS):{},De="FORBID_ATTR"in e?k({},e.FORBID_ATTR):{},Be="USE_PROFILES"in e&&e.USE_PROFILES,Ce=!1!==e.ALLOW_ARIA_ATTR,Ee=!1!==e.ALLOW_DATA_ATTR,je=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Fe=e.SAFE_FOR_TEMPLATES||!1,Pe=e.WHOLE_DOCUMENT||!1,Te=e.RETURN_DOM||!1,Ne=e.RETURN_DOM_FRAGMENT||!1,Ie=!1!==e.RETURN_DOM_IMPORT,Re=e.RETURN_TRUSTED_TYPE||!1,xe=e.FORCE_BODY||!1,Ae=!1!==e.SANITIZE_DOM,Le=!1!==e.KEEP_CONTENT,Ue=e.IN_PLACE||!1,ve=e.ALLOWED_URI_REGEXP||ve,$e=e.NAMESPACE||Ye,Fe&&(Ee=!1),Ne&&(Te=!0),Be&&(_e=k({},[].concat(V(x))),we=[],!0===Be.html&&(k(_e,C),k(we,T)),!0===Be.svg&&(k(_e,E),k(we,N),k(we,R)),!0===Be.svgFilters&&(k(_e,j),k(we,N),k(we,R)),!0===Be.mathMl&&(k(_e,P),k(we,I),k(we,R))),e.ADD_TAGS&&(_e===Oe&&(_e=S(_e)),k(_e,e.ADD_TAGS)),e.ADD_ATTR&&(we===ke&&(we=S(we)),k(we,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&k(qe,e.ADD_URI_SAFE_ATTR),Le&&(_e["#text"]=!0),Pe&&k(_e,["html","head","body"]),_e.table&&(k(_e,["tbody"]),delete Se.tbody),a&&a(e),Ze=e)},et=k({},["mi","mo","mn","ms","mtext"]),tt=k({},["foreignobject","desc","title","annotation-xml"]),nt=k({},E);k(nt,j),k(nt,F);var rt=k({},P);k(rt,M);var ot=function(e){var t=ne(e);t&&t.tagName||(t={namespaceURI:Ye,tagName:"template"});var n=m(e.tagName),r=m(t.tagName);if(e.namespaceURI===Ge)return t.namespaceURI===Ye?"svg"===n:t.namespaceURI===We?"svg"===n&&("annotation-xml"===r||et[r]):Boolean(nt[n]);if(e.namespaceURI===We)return t.namespaceURI===Ye?"math"===n:t.namespaceURI===Ge?"math"===n&&tt[r]:Boolean(rt[n]);if(e.namespaceURI===Ye){if(t.namespaceURI===Ge&&!tt[r])return!1;if(t.namespaceURI===We&&!et[r])return!1;var o=k({},["title","style","font","a","script"]);return!rt[n]&&(o[n]||!nt[n])}return!1},at=function(e){h(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ae}catch(t){e.remove()}}},it=function(e,t){try{h(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){h(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!we[e])if(Te||Ne)try{at(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},st=function(e){var t=void 0,n=void 0;if(xe)e=""+e;else{var r=b(e,/^[\r\n\t ]+/);n=r&&r[0]}var a=oe?oe.createHTML(e):e;if($e===Ye)try{t=(new Q).parseFromString(a,"text/html")}catch(e){}if(!t||!t.documentElement){t=se.createDocument($e,"template",null);try{t.documentElement.innerHTML=Qe?"":a}catch(e){}}var i=t.body||t.documentElement;return e&&n&&i.insertBefore(o.createTextNode(n),i.childNodes[0]||null),Pe?t.documentElement:i},ct=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},lt=function(e){return!(e instanceof Y||e instanceof $||"string"==typeof e.nodeName&&"string"==typeof e.textContent&&"function"==typeof e.removeChild&&e.attributes instanceof w&&"function"==typeof e.removeAttribute&&"function"==typeof e.setAttribute&&"string"==typeof e.namespaceURI&&"function"==typeof e.insertBefore)},ut=function(e){return"object"===(void 0===c?"undefined":q(c))?e instanceof c:e&&"object"===(void 0===e?"undefined":q(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},dt=function(e,t,r){fe[e]&&f(fe[e],(function(e){e.call(n,t,r,Ze)}))},ft=function(e){var t=void 0;if(dt("beforeSanitizeElements",e,null),lt(e))return at(e),!0;if(b(e.nodeName,/[\u0080-\uFFFF]/))return at(e),!0;var r=m(e.nodeName);if(dt("uponSanitizeElement",e,{tagName:r,allowedTags:_e}),!ut(e.firstElementChild)&&(!ut(e.content)||!ut(e.content.firstElementChild))&&_(/<[/\w]/g,e.innerHTML)&&_(/<[/\w]/g,e.textContent))return at(e),!0;if(!_e[r]||Se[r]){if(Le&&!He[r]){var o=ne(e)||e.parentNode,a=te(e)||e.childNodes;if(a&&o)for(var i=a.length-1;i>=0;--i)o.insertBefore(J(a[i],!0),ee(e))}return at(e),!0}return e instanceof l&&!ot(e)?(at(e),!0):"noscript"!==r&&"noembed"!==r||!_(/<\/no(script|embed)/i,e.innerHTML)?(Fe&&3===e.nodeType&&(t=e.textContent,t=g(t,pe," "),t=g(t,he," "),e.textContent!==t&&(h(n.removed,{element:e.cloneNode()}),e.textContent=t)),dt("afterSanitizeElements",e,null),!1):(at(e),!0)},pt=function(e,t,n){if(Ae&&("id"===t||"name"===t)&&(n in o||n in Xe))return!1;if(Ee&&_(me,t));else if(Ce&&_(be,t));else{if(!we[t]||De[t])return!1;if(qe[t]);else if(_(ve,g(n,ye,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==y(n,"data:")||!Ke[e])if(je&&!_(ge,g(n,ye,"")));else if(n)return!1}return!0},ht=function(e){var t=void 0,r=void 0,o=void 0,a=void 0;dt("beforeSanitizeAttributes",e,null);var i=e.attributes;if(i){var s={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we};for(a=i.length;a--;){var c=t=i[a],l=c.name,u=c.namespaceURI;if(r=v(t.value),o=m(l),s.attrName=o,s.attrValue=r,s.keepAttr=!0,s.forceKeepAttr=void 0,dt("uponSanitizeAttribute",e,s),r=s.attrValue,!s.forceKeepAttr&&(it(l,e),s.keepAttr))if(_(/\/>/i,r))it(l,e);else{Fe&&(r=g(r,pe," "),r=g(r,he," "));var d=e.nodeName.toLowerCase();if(pt(d,o,r))try{u?e.setAttributeNS(u,l,r):e.setAttribute(l,r),p(n.removed)}catch(e){}}}dt("afterSanitizeAttributes",e,null)}},mt=function e(t){var n=void 0,r=ct(t);for(dt("beforeSanitizeShadowDOM",t,null);n=r.nextNode();)dt("uponSanitizeShadowNode",n,null),ft(n)||(n.content instanceof i&&e(n.content),ht(n));dt("afterSanitizeShadowDOM",t,null)};return n.sanitize=function(e,o){var a=void 0,s=void 0,l=void 0,u=void 0,d=void 0;if((Qe=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!ut(e)){if("function"!=typeof e.toString)throw O("toString is not a function");if("string"!=typeof(e=e.toString()))throw O("dirty is not a string, aborting")}if(!n.isSupported){if("object"===q(t.toStaticHTML)||"function"==typeof t.toStaticHTML){if("string"==typeof e)return t.toStaticHTML(e);if(ut(e))return t.toStaticHTML(e.outerHTML)}return e}if(Me||Je(o),n.removed=[],"string"==typeof e&&(Ue=!1),Ue);else if(e instanceof c)1===(s=(a=st("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===s.nodeName||"HTML"===s.nodeName?a=s:a.appendChild(s);else{if(!Te&&!Fe&&!Pe&&-1===e.indexOf("<"))return oe&&Re?oe.createHTML(e):e;if(!(a=st(e)))return Te?null:ae}a&&xe&&at(a.firstChild);for(var f=ct(Ue?e:a);l=f.nextNode();)3===l.nodeType&&l===u||ft(l)||(l.content instanceof i&&mt(l.content),ht(l),u=l);if(u=null,Ue)return e;if(Te){if(Ne)for(d=le.call(a.ownerDocument);a.firstChild;)d.appendChild(a.firstChild);else d=a;return Ie&&(d=ue.call(r,d,!0)),d}var p=Pe?a.outerHTML:a.innerHTML;return Fe&&(p=g(p,pe," "),p=g(p,he," ")),oe&&Re?oe.createHTML(p):p},n.setConfig=function(e){Je(e),Me=!0},n.clearConfig=function(){Ze=null,Me=!1},n.isValidAttribute=function(e,t,n){Ze||Je({});var r=m(e),o=m(t);return pt(r,o,n)},n.addHook=function(e,t){"function"==typeof t&&(fe[e]=fe[e]||[],h(fe[e],t))},n.removeHook=function(e){fe[e]&&p(fe[e])},n.removeHooks=function(e){fe[e]&&(fe[e]=[])},n.removeAllHooks=function(){fe={}},n}()}()},function(e,t){e.exports=window.wp.viewport},,function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t,n){"use strict";var r=n(194),o="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),a=Object.prototype.toString,i=Array.prototype.concat,s=Object.defineProperty,c=s&&function(){var e={};try{for(var t in s(e,"x",{enumerable:!1,value:e}),e)return!1;return e.x===e}catch(e){return!1}}(),l=function(e,t,n,r){var o;(!(t in e)||"function"==typeof(o=r)&&"[object Function]"===a.call(o)&&r())&&(c?s(e,t,{configurable:!0,enumerable:!1,value:n,writable:!0}):e[t]=n)},u=function(e,t){var n=arguments.length>2?arguments[2]:{},a=r(t);o&&(a=i.call(a,Object.getOwnPropertySymbols(t)));for(var s=0;s-1?o(n):n}},function(e,t,n){"use strict";var r=n(22),o=n(25),a=(n(1),n(5)),i=n.n(a),s=n(29),c=n.n(s),l=!1,u=n(33),d=function(e){function t(t,n){var r;r=e.call(this,t,n)||this;var o,a=n&&!n.isMounting?t.enter:t.appear;return r.appearStatus=null,t.in?a?(o="exited",r.appearStatus="entering"):o="entered":o=t.unmountOnExit||t.mountOnEnter?"unmounted":"exited",r.state={status:o},r.nextCallback=null,r}Object(o.a)(t,e),t.getDerivedStateFromProps=function(e,t){return e.in&&"unmounted"===t.status?{status:"exited"}:null};var n=t.prototype;return n.componentDidMount=function(){this.updateStatus(!0,this.appearStatus)},n.componentDidUpdate=function(e){var t=null;if(e!==this.props){var n=this.state.status;this.props.in?"entering"!==n&&"entered"!==n&&(t="entering"):"entering"!==n&&"entered"!==n||(t="exiting")}this.updateStatus(!1,t)},n.componentWillUnmount=function(){this.cancelNextCallback()},n.getTimeouts=function(){var e,t,n,r=this.props.timeout;return e=t=n=r,null!=r&&"number"!=typeof r&&(e=r.exit,t=r.enter,n=void 0!==r.appear?r.appear:t),{exit:e,enter:t,appear:n}},n.updateStatus=function(e,t){void 0===e&&(e=!1),null!==t?(this.cancelNextCallback(),"entering"===t?this.performEnter(e):this.performExit()):this.props.unmountOnExit&&"exited"===this.state.status&&this.setState({status:"unmounted"})},n.performEnter=function(e){var t=this,n=this.props.enter,r=this.context?this.context.isMounting:e,o=this.props.nodeRef?[r]:[c.a.findDOMNode(this),r],a=o[0],i=o[1],s=this.getTimeouts(),u=r?s.appear:s.enter;!e&&!n||l?this.safeSetState({status:"entered"},(function(){t.props.onEntered(a)})):(this.props.onEnter(a,i),this.safeSetState({status:"entering"},(function(){t.props.onEntering(a,i),t.onTransitionEnd(u,(function(){t.safeSetState({status:"entered"},(function(){t.props.onEntered(a,i)}))}))})))},n.performExit=function(){var e=this,t=this.props.exit,n=this.getTimeouts(),r=this.props.nodeRef?void 0:c.a.findDOMNode(this);t&&!l?(this.props.onExit(r),this.safeSetState({status:"exiting"},(function(){e.props.onExiting(r),e.onTransitionEnd(n.exit,(function(){e.safeSetState({status:"exited"},(function(){e.props.onExited(r)}))}))}))):this.safeSetState({status:"exited"},(function(){e.props.onExited(r)}))},n.cancelNextCallback=function(){null!==this.nextCallback&&(this.nextCallback.cancel(),this.nextCallback=null)},n.safeSetState=function(e,t){t=this.setNextCallback(t),this.setState(e,t)},n.setNextCallback=function(e){var t=this,n=!0;return this.nextCallback=function(r){n&&(n=!1,t.nextCallback=null,e(r))},this.nextCallback.cancel=function(){n=!1},this.nextCallback},n.onTransitionEnd=function(e,t){this.setNextCallback(t);var n=this.props.nodeRef?this.props.nodeRef.current:c.a.findDOMNode(this),r=null==e&&!this.props.addEndListener;if(n&&!r){if(this.props.addEndListener){var o=this.props.nodeRef?[this.nextCallback]:[n,this.nextCallback],a=o[0],i=o[1];this.props.addEndListener(a,i)}null!=e&&setTimeout(this.nextCallback,e)}else setTimeout(this.nextCallback,0)},n.render=function(){var e=this.state.status;if("unmounted"===e)return null;var t=this.props,n=t.children,o=(t.in,t.mountOnEnter,t.unmountOnExit,t.appear,t.enter,t.exit,t.timeout,t.addEndListener,t.onEnter,t.onEntering,t.onEntered,t.onExit,t.onExiting,t.onExited,t.nodeRef,Object(r.a)(t,["children","in","mountOnEnter","unmountOnExit","appear","enter","exit","timeout","addEndListener","onEnter","onEntering","onEntered","onExit","onExiting","onExited","nodeRef"]));return i.a.createElement(u.a.Provider,{value:null},"function"==typeof n?n(e,o):i.a.cloneElement(i.a.Children.only(n),o))},t}(i.a.Component);function f(){}d.contextType=u.a,d.propTypes={},d.defaultProps={in:!1,mountOnEnter:!1,unmountOnExit:!1,appear:!1,enter:!0,exit:!0,onEnter:f,onEntering:f,onEntered:f,onExit:f,onExiting:f,onExited:f},d.UNMOUNTED="unmounted",d.EXITED="exited",d.ENTERING="entering",d.ENTERED="entered",d.EXITING="exiting";t.a=d},,,,function(e,t,n){"use strict";var r=n(5),o="function"==typeof Symbol&&Symbol.for&&Symbol.for("react.element")||60103,a=n(50),i=n(70),s=n(71),c="function"==typeof Symbol&&Symbol.iterator;function l(e,t){return e&&"object"==typeof e&&null!=e.key?(n=e.key,r={"=":"=0",":":"=2"},"$"+(""+n).replace(/[=:]/g,(function(e){return r[e]}))):t.toString(36);var n,r}function u(e,t,n,r){var a,s=typeof e;if("undefined"!==s&&"boolean"!==s||(e=null),null===e||"string"===s||"number"===s||"object"===s&&e.$$typeof===o)return n(r,e,""===t?"."+l(e,0):t),1;var d=0,f=""===t?".":t+":";if(Array.isArray(e))for(var p=0;p-1&&e%1==0&&e<=9007199254740991}(e.length)&&"[object Array]"==u.call(e)};e.exports=p},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r,o=n(1),a=(r=o)&&r.__esModule?r:{default:r},i=n(31);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function c(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t2?n-2:0),o=2;o=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(a[n]=e[n])}return a}(e,["icon","size"]);return Object(a.cloneElement)(t,function(e){for(var t=1;t0?r:n)(e)}},function(e,t){e.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},function(e,t,n){var r=n(133);e.exports=function(e){return Object(r(e))}},function(e,t,n){var r=n(341),o=n(342),a=n(107),i=/^\d+$/,s=Object.prototype.hasOwnProperty,c=r(Object,"keys");var l,u=(l="length",function(e){return null==e?void 0:e[l]});function d(e,t){return t=null==t?9007199254740991:t,(e="number"==typeof e||i.test(e)?+e:-1)>-1&&e%1==0&&e-1&&e%1==0&&e<=9007199254740991}function p(e){for(var t=function(e){if(null==e)return[];h(e)||(e=Object(e));var t=e.length;t=t&&f(t)&&(a(e)||o(e))&&t||0;var n=e.constructor,r=-1,i="function"==typeof n&&n.prototype===e,c=Array(t),l=t>0;for(;++rc;)r(s,n=t[c++])&&(~a(l,n)||l.push(n));return l}},function(e,t,n){var r=n(142),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t,n){var r=n(185),o=n(143);e.exports=Object.keys||function(e){return r(e,o)}},function(e,t,n){var r=n(309);e.exports=r},function(e,t,n){"use strict";var r,o,a,i=n(62),s=n(191),c=n(80),l=n(56),u=n(63),d=n(138),f=u("iterator"),p=!1;[].keys&&("next"in(a=[].keys())?(o=s(s(a)))!==Object.prototype&&(r=o):p=!0);var h=null==r||i((function(){var e={};return r[f].call(e)!==e}));h&&(r={}),d&&!h||l(r,f)||c(r,f,(function(){return this})),e.exports={IteratorPrototype:r,BUGGY_SAFARI_ITERATORS:p}},function(e,t,n){var r=n(56),o=n(144),a=n(137),i=n(314),s=a("IE_PROTO"),c=Object.prototype;e.exports=i?Object.getPrototypeOf:function(e){return e=o(e),r(e,s)?e[s]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?c:null}},function(e,t,n){var r=n(315),o=n(316),a=n(62);e.exports=!!Object.getOwnPropertySymbols&&!a((function(){return!Symbol.sham&&(r?38===o:o>37&&o<41)}))},function(e,t,n){var r=n(94).f,o=n(56),a=n(63)("toStringTag");e.exports=function(e,t,n){e&&!o(e=n?e:e.prototype,a)&&r(e,a,{configurable:!0,value:t})}},function(e,t,n){"use strict";var r=Array.prototype.slice,o=n(195),a=Object.keys,i=a?function(e){return a(e)}:n(349),s=Object.keys;i.shim=function(){Object.keys?function(){var e=Object.keys(arguments);return e&&e.length===arguments.length}(1,2)||(Object.keys=function(e){return o(e)?s(r.call(e)):s(e)}):Object.keys=i;return Object.keys||i},e.exports=i},function(e,t,n){"use strict";var r=Object.prototype.toString;e.exports=function(e){var t=r.call(e),n="[object Arguments]"===t;return n||(n="[object Array]"!==t&&null!==e&&"object"==typeof e&&"number"==typeof e.length&&e.length>=0&&"[object Function]"===r.call(e.callee)),n}},function(e,t,n){"use strict";var r="undefined"!=typeof Symbol&&Symbol,o=n(148);e.exports=function(){return"function"==typeof r&&("function"==typeof Symbol&&("symbol"==typeof r("foo")&&("symbol"==typeof Symbol("bar")&&o())))}},function(e,t,n){"use strict";var r=n(194),o=function(e){return null!=e},a=n(148)(),i=n(64),s=Object,c=i("Array.prototype.push"),l=i("Object.prototype.propertyIsEnumerable"),u=a?Object.getOwnPropertySymbols:null;e.exports=function(e,t){if(!o(e))throw new TypeError("target must be an object");var n,i,d,f,p,h,m,b=s(e);for(n=1;n2&&void 0!==arguments[2]&&arguments[2],r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if(!e)return 0;var o="width"===t?"Left":"Top",a="width"===t?"Right":"Bottom",i=!n||r?window.getComputedStyle(e):null,s=e.offsetWidth,c=e.offsetHeight,l="width"===t?s:c;n||(l-=parseFloat(i["padding"+o])+parseFloat(i["padding"+a])+parseFloat(i["border"+o+"Width"])+parseFloat(i["border"+a+"Width"]));r&&(l+=parseFloat(i["margin"+o])+parseFloat(i["margin"+a]));return l}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t=o&&at.clientHeight?t:o(t)}function a(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:new Map,n=r(),i=o(e);return t.set(i,i.style.overflowY),i===n?t:a(i,t)}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n0&&(L||I||i!==O)){var W=y||this.today;H=this.deleteModifierFromRange(H,W,W.clone().add(O,"days"),"blocked-minimum-nights"),H=this.deleteModifierFromRange(H,W,W.clone().add(O,"days"),"blocked")}(L||N)&&(0,d.default)(F).forEach((function(e){Object.keys(e).forEach((function(e){var n=(0,u.default)(e),r=!1;(L||M)&&(s(n)?(H=t.addModifier(H,n,"blocked-out-of-range"),r=!0):H=t.deleteModifier(H,n,"blocked-out-of-range")),(L||x)&&(c(n)?(H=t.addModifier(H,n,"blocked-calendar"),r=!0):H=t.deleteModifier(H,n,"blocked-calendar")),H=r?t.addModifier(H,n,"blocked"):t.deleteModifier(H,n,"blocked"),(L||T)&&(H=l(n)?t.addModifier(H,n,"highlighted-calendar"):t.deleteModifier(H,n,"highlighted-calendar"))}))})),i>0&&n&&o===P.END_DATE&&(H=this.addModifierToRange(H,n,n.clone().add(i,"days"),"blocked-minimum-nights"),H=this.addModifierToRange(H,n,n.clone().add(i,"days"),"blocked"));var G=(0,u.default)();if((0,g.default)(this.today,G)||(H=this.deleteModifier(H,this.today,"today"),H=this.addModifier(H,G,"today"),this.today=G),Object.keys(H).length>0&&this.setState({visibleDays:(0,a.default)({},F,H)}),L||f!==D){var Y=R(f,o);this.setState({phrases:(0,a.default)({},f,{chooseAvailableDate:Y})})}}},{key:"onDayClick",value:function(e,t){var n=this.props,r=n.keepOpenOnDateSelect,o=n.minimumNights,a=n.onBlur,i=n.focusedInput,s=n.onFocusChange,c=n.onClose,l=n.onDatesChange,u=n.startDateOffset,d=n.endDateOffset,f=n.disabled;if(t&&t.preventDefault(),!this.isBlocked(e)){var p=this.props,h=p.startDate,b=p.endDate;if(u||d)h=(0,w.default)(u,e),b=(0,w.default)(d,e),r||(s(null),c({startDate:h,endDate:b}));else if(i===P.START_DATE){var g=b&&b.clone().subtract(o,"days"),_=(0,v.default)(g,e)||(0,y.default)(h,b),O=f===P.END_DATE;O&&_||(h=e,_&&(b=null)),O&&!_?(s(null),c({startDate:h,endDate:b})):O||s(P.END_DATE)}else if(i===P.END_DATE){var k=h&&h.clone().add(o,"days");h?(0,m.default)(e,k)?(b=e,r||(s(null),c({startDate:h,endDate:b}))):f!==P.START_DATE&&(h=e,b=null):(b=e,s(P.START_DATE))}l({startDate:h,endDate:b}),a()}}},{key:"onDayMouseEnter",value:function(e){if(!this.isTouchDevice){var t=this.props,n=t.startDate,r=t.endDate,o=t.focusedInput,i=t.minimumNights,s=t.startDateOffset,c=t.endDateOffset,l=this.state,u=l.hoverDate,d=l.visibleDays,f=null;if(o){var p=s||c,h={};if(p){var m=(0,w.default)(s,e),b=(0,w.default)(c,e,(function(e){return e.add(1,"day")}));f={start:m,end:b},this.state.dateOffset&&this.state.dateOffset.start&&this.state.dateOffset.end&&(h=this.deleteModifierFromRange(h,this.state.dateOffset.start,this.state.dateOffset.end,"hovered-offset")),h=this.addModifierToRange(h,m,b,"hovered-offset")}if(!p){if(h=this.deleteModifier(h,u,"hovered"),h=this.addModifier(h,e,"hovered"),n&&!r&&o===P.END_DATE){if((0,y.default)(u,n)){var _=u.clone().add(1,"day");h=this.deleteModifierFromRange(h,n,_,"hovered-span")}if(!this.isBlocked(e)&&(0,y.default)(e,n)){var O=e.clone().add(1,"day");h=this.addModifierToRange(h,n,O,"hovered-span")}}if(!n&&r&&o===P.START_DATE&&((0,v.default)(u,r)&&(h=this.deleteModifierFromRange(h,u,r,"hovered-span")),!this.isBlocked(e)&&(0,v.default)(e,r)&&(h=this.addModifierToRange(h,e,r,"hovered-span"))),n){var k=n.clone().add(1,"day"),S=n.clone().add(i+1,"days");if(h=this.deleteModifierFromRange(h,k,S,"after-hovered-start"),(0,g.default)(e,n)){var D=n.clone().add(1,"day"),C=n.clone().add(i+1,"days");h=this.addModifierToRange(h,D,C,"after-hovered-start")}}}this.setState({hoverDate:e,dateOffset:f,visibleDays:(0,a.default)({},d,h)})}}}},{key:"onDayMouseLeave",value:function(e){var t=this.props,n=t.startDate,r=t.endDate,o=t.minimumNights,i=this.state,s=i.hoverDate,c=i.visibleDays,l=i.dateOffset;if(!this.isTouchDevice&&s){var u={};if(u=this.deleteModifier(u,s,"hovered"),l&&(u=this.deleteModifierFromRange(u,this.state.dateOffset.start,this.state.dateOffset.end,"hovered-offset")),n&&!r&&(0,y.default)(s,n)){var d=s.clone().add(1,"day");u=this.deleteModifierFromRange(u,n,d,"hovered-span")}if(!n&&r&&(0,y.default)(r,s)&&(u=this.deleteModifierFromRange(u,s,r,"hovered-span")),n&&(0,g.default)(e,n)){var f=n.clone().add(1,"day"),p=n.clone().add(o+1,"days");u=this.deleteModifierFromRange(u,f,p,"after-hovered-start")}this.setState({hoverDate:null,visibleDays:(0,a.default)({},c,u)})}}},{key:"onPrevMonthClick",value:function(){var e=this.props,t=e.onPrevMonthClick,n=e.numberOfMonths,r=e.enableOutsideDays,o=this.state,i=o.currentMonth,s=o.visibleDays,c={};Object.keys(s).sort().slice(0,n+1).forEach((function(e){c[e]=s[e]}));var l=i.clone().subtract(2,"months"),u=(0,_.default)(l,1,r,!0),d=i.clone().subtract(1,"month");this.setState({currentMonth:d,visibleDays:(0,a.default)({},c,this.getModifiers(u))},(function(){t(d.clone())}))}},{key:"onNextMonthClick",value:function(){var e=this.props,t=e.onNextMonthClick,n=e.numberOfMonths,r=e.enableOutsideDays,o=this.state,i=o.currentMonth,s=o.visibleDays,c={};Object.keys(s).sort().slice(1).forEach((function(e){c[e]=s[e]}));var l=i.clone().add(n+1,"month"),u=(0,_.default)(l,1,r,!0),d=i.clone().add(1,"month");this.setState({currentMonth:d,visibleDays:(0,a.default)({},c,this.getModifiers(u))},(function(){t(d.clone())}))}},{key:"onMonthChange",value:function(e){var t=this.props,n=t.numberOfMonths,r=t.enableOutsideDays,o=t.orientation===P.VERTICAL_SCROLLABLE,a=(0,_.default)(e,n,r,o);this.setState({currentMonth:e.clone(),visibleDays:this.getModifiers(a)})}},{key:"onYearChange",value:function(e){var t=this.props,n=t.numberOfMonths,r=t.enableOutsideDays,o=t.orientation===P.VERTICAL_SCROLLABLE,a=(0,_.default)(e,n,r,o);this.setState({currentMonth:e.clone(),visibleDays:this.getModifiers(a)})}},{key:"onMultiplyScrollableMonths",value:function(){var e=this.props,t=e.numberOfMonths,n=e.enableOutsideDays,r=this.state,o=r.currentMonth,i=r.visibleDays,s=Object.keys(i).length,c=o.clone().add(s,"month"),l=(0,_.default)(c,t,n,!0);this.setState({visibleDays:(0,a.default)({},i,this.getModifiers(l))})}},{key:"getFirstFocusableDay",value:function(e){var t=this,n=this.props,o=n.startDate,a=n.endDate,i=n.focusedInput,s=n.minimumNights,c=n.numberOfMonths,l=e.clone().startOf("month");if(i===P.START_DATE&&o?l=o.clone():i===P.END_DATE&&!a&&o?l=o.clone().add(s,"days"):i===P.END_DATE&&a&&(l=a.clone()),this.isBlocked(l)){for(var u=[],d=e.clone().add(c-1,"months").endOf("month"),f=l.clone();!(0,y.default)(f,d);)f=f.clone().add(1,"day"),u.push(f);var p=u.filter((function(e){return!t.isBlocked(e)}));p.length>0&&(l=r(p,1)[0])}return l}},{key:"getModifiers",value:function(e){var t=this,n={};return Object.keys(e).forEach((function(r){n[r]={},e[r].forEach((function(e){n[r][(0,k.default)(e)]=t.getModifiersForDay(e)}))})),n}},{key:"getModifiersForDay",value:function(e){var t=this;return new Set(Object.keys(this.modifiers).filter((function(n){return t.modifiers[n](e)})))}},{key:"getStateForNewMonth",value:function(e){var t=this,n=e.initialVisibleMonth,r=e.numberOfMonths,o=e.enableOutsideDays,a=e.orientation,i=e.startDate,s=(n||(i?function(){return i}:function(){return t.today}))(),c=a===P.VERTICAL_SCROLLABLE;return{currentMonth:s,visibleDays:this.getModifiers((0,_.default)(s,r,o,c))}}},{key:"addModifier",value:function(e,t,n){var r=this.props,o=r.numberOfMonths,i=r.enableOutsideDays,s=r.orientation,c=this.state,l=c.currentMonth,u=c.visibleDays,d=l,f=o;if(s===P.VERTICAL_SCROLLABLE?f=Object.keys(u).length:(d=d.clone().subtract(1,"month"),f+=2),!t||!(0,O.default)(t,d,f,i))return e;var p=(0,k.default)(t),h=(0,a.default)({},e);if(i)h=Object.keys(u).filter((function(e){return Object.keys(u[e]).indexOf(p)>-1})).reduce((function(t,r){var o=e[r]||u[r],i=new Set(o[p]);return i.add(n),(0,a.default)({},t,T({},r,(0,a.default)({},o,T({},p,i))))}),h);else{var m=(0,S.default)(t),b=e[m]||u[m],g=new Set(b[p]);g.add(n),h=(0,a.default)({},h,T({},m,(0,a.default)({},b,T({},p,g))))}return h}},{key:"addModifierToRange",value:function(e,t,n,r){for(var o=e,a=t.clone();(0,v.default)(a,n);)o=this.addModifier(o,a,r),a=a.clone().add(1,"day");return o}},{key:"deleteModifier",value:function(e,t,n){var r=this.props,o=r.numberOfMonths,i=r.enableOutsideDays,s=r.orientation,c=this.state,l=c.currentMonth,u=c.visibleDays,d=l,f=o;if(s===P.VERTICAL_SCROLLABLE?f=Object.keys(u).length:(d=d.clone().subtract(1,"month"),f+=2),!t||!(0,O.default)(t,d,f,i))return e;var p=(0,k.default)(t),h=(0,a.default)({},e);if(i)h=Object.keys(u).filter((function(e){return Object.keys(u[e]).indexOf(p)>-1})).reduce((function(t,r){var o=e[r]||u[r],i=new Set(o[p]);return i.delete(n),(0,a.default)({},t,T({},r,(0,a.default)({},o,T({},p,i))))}),h);else{var m=(0,S.default)(t),b=e[m]||u[m],g=new Set(b[p]);g.delete(n),h=(0,a.default)({},h,T({},m,(0,a.default)({},b,T({},p,g))))}return h}},{key:"deleteModifierFromRange",value:function(e,t,n,r){for(var o=e,a=t.clone();(0,v.default)(a,n);)o=this.deleteModifier(o,a,r),a=a.clone().add(1,"day");return o}},{key:"doesNotMeetMinimumNights",value:function(e){var t=this.props,n=t.startDate,r=t.isOutsideRange,o=t.focusedInput,a=t.minimumNights;if(o!==P.END_DATE)return!1;if(n){var i=e.diff(n.clone().startOf("day").hour(12),"days");return i=0}return r((0,u.default)(e).subtract(a,"days"))}},{key:"isDayAfterHoveredStartDate",value:function(e){var t=this.props,n=t.startDate,r=t.endDate,o=t.minimumNights,a=(this.state||{}).hoverDate;return!!n&&!r&&!this.isBlocked(e)&&(0,b.default)(a,e)&&o>0&&(0,g.default)(a,n)}},{key:"isEndDate",value:function(e){var t=this.props.endDate;return(0,g.default)(e,t)}},{key:"isHovered",value:function(e){var t=(this.state||{}).hoverDate;return!!this.props.focusedInput&&(0,g.default)(e,t)}},{key:"isInHoveredSpan",value:function(e){var t=this.props,n=t.startDate,r=t.endDate,o=(this.state||{}).hoverDate,a=!!n&&!r&&(e.isBetween(n,o)||(0,g.default)(o,e)),i=!!r&&!n&&(e.isBetween(o,r)||(0,g.default)(o,e)),s=o&&!this.isBlocked(o);return(a||i)&&s}},{key:"isInSelectedSpan",value:function(e){var t=this.props,n=t.startDate,r=t.endDate;return e.isBetween(n,r)}},{key:"isLastInRange",value:function(e){var t=this.props.endDate;return this.isInSelectedSpan(e)&&(0,b.default)(e,t)}},{key:"isStartDate",value:function(e){var t=this.props.startDate;return(0,g.default)(e,t)}},{key:"isBlocked",value:function(e){var t=this.props,n=t.isDayBlocked,r=t.isOutsideRange;return n(e)||r(e)||this.doesNotMeetMinimumNights(e)}},{key:"isToday",value:function(e){return(0,g.default)(e,this.today)}},{key:"isFirstDayOfWeek",value:function(e){var t=this.props.firstDayOfWeek;return e.day()===(t||u.default.localeData().firstDayOfWeek())}},{key:"isLastDayOfWeek",value:function(e){var t=this.props.firstDayOfWeek;return e.day()===((t||u.default.localeData().firstDayOfWeek())+6)%7}},{key:"render",value:function(){var e=this.props,t=e.numberOfMonths,n=e.orientation,r=e.monthFormat,o=e.renderMonthText,a=e.navPrev,s=e.navNext,c=e.noNavButtons,l=e.onOutsideClick,u=e.withPortal,d=e.enableOutsideDays,f=e.firstDayOfWeek,p=e.hideKeyboardShortcutsPanel,h=e.daySize,m=e.focusedInput,b=e.renderCalendarDay,g=e.renderDayContents,y=e.renderCalendarInfo,v=e.renderMonthElement,_=e.calendarInfoPosition,O=e.onBlur,w=e.isFocused,k=e.showKeyboardShortcuts,S=e.isRTL,D=e.weekDayFormat,C=e.dayAriaLabelFormat,E=e.verticalHeight,j=e.noBorder,F=e.transitionDuration,P=e.verticalBorderSpacing,x=e.horizontalMonthPadding,T=this.state,N=T.currentMonth,I=T.phrases,R=T.visibleDays;return i.default.createElement(M.default,{orientation:n,enableOutsideDays:d,modifiers:R,numberOfMonths:t,onDayClick:this.onDayClick,onDayMouseEnter:this.onDayMouseEnter,onDayMouseLeave:this.onDayMouseLeave,onPrevMonthClick:this.onPrevMonthClick,onNextMonthClick:this.onNextMonthClick,onMonthChange:this.onMonthChange,onYearChange:this.onYearChange,onMultiplyScrollableMonths:this.onMultiplyScrollableMonths,monthFormat:r,renderMonthText:o,withPortal:u,hidden:!m,initialVisibleMonth:function(){return N},daySize:h,onOutsideClick:l,navPrev:a,navNext:s,noNavButtons:c,renderCalendarDay:b,renderDayContents:g,renderCalendarInfo:y,renderMonthElement:v,calendarInfoPosition:_,firstDayOfWeek:f,hideKeyboardShortcutsPanel:p,isFocused:w,getFirstFocusableDay:this.getFirstFocusableDay,onBlur:O,showKeyboardShortcuts:k,phrases:I,isRTL:S,weekDayFormat:D,dayAriaLabelFormat:C,verticalHeight:E,verticalBorderSpacing:P,noBorder:j,transitionDuration:F,horizontalMonthPadding:x})}}]),t}(i.default.Component);t.default=A,A.propTypes=N,A.defaultProps=I},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){if(!r.default.isMoment(e)||!r.default.isMoment(t))return!1;var n=(0,r.default)(e).add(1,"day");return(0,o.default)(n,t)};var r=a(n(9)),o=a(n(82));function a(e){return e&&e.__esModule?e:{default:e}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t,n,a){if(!r.default.isMoment(e))return{};for(var i={},s=a?e.clone():e.clone().subtract(1,"month"),c=0;c<(a?t:t+2);c+=1){var l=[],u=s.clone(),d=u.clone().startOf("month").hour(12),f=u.clone().endOf("month").hour(12),p=d.clone();if(n)for(var h=0;h0&&this.setState({visibleDays:(0,a.default)({},k,M)})}},{key:"componentWillUpdate",value:function(){this.today=(0,u.default)()}},{key:"onDayClick",value:function(e,t){if(t&&t.preventDefault(),!this.isBlocked(e)){var n=this.props,r=n.onDateChange,o=n.keepOpenOnDateSelect,a=n.onFocusChange,i=n.onClose;r(e),o||(a({focused:!1}),i({date:e}))}}},{key:"onDayMouseEnter",value:function(e){if(!this.isTouchDevice){var t=this.state,n=t.hoverDate,r=t.visibleDays,o=this.deleteModifier({},n,"hovered");o=this.addModifier(o,e,"hovered"),this.setState({hoverDate:e,visibleDays:(0,a.default)({},r,o)})}}},{key:"onDayMouseLeave",value:function(){var e=this.state,t=e.hoverDate,n=e.visibleDays;if(!this.isTouchDevice&&t){var r=this.deleteModifier({},t,"hovered");this.setState({hoverDate:null,visibleDays:(0,a.default)({},n,r)})}}},{key:"onPrevMonthClick",value:function(){var e=this.props,t=e.onPrevMonthClick,n=e.numberOfMonths,r=e.enableOutsideDays,o=this.state,i=o.currentMonth,s=o.visibleDays,c={};Object.keys(s).sort().slice(0,n+1).forEach((function(e){c[e]=s[e]}));var l=i.clone().subtract(1,"month"),u=(0,g.default)(l,1,r);this.setState({currentMonth:l,visibleDays:(0,a.default)({},c,this.getModifiers(u))},(function(){t(l.clone())}))}},{key:"onNextMonthClick",value:function(){var e=this.props,t=e.onNextMonthClick,n=e.numberOfMonths,r=e.enableOutsideDays,o=this.state,i=o.currentMonth,s=o.visibleDays,c={};Object.keys(s).sort().slice(1).forEach((function(e){c[e]=s[e]}));var l=i.clone().add(n,"month"),u=(0,g.default)(l,1,r),d=i.clone().add(1,"month");this.setState({currentMonth:d,visibleDays:(0,a.default)({},c,this.getModifiers(u))},(function(){t(d.clone())}))}},{key:"onMonthChange",value:function(e){var t=this.props,n=t.numberOfMonths,r=t.enableOutsideDays,o=t.orientation===S.VERTICAL_SCROLLABLE,a=(0,g.default)(e,n,r,o);this.setState({currentMonth:e.clone(),visibleDays:this.getModifiers(a)})}},{key:"onYearChange",value:function(e){var t=this.props,n=t.numberOfMonths,r=t.enableOutsideDays,o=t.orientation===S.VERTICAL_SCROLLABLE,a=(0,g.default)(e,n,r,o);this.setState({currentMonth:e.clone(),visibleDays:this.getModifiers(a)})}},{key:"getFirstFocusableDay",value:function(e){var t=this,n=this.props,o=n.date,a=n.numberOfMonths,i=e.clone().startOf("month");if(o&&(i=o.clone()),this.isBlocked(i)){for(var s=[],c=e.clone().add(a-1,"months").endOf("month"),l=i.clone();!(0,b.default)(l,c);)l=l.clone().add(1,"day"),s.push(l);var u=s.filter((function(e){return!t.isBlocked(e)&&(0,b.default)(e,i)}));if(u.length>0){var d=r(u,1);i=d[0]}}return i}},{key:"getModifiers",value:function(e){var t=this,n={};return Object.keys(e).forEach((function(r){n[r]={},e[r].forEach((function(e){n[r][(0,v.default)(e)]=t.getModifiersForDay(e)}))})),n}},{key:"getModifiersForDay",value:function(e){var t=this;return new Set(Object.keys(this.modifiers).filter((function(n){return t.modifiers[n](e)})))}},{key:"getStateForNewMonth",value:function(e){var t=this,n=e.initialVisibleMonth,r=e.date,o=e.numberOfMonths,a=e.enableOutsideDays,i=(n||(r?function(){return r}:function(){return t.today}))();return{currentMonth:i,visibleDays:this.getModifiers((0,g.default)(i,o,a))}}},{key:"addModifier",value:function(e,t,n){var r=this.props,o=r.numberOfMonths,i=r.enableOutsideDays,s=r.orientation,c=this.state,l=c.currentMonth,u=c.visibleDays,d=l,f=o;if(s===S.VERTICAL_SCROLLABLE?f=Object.keys(u).length:(d=d.clone().subtract(1,"month"),f+=2),!t||!(0,y.default)(t,d,f,i))return e;var p=(0,v.default)(t),h=(0,a.default)({},e);if(i)h=Object.keys(u).filter((function(e){return Object.keys(u[e]).indexOf(p)>-1})).reduce((function(t,r){var o=e[r]||u[r],i=new Set(o[p]);return i.add(n),(0,a.default)({},t,E({},r,(0,a.default)({},o,E({},p,i))))}),h);else{var m=(0,_.default)(t),b=e[m]||u[m],g=new Set(b[p]);g.add(n),h=(0,a.default)({},h,E({},m,(0,a.default)({},b,E({},p,g))))}return h}},{key:"deleteModifier",value:function(e,t,n){var r=this.props,o=r.numberOfMonths,i=r.enableOutsideDays,s=r.orientation,c=this.state,l=c.currentMonth,u=c.visibleDays,d=l,f=o;if(s===S.VERTICAL_SCROLLABLE?f=Object.keys(u).length:(d=d.clone().subtract(1,"month"),f+=2),!t||!(0,y.default)(t,d,f,i))return e;var p=(0,v.default)(t),h=(0,a.default)({},e);if(i)h=Object.keys(u).filter((function(e){return Object.keys(u[e]).indexOf(p)>-1})).reduce((function(t,r){var o=e[r]||u[r],i=new Set(o[p]);return i.delete(n),(0,a.default)({},t,E({},r,(0,a.default)({},o,E({},p,i))))}),h);else{var m=(0,_.default)(t),b=e[m]||u[m],g=new Set(b[p]);g.delete(n),h=(0,a.default)({},h,E({},m,(0,a.default)({},b,E({},p,g))))}return h}},{key:"isBlocked",value:function(e){var t=this.props,n=t.isDayBlocked,r=t.isOutsideRange;return n(e)||r(e)}},{key:"isHovered",value:function(e){var t=(this.state||{}).hoverDate;return(0,m.default)(e,t)}},{key:"isSelected",value:function(e){var t=this.props.date;return(0,m.default)(e,t)}},{key:"isToday",value:function(e){return(0,m.default)(e,this.today)}},{key:"isFirstDayOfWeek",value:function(e){var t=this.props.firstDayOfWeek;return e.day()===(t||u.default.localeData().firstDayOfWeek())}},{key:"isLastDayOfWeek",value:function(e){var t=this.props.firstDayOfWeek;return e.day()===((t||u.default.localeData().firstDayOfWeek())+6)%7}},{key:"render",value:function(){var e=this.props,t=e.numberOfMonths,n=e.orientation,r=e.monthFormat,o=e.renderMonthText,a=e.navPrev,s=e.navNext,c=e.onOutsideClick,l=e.withPortal,u=e.focused,d=e.enableOutsideDays,f=e.hideKeyboardShortcutsPanel,p=e.daySize,h=e.firstDayOfWeek,m=e.renderCalendarDay,b=e.renderDayContents,g=e.renderCalendarInfo,y=e.renderMonthElement,v=e.calendarInfoPosition,_=e.isFocused,O=e.isRTL,w=e.phrases,k=e.dayAriaLabelFormat,S=e.onBlur,C=e.showKeyboardShortcuts,E=e.weekDayFormat,j=e.verticalHeight,F=e.noBorder,P=e.transitionDuration,M=e.verticalBorderSpacing,x=e.horizontalMonthPadding,T=this.state,N=T.currentMonth,I=T.visibleDays;return i.default.createElement(D.default,{orientation:n,enableOutsideDays:d,modifiers:I,numberOfMonths:t,onDayClick:this.onDayClick,onDayMouseEnter:this.onDayMouseEnter,onDayMouseLeave:this.onDayMouseLeave,onPrevMonthClick:this.onPrevMonthClick,onNextMonthClick:this.onNextMonthClick,onMonthChange:this.onMonthChange,onYearChange:this.onYearChange,monthFormat:r,withPortal:l,hidden:!u,hideKeyboardShortcutsPanel:f,initialVisibleMonth:function(){return N},firstDayOfWeek:h,onOutsideClick:c,navPrev:a,navNext:s,renderMonthText:o,renderCalendarDay:m,renderDayContents:b,renderCalendarInfo:g,renderMonthElement:y,calendarInfoPosition:v,isFocused:_,getFirstFocusableDay:this.getFirstFocusableDay,onBlur:S,phrases:w,daySize:p,isRTL:O,showKeyboardShortcuts:C,weekDayFormat:E,dayAriaLabelFormat:k,verticalHeight:j,noBorder:F,transitionDuration:P,verticalBorderSpacing:M,horizontalMonthPadding:x})}}]),t}(i.default.Component);t.default=P,P.propTypes=j,P.defaultProps=F},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=h(n(1)),o=h(n(58)),a=n(31),i=n(37),s=h(n(40)),c=h(n(96)),l=h(n(214)),u=h(n(215)),d=h(n(86)),f=h(n(75)),p=h(n(97));function h(e){return e&&e.__esModule?e:{default:e}}t.default={date:o.default.momentObj,onDateChange:r.default.func.isRequired,focused:r.default.bool,onFocusChange:r.default.func.isRequired,id:r.default.string.isRequired,placeholder:r.default.string,disabled:r.default.bool,required:r.default.bool,readOnly:r.default.bool,screenReaderInputMessage:r.default.string,showClearDate:r.default.bool,customCloseIcon:r.default.node,showDefaultInputIcon:r.default.bool,inputIconPosition:c.default,customInputIcon:r.default.node,noBorder:r.default.bool,block:r.default.bool,small:r.default.bool,regular:r.default.bool,verticalSpacing:a.nonNegativeInteger,keepFocusOnInput:r.default.bool,renderMonthText:(0,a.mutuallyExclusiveProps)(r.default.func,"renderMonthText","renderMonthElement"),renderMonthElement:(0,a.mutuallyExclusiveProps)(r.default.func,"renderMonthText","renderMonthElement"),orientation:l.default,anchorDirection:u.default,openDirection:d.default,horizontalMargin:r.default.number,withPortal:r.default.bool,withFullScreenPortal:r.default.bool,appendToBody:r.default.bool,disableScroll:r.default.bool,initialVisibleMonth:r.default.func,firstDayOfWeek:f.default,numberOfMonths:r.default.number,keepOpenOnDateSelect:r.default.bool,reopenPickerOnClearDate:r.default.bool,renderCalendarInfo:r.default.func,calendarInfoPosition:p.default,hideKeyboardShortcutsPanel:r.default.bool,daySize:a.nonNegativeInteger,isRTL:r.default.bool,verticalHeight:a.nonNegativeInteger,transitionDuration:a.nonNegativeInteger,horizontalMonthPadding:a.nonNegativeInteger,navPrev:r.default.node,navNext:r.default.node,onPrevMonthClick:r.default.func,onNextMonthClick:r.default.func,onClose:r.default.func,renderCalendarDay:r.default.func,renderDayContents:r.default.func,enableOutsideDays:r.default.bool,isDayBlocked:r.default.func,isOutsideRange:r.default.func,isDayHighlighted:r.default.func,displayFormat:r.default.oneOfType([r.default.string,r.default.func]),monthFormat:r.default.string,weekDayFormat:r.default.string,phrases:r.default.shape((0,s.default)(i.SingleDatePickerPhrases)),dayAriaLabelFormat:r.default.string}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0]&&(n=i(arguments[0]));var l=r(e,0);return o(l,e,t,0,n),l}},function(e,t,n){"use strict";var r=n(26)("%Object.defineProperty%",!0);if(r)try{r({},"a",{value:1})}catch(e){r=null}var o=n(64)("Object.prototype.propertyIsEnumerable");e.exports=function(e,t,n,a,i,s){if(!r){if(!e(s))return!1;if(!s["[[Configurable]]"]||!s["[[Writable]]"])return!1;if(i in a&&o(a,i)!==!!s["[[Enumerable]]"])return!1;var c=s["[[Value]]"];return a[i]=c,t(a[i],c)}return r(a,i,n(s)),!0}},function(e,t,n){"use strict";var r=n(156),o=n(45);e.exports=function(e){if(void 0===e)return e;r(o,"Property Descriptor","Desc",e);var t={};return"[[Value]]"in e&&(t.value=e["[[Value]]"]),"[[Writable]]"in e&&(t.writable=e["[[Writable]]"]),"[[Get]]"in e&&(t.get=e["[[Get]]"]),"[[Set]]"in e&&(t.set=e["[[Set]]"]),"[[Enumerable]]"in e&&(t.enumerable=e["[[Enumerable]]"]),"[[Configurable]]"in e&&(t.configurable=e["[[Configurable]]"]),t}},function(e,t,n){"use strict";var r=n(74),o=n(156),a=n(45);e.exports=function(e){return void 0!==e&&(o(a,"Property Descriptor","Desc",e),!(!r(e,"[[Value]]")&&!r(e,"[[Writable]]")))}},function(e,t,n){"use strict";var r=n(157);e.exports=function(e,t){return e===t?0!==e||1/e==1/t:r(e)&&r(t)}},function(e,t,n){"use strict";var r=n(74),o=n(26)("%TypeError%"),a=n(45),i=n(240),s=n(407);e.exports=function(e){if("Object"!==a(e))throw new o("ToPropertyDescriptor requires an object");var t={};if(r(e,"enumerable")&&(t["[[Enumerable]]"]=i(e.enumerable)),r(e,"configurable")&&(t["[[Configurable]]"]=i(e.configurable)),r(e,"value")&&(t["[[Value]]"]=e.value),r(e,"writable")&&(t["[[Writable]]"]=i(e.writable)),r(e,"get")){var n=e.get;if(void 0!==n&&!s(n))throw new o("getter must be a function");t["[[Get]]"]=n}if(r(e,"set")){var c=e.set;if(void 0!==c&&!s(c))throw new o("setter must be a function");t["[[Set]]"]=c}if((r(t,"[[Get]]")||r(t,"[[Set]]"))&&(r(t,"[[Value]]")||r(t,"[[Writable]]")))throw new o("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return t}},function(e,t,n){"use strict";e.exports=function(e){return!!e}},function(e,t,n){"use strict";var r=Number.isNaN||function(e){return e!=e};e.exports=Number.isFinite||function(e){return"number"==typeof e&&!r(e)&&e!==1/0&&e!==-1/0}},function(e,t,n){"use strict";var r=n(26),o=r("%Math%"),a=r("%Number%");e.exports=a.MAX_SAFE_INTEGER||o.pow(2,53)-1},function(e,t,n){"use strict";var r=n(148);e.exports=function(){return r()&&!!Symbol.toStringTag}},function(e,t,n){"use strict";e.exports=function(e){return null===e||"function"!=typeof e&&"object"!=typeof e}},function(e,t,n){"use strict";var r=n(242),o=n(246);e.exports=function(e){var t=o(e);return t<=0?0:t>r?r:t}},function(e,t,n){"use strict";var r=n(422),o=n(429);e.exports=function(e){var t=o(e);return 0!==t&&(t=r(t)),0===t?0:t}},function(e,t,n){"use strict";e.exports=function(e){return null===e||"function"!=typeof e&&"object"!=typeof e}},function(e,t,n){"use strict";var r=Object.prototype.toString;if(n(196)()){var o=Symbol.prototype.toString,a=/^Symbol\(.*\)$/;e.exports=function(e){if("symbol"==typeof e)return!0;if("[object Symbol]"!==r.call(e))return!1;try{return function(e){return"symbol"==typeof e.valueOf()&&a.test(o.call(e))}(e)}catch(e){return!1}}}else e.exports=function(e){return!1}},function(e,t,n){"use strict";var r=n(234);e.exports=function(){return Array.prototype.flat||r}},,,function(e,t,n){"use strict";n.r(t),n.d(t,"Portal",(function(){return _})),n.d(t,"PortalWithState",(function(){return S}));var r=n(29),o=n.n(r),a=n(5),i=n.n(a),s=n(1),c=n.n(s),l=!("undefined"==typeof window||!window.document||!window.document.createElement),u=function(){function e(e,t){for(var n=0;nu;)if((s=c[u++])!=s)return!0}else for(;l>u;u++)if((e||u in c)&&c[u]===n)return e||u||0;return!e&&-1}};e.exports={includes:i(!0),indexOf:i(!1)}},function(e,t,n){var r=n(142),o=Math.max,a=Math.min;e.exports=function(e,t){var n=r(e);return n<0?o(n+t,0):a(n,t)}},function(e,t,n){var r=n(62),o=/#|\.prototype\./,a=function(e,t){var n=s[i(e)];return n==l||n!=c&&("function"==typeof t?r(t):!!t)},i=a.normalize=function(e){return String(e).replace(o,".").toLowerCase()},s=a.data={},c=a.NATIVE="N",l=a.POLYFILL="P";e.exports=a},function(e,t,n){"use strict";var r=n(79),o=n(62),a=n(188),i=n(187),s=n(176),c=n(144),l=n(177),u=Object.assign,d=Object.defineProperty;e.exports=!u||o((function(){if(r&&1!==u({b:1},u(d({},"a",{enumerable:!0,get:function(){d(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var e={},t={},n=Symbol();return e[n]=7,"abcdefghijklmnopqrst".split("").forEach((function(e){t[e]=e})),7!=u({},e)[n]||"abcdefghijklmnopqrst"!=a(u({},t)).join("")}))?function(e,t){for(var n=c(e),o=arguments.length,u=1,d=i.f,f=s.f;o>u;)for(var p,h=l(arguments[u++]),m=d?a(h).concat(d(h)):a(h),b=m.length,g=0;b>g;)p=m[g++],r&&!f.call(h,p)||(n[p]=h[p]);return n}:u},function(e,t,n){n(310),n(324);var r=n(141);e.exports=r.Array.from},function(e,t,n){"use strict";var r=n(311).charAt,o=n(182),a=n(312),i=o.set,s=o.getterFor("String Iterator");a(String,"String",(function(e){i(this,{type:"String Iterator",string:String(e),index:0})}),(function(){var e,t=s(this),n=t.string,o=t.index;return o>=n.length?{value:void 0,done:!0}:(e=r(n,o),t.index+=e.length,{value:e,done:!1})}))},function(e,t,n){var r=n(142),o=n(133),a=function(e){return function(t,n){var a,i,s=String(o(t)),c=r(n),l=s.length;return c<0||c>=l?e?"":void 0:(a=s.charCodeAt(c))<55296||a>56319||c+1===l||(i=s.charCodeAt(c+1))<56320||i>57343?e?s.charAt(c):a:e?s.slice(c,c+2):i-56320+(a-55296<<10)+65536}};e.exports={codeAt:a(!1),charAt:a(!0)}},function(e,t,n){"use strict";var r=n(130),o=n(313),a=n(191),i=n(322),s=n(193),c=n(80),l=n(180),u=n(63),d=n(138),f=n(106),p=n(190),h=p.IteratorPrototype,m=p.BUGGY_SAFARI_ITERATORS,b=u("iterator"),g=function(){return this};e.exports=function(e,t,n,u,p,y,v){o(n,t,u);var _,O,w,k=function(e){if(e===p&&j)return j;if(!m&&e in C)return C[e];switch(e){case"keys":case"values":case"entries":return function(){return new n(this,e)}}return function(){return new n(this)}},S=t+" Iterator",D=!1,C=e.prototype,E=C[b]||C["@@iterator"]||p&&C[p],j=!m&&E||k(p),F="Array"==t&&C.entries||E;if(F&&(_=a(F.call(new e)),h!==Object.prototype&&_.next&&(d||a(_)===h||(i?i(_,h):"function"!=typeof _[b]&&c(_,b,g)),s(_,S,!0,!0),d&&(f[S]=g))),"values"==p&&E&&"values"!==E.name&&(D=!0,j=function(){return E.call(this)}),d&&!v||C[b]===j||c(C,b,j),f[t]=j,p)if(O={values:k("values"),keys:y?j:k("keys"),entries:k("entries")},v)for(w in O)(m||D||!(w in C))&&l(C,w,O[w]);else r({target:t,proto:!0,forced:m||D},O);return O}},function(e,t,n){"use strict";var r=n(190).IteratorPrototype,o=n(319),a=n(105),i=n(193),s=n(106),c=function(){return this};e.exports=function(e,t,n){var l=t+" Iterator";return e.prototype=o(r,{next:a(1,n)}),i(e,l,!1,!0),s[l]=c,e}},function(e,t,n){var r=n(62);e.exports=!r((function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype}))},function(e,t,n){var r=n(132),o=n(44);e.exports="process"==r(o.process)},function(e,t,n){var r,o,a=n(44),i=n(317),s=a.process,c=s&&s.versions,l=c&&c.v8;l?o=(r=l.split("."))[0]+r[1]:i&&(!(r=i.match(/Edge\/(\d+)/))||r[1]>=74)&&(r=i.match(/Chrome\/(\d+)/))&&(o=r[1]),e.exports=o&&+o},function(e,t,n){var r=n(140);e.exports=r("navigator","userAgent")||""},function(e,t,n){var r=n(192);e.exports=r&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},function(e,t,n){var r,o=n(73),a=n(320),i=n(143),s=n(139),c=n(321),l=n(179),u=n(137),d=u("IE_PROTO"),f=function(){},p=function(e){return"` + * This function checks whether an element matching that selector exists. + * Useful to know if a script has already been appended to the page. + */ +const isScriptTagInDOM = ( scriptId: string ): boolean => { + const scriptElements = document.querySelectorAll( `script#${ scriptId }` ); + return scriptElements.length > 0; +}; + +/** + * Appends a script element to the document body if a script with the same id + * doesn't exist. + */ +const appendScript = ( attributes: appendScriptAttributesParam ): void => { + // Abort if id is not valid or a script with the same id exists. + if ( ! isString( attributes.id ) || isScriptTagInDOM( attributes.id ) ) { + return; + } + const scriptElement = document.createElement( 'script' ); + for ( const attr in attributes ) { + // We could technically be iterating over inherited members here, so + // if this is the case we should skip it. + if ( ! attributes.hasOwnProperty( attr ) ) { + continue; + } + const key = attr as keyof appendScriptAttributesParam; + + // Skip the keys that aren't strings, because TS can't be sure which + // key in the scriptElement object we're assigning to. + if ( key === 'onload' || key === 'onerror' ) { + continue; + } + + // This assignment stops TS complaining about the value maybe being + // undefined following the isString check below. + const value = attributes[ key ]; + if ( isString( value ) ) { + scriptElement[ key ] = value; + } + } + + // Now that we've assigned all the strings, we can explicitly assign to the + // function keys. + if ( typeof attributes.onload === 'function' ) { + scriptElement.onload = attributes.onload; + } + if ( typeof attributes.onerror === 'function' ) { + scriptElement.onerror = attributes.onerror; + } + document.body.appendChild( scriptElement ); +}; + +/** + * Appends a `"; + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php b/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php new file mode 100644 index 0000000..0521a43 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php @@ -0,0 +1,92 @@ + 'string', + 'enum' => array( 'left', 'center', 'right', 'wide', 'full' ), + ); + } + + /** + * Get the schema for a list of IDs. + * + * @return array Property definition for a list of numeric ids. + */ + protected function get_schema_list_ids() { + return array( + 'type' => 'array', + 'items' => array( + 'type' => 'number', + ), + 'default' => array(), + ); + } + + /** + * Get the schema for a boolean value. + * + * @param string $default The default value. + * @return array Property definition. + */ + protected function get_schema_boolean( $default = true ) { + return array( + 'type' => 'boolean', + 'default' => $default, + ); + } + + /** + * Get the schema for a numeric value. + * + * @param string $default The default value. + * @return array Property definition. + */ + protected function get_schema_number( $default ) { + return array( + 'type' => 'number', + 'default' => $default, + ); + } + + /** + * Get the schema for a string value. + * + * @param string $default The default value. + * @return array Property definition. + */ + protected function get_schema_string( $default = '' ) { + return array( + 'type' => 'string', + 'default' => $default, + ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php b/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php new file mode 100644 index 0000000..75c47d9 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php @@ -0,0 +1,520 @@ + $this->get_schema_string(), + 'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ), + 'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ), + 'categories' => $this->get_schema_list_ids(), + 'catOperator' => array( + 'type' => 'string', + 'default' => 'any', + ), + 'contentVisibility' => $this->get_schema_content_visibility(), + 'align' => $this->get_schema_align(), + 'alignButtons' => $this->get_schema_boolean( false ), + 'isPreview' => $this->get_schema_boolean( false ), + ); + } + + /** + * Include and render the dynamic block. + * + * @param array $attributes Block attributes. Default empty array. + * @param string $content Block content. Default empty string. + * @return string Rendered block type output. + */ + protected function render( $attributes = array(), $content = '' ) { + $this->attributes = $this->parse_attributes( $attributes ); + $this->content = $content; + $this->query_args = $this->parse_query_args(); + $products = array_filter( array_map( 'wc_get_product', $this->get_products() ) ); + + if ( ! $products ) { + return ''; + } + + /** + * Product List Render event. + * + * Fires a WP Hook named `experimental__woocommerce_blocks-product-list-render` on render so that the client + * can add event handling when certain products are displayed. This can be used by tracking extensions such + * as Google Analytics to track impressions. + * + * Provides the list of product data (shaped like the Store API responses) and the block name. + */ + $this->asset_api->add_inline_script( + 'wp-hooks', + ' + window.addEventListener( "DOMContentLoaded", () => { + wp.hooks.doAction( + "experimental__woocommerce_blocks-product-list-render", + { + products: JSON.parse( decodeURIComponent( "' . esc_js( + rawurlencode( + wp_json_encode( + array_map( + [ Package::container()->get( SchemaController::class )->get( 'product' ), 'get_item_response' ], + $products + ) + ) + ) + ) . '" ) ), + listName: "' . esc_js( $this->block_name ) . '" + } + ); + } ); + ', + 'after' + ); + + return sprintf( + '
      %s
    ', + esc_attr( $this->get_container_classes() ), + implode( '', array_map( array( $this, 'render_product' ), $products ) ) + ); + } + + /** + * Get the schema for the contentVisibility attribute + * + * @return array List of block attributes with type and defaults. + */ + protected function get_schema_content_visibility() { + return array( + 'type' => 'object', + 'properties' => array( + 'title' => $this->get_schema_boolean( true ), + 'price' => $this->get_schema_boolean( true ), + 'rating' => $this->get_schema_boolean( true ), + 'button' => $this->get_schema_boolean( true ), + ), + ); + } + + /** + * Get the schema for the orderby attribute. + * + * @return array Property definition of `orderby` attribute. + */ + protected function get_schema_orderby() { + return array( + 'type' => 'string', + 'enum' => array( 'date', 'popularity', 'price_asc', 'price_desc', 'rating', 'title', 'menu_order' ), + 'default' => 'date', + ); + } + + /** + * Get the block's attributes. + * + * @param array $attributes Block attributes. Default empty array. + * @return array Block attributes merged with defaults. + */ + protected function parse_attributes( $attributes ) { + // These should match what's set in JS `registerBlockType`. + $defaults = array( + 'columns' => wc_get_theme_support( 'product_blocks::default_columns', 3 ), + 'rows' => wc_get_theme_support( 'product_blocks::default_rows', 3 ), + 'alignButtons' => false, + 'categories' => array(), + 'catOperator' => 'any', + 'contentVisibility' => array( + 'title' => true, + 'price' => true, + 'rating' => true, + 'button' => true, + ), + ); + + return wp_parse_args( $attributes, $defaults ); + } + + /** + * Parse query args. + * + * @return array + */ + protected function parse_query_args() { + $query_args = array( + 'post_type' => 'product', + 'post_status' => 'publish', + 'fields' => 'ids', + 'ignore_sticky_posts' => true, + 'no_found_rows' => false, + 'orderby' => '', + 'order' => '', + 'meta_query' => WC()->query->get_meta_query(), // phpcs:ignore WordPress.DB.SlowDBQuery + 'tax_query' => array(), // phpcs:ignore WordPress.DB.SlowDBQuery + 'posts_per_page' => $this->get_products_limit(), + ); + + $this->set_block_query_args( $query_args ); + $this->set_ordering_query_args( $query_args ); + $this->set_categories_query_args( $query_args ); + $this->set_visibility_query_args( $query_args ); + + return $query_args; + } + + /** + * Parse query args. + * + * @param array $query_args Query args. + */ + protected function set_ordering_query_args( &$query_args ) { + if ( isset( $this->attributes['orderby'] ) ) { + if ( 'price_desc' === $this->attributes['orderby'] ) { + $query_args['orderby'] = 'price'; + $query_args['order'] = 'DESC'; + } elseif ( 'price_asc' === $this->attributes['orderby'] ) { + $query_args['orderby'] = 'price'; + $query_args['order'] = 'ASC'; + } elseif ( 'date' === $this->attributes['orderby'] ) { + $query_args['orderby'] = 'date'; + $query_args['order'] = 'DESC'; + } else { + $query_args['orderby'] = $this->attributes['orderby']; + } + } + + $query_args = array_merge( + $query_args, + WC()->query->get_catalog_ordering_args( $query_args['orderby'], $query_args['order'] ) + ); + } + + /** + * Set args specific to this block + * + * @param array $query_args Query args. + */ + abstract protected function set_block_query_args( &$query_args ); + + /** + * Set categories query args. + * + * @param array $query_args Query args. + */ + protected function set_categories_query_args( &$query_args ) { + if ( ! empty( $this->attributes['categories'] ) ) { + $categories = array_map( 'absint', $this->attributes['categories'] ); + + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_cat', + 'terms' => $categories, + 'field' => 'term_id', + 'operator' => 'all' === $this->attributes['catOperator'] ? 'AND' : 'IN', + + /* + * When cat_operator is AND, the children categories should be excluded, + * as only products belonging to all the children categories would be selected. + */ + 'include_children' => 'all' === $this->attributes['catOperator'] ? false : true, + ); + } + } + + /** + * Set visibility query args. + * + * @param array $query_args Query args. + */ + protected function set_visibility_query_args( &$query_args ) { + $product_visibility_terms = wc_get_product_visibility_term_ids(); + $product_visibility_not_in = array( $product_visibility_terms['exclude-from-catalog'] ); + + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $product_visibility_not_in[] = $product_visibility_terms['outofstock']; + } + + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => $product_visibility_not_in, + 'operator' => 'NOT IN', + ); + } + + /** + * Works out the item limit based on rows and columns, or returns default. + * + * @return int + */ + protected function get_products_limit() { + if ( isset( $this->attributes['rows'], $this->attributes['columns'] ) && ! empty( $this->attributes['rows'] ) ) { + $this->attributes['limit'] = intval( $this->attributes['columns'] ) * intval( $this->attributes['rows'] ); + } + return intval( $this->attributes['limit'] ); + } + + /** + * Run the query and return an array of product IDs + * + * @return array List of product IDs + */ + protected function get_products() { + $is_cacheable = (bool) apply_filters( 'woocommerce_blocks_product_grid_is_cacheable', true, $this->query_args ); + $transient_version = \WC_Cache_Helper::get_transient_version( 'product_query' ); + + $query = new BlocksWpQuery( $this->query_args ); + $results = wp_parse_id_list( $is_cacheable ? $query->get_cached_posts( $transient_version ) : $query->get_posts() ); + + // Remove ordering query arguments which may have been added by get_catalog_ordering_args. + WC()->query->remove_ordering_args(); + + // Prime caches to reduce future queries. + if ( is_callable( '_prime_post_caches' ) ) { + _prime_post_caches( $results ); + } + + return $results; + } + + /** + * Get the list of classes to apply to this block. + * + * @return string space-separated list of classes. + */ + protected function get_container_classes() { + $classes = array( + 'wc-block-grid', + "wp-block-{$this->block_name}", + "wc-block-{$this->block_name}", + "has-{$this->attributes['columns']}-columns", + ); + + if ( $this->attributes['rows'] > 1 ) { + $classes[] = 'has-multiple-rows'; + } + + if ( isset( $this->attributes['align'] ) ) { + $classes[] = "align{$this->attributes['align']}"; + } + + if ( ! empty( $this->attributes['alignButtons'] ) ) { + $classes[] = 'has-aligned-buttons'; + } + + if ( ! empty( $this->attributes['className'] ) ) { + $classes[] = $this->attributes['className']; + } + + return implode( ' ', $classes ); + } + + /** + * Render a single products. + * + * @param \WC_Product $product Product object. + * @return string Rendered product output. + */ + protected function render_product( $product ) { + $data = (object) array( + 'permalink' => esc_url( $product->get_permalink() ), + 'image' => $this->get_image_html( $product ), + 'title' => $this->get_title_html( $product ), + 'rating' => $this->get_rating_html( $product ), + 'price' => $this->get_price_html( $product ), + 'badge' => $this->get_sale_badge_html( $product ), + 'button' => $this->get_button_html( $product ), + ); + + return apply_filters( + 'woocommerce_blocks_product_grid_item_html', + "
  • + permalink}\" class=\"wc-block-grid__product-link\"> + {$data->image} + {$data->title} + + {$data->badge} + {$data->price} + {$data->rating} + {$data->button} +
  • ", + $data, + $product + ); + } + + /** + * Get the product image. + * + * @param \WC_Product $product Product. + * @return string + */ + protected function get_image_html( $product ) { + return '
    ' . $product->get_image( 'woocommerce_thumbnail' ) . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + /** + * Get the product title. + * + * @param \WC_Product $product Product. + * @return string + */ + protected function get_title_html( $product ) { + if ( empty( $this->attributes['contentVisibility']['title'] ) ) { + return ''; + } + return '
    ' . wp_kses_post( $product->get_title() ) . '
    '; + } + + /** + * Render the rating icons. + * + * @param WC_Product $product Product. + * @return string Rendered product output. + */ + protected function get_rating_html( $product ) { + if ( empty( $this->attributes['contentVisibility']['rating'] ) ) { + return ''; + } + $rating_count = $product->get_rating_count(); + $review_count = $product->get_review_count(); + $average = $product->get_average_rating(); + + if ( $rating_count > 0 ) { + return sprintf( + '
    %s
    ', + wc_get_rating_html( $average, $rating_count ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + ); + } + return ''; + } + + /** + * Get the price. + * + * @param \WC_Product $product Product. + * @return string Rendered product output. + */ + protected function get_price_html( $product ) { + if ( empty( $this->attributes['contentVisibility']['price'] ) ) { + return ''; + } + return sprintf( + '
    %s
    ', + wp_kses_post( $product->get_price_html() ) + ); + } + + /** + * Get the sale badge. + * + * @param \WC_Product $product Product. + * @return string Rendered product output. + */ + protected function get_sale_badge_html( $product ) { + if ( empty( $this->attributes['contentVisibility']['price'] ) ) { + return ''; + } + + if ( ! $product->is_on_sale() ) { + return; + } + + return '
    + + ' . esc_html__( 'Product on sale', 'woocommerce' ) . ' +
    '; + } + + /** + * Get the button. + * + * @param \WC_Product $product Product. + * @return string Rendered product output. + */ + protected function get_button_html( $product ) { + if ( empty( $this->attributes['contentVisibility']['button'] ) ) { + return ''; + } + return '
    ' . $this->get_add_to_cart( $product ) . '
    '; + } + + /** + * Get the "add to cart" button. + * + * @param \WC_Product $product Product. + * @return string Rendered product output. + */ + protected function get_add_to_cart( $product ) { + $attributes = array( + 'aria-label' => $product->add_to_cart_description(), + 'data-quantity' => '1', + 'data-product_id' => $product->get_id(), + 'data-product_sku' => $product->get_sku(), + 'rel' => 'nofollow', + 'class' => 'wp-block-button__link add_to_cart_button', + ); + + if ( + $product->supports( 'ajax_add_to_cart' ) && + $product->is_purchasable() && + ( $product->is_in_stock() || $product->backorders_allowed() ) + ) { + $attributes['class'] .= ' ajax_add_to_cart'; + } + + return sprintf( + '%s', + esc_url( $product->add_to_cart_url() ), + wc_implode_html_attributes( $attributes ), + esc_html( $product->add_to_cart_text() ) + ); + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + $this->asset_data_registry->add( 'min_columns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true ); + $this->asset_data_registry->add( 'max_columns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true ); + $this->asset_data_registry->add( 'default_columns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true ); + $this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true ); + $this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true ); + $this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php b/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php new file mode 100644 index 0000000..61473c2 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php @@ -0,0 +1,14 @@ +asset_data_registry->add( 'min_columns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true ); + $this->asset_data_registry->add( 'max_columns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true ); + $this->asset_data_registry->add( 'default_columns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true ); + $this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true ); + $this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true ); + $this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true ); + } + + /** + * Register script and style assets for the block type before it is registered. + * + * This registers the scripts; it does not enqueue them. + */ + protected function register_block_type_assets() { + parent::register_block_type_assets(); + $this->register_chunk_translations( + [ + 'atomic-block-components/price', + 'atomic-block-components/image', + 'atomic-block-components/title', + 'atomic-block-components/rating', + 'atomic-block-components/button', + 'atomic-block-components/summary', + 'atomic-block-components/sale-badge', + 'atomic-block-components/sku', + 'atomic-block-components/category-list', + 'atomic-block-components/tag-list', + 'atomic-block-components/stock-indicator', + 'atomic-block-components/add-to-cart', + ] + ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php b/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php new file mode 100644 index 0000000..d079405 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php @@ -0,0 +1,43 @@ +register_block_type() + * @param string $key Data to get, or default to everything. + * @return array|string + */ + protected function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'wc-reviews-block-frontend', + 'path' => $this->asset_api->get_block_asset_build_path( 'reviews-frontend' ), + 'dependencies' => [], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + $this->asset_data_registry->add( 'reviewRatingsEnabled', wc_review_ratings_enabled(), true ); + $this->asset_data_registry->add( 'showAvatars', '1' === get_option( 'show_avatars' ), true ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php b/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php new file mode 100644 index 0000000..5da0b72 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php @@ -0,0 +1,69 @@ +inject_html_data_attributes( $content, $attributes ); + } + + /** + * Get the editor script data for this block type. + * + * @param string $key Data to get, or default to everything. + * @return null + */ + protected function get_block_type_editor_script( $key = null ) { + return null; + } + + /** + * Get the editor style handle for this block type. + * + * @return null + */ + protected function get_block_type_editor_style() { + return null; + } + + /** + * Get the frontend script handle for this block type. + * + * @param string $key Data to get, or default to everything. + * @return null + */ + protected function get_block_type_script( $key = null ) { + return null; + } + + /** + * Get the frontend style handle for this block type. + * + * @return null + */ + protected function get_block_type_style() { + return null; + } + + /** + * Converts block attributes to HTML data attributes. + * + * @param array $attributes Key value pairs of attributes. + * @return string Rendered HTML attributes. + */ + protected function get_html_data_attributes( array $attributes ) { + $data = parent::get_html_data_attributes( $attributes ); + return trim( $data . ' data-block-name="' . esc_attr( $this->namespace . '/' . $this->block_name ) . '"' ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php b/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php new file mode 100644 index 0000000..ec2d0bf --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php @@ -0,0 +1,26 @@ +asset_data_registry->add( 'attributes', array_values( wc_get_attribute_taxonomies() ), true ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/Cart.php b/packages/woocommerce-blocks/src/BlockTypes/Cart.php new file mode 100644 index 0000000..2e6f0d0 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/Cart.php @@ -0,0 +1,246 @@ + 'wc-' . $this->block_name . '-block', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), + 'dependencies' => [ 'wc-blocks' ], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Get the frontend script handle for this block type. + * + * @see $this->register_block_type() + * @param string $key Data to get, or default to everything. + * @return array|string + */ + protected function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'wc-' . $this->block_name . '-block-frontend', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), + 'dependencies' => [], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Enqueue frontend assets for this block, just in time for rendering. + * + * @param array $attributes Any attributes that currently are available from the block. + */ + protected function enqueue_assets( array $attributes ) { + do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before' ); + parent::enqueue_assets( $attributes ); + do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after' ); + } + + /** + * Append frontend scripts when rendering the Cart block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + // Deregister core cart scripts and styles. + wp_dequeue_script( 'wc-cart' ); + wp_dequeue_script( 'wc-password-strength-meter' ); + wp_dequeue_script( 'selectWoo' ); + wp_dequeue_style( 'select2' ); + + return $this->inject_html_data_attributes( $content . $this->get_skeleton(), $attributes ); + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + + $this->asset_data_registry->add( + 'shippingCountries', + function() { + return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() ); + }, + true + ); + $this->asset_data_registry->add( + 'shippingStates', + function() { + return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() ); + }, + true + ); + $this->asset_data_registry->add( + 'countryLocale', + function() { + // Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944. + $country_locale = wc()->countries->get_country_locale(); + $states = wc()->countries->get_states(); + + foreach ( $states as $country => $states ) { + if ( empty( $states ) ) { + $country_locale[ $country ]['state']['required'] = false; + $country_locale[ $country ]['state']['hidden'] = true; + } + } + return $country_locale; + }, + true + ); + $this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true ); + $this->asset_data_registry->add( 'isShippingCalculatorEnabled', filter_var( get_option( 'woocommerce_enable_shipping_calc' ), FILTER_VALIDATE_BOOLEAN ), true ); + $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true ); + $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true ); + $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled(), true ); + $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled(), true ); + $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true ); + $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true ); + $this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 ); + + // Hydrate the following data depending on admin or frontend context. + if ( ! is_admin() && ! WC()->is_rest_api_request() ) { + $this->hydrate_from_api(); + } + + do_action( 'woocommerce_blocks_cart_enqueue_data' ); + } + + /** + * Removes accents from an array of values, sorts by the values, then returns the original array values sorted. + * + * @param array $array Array of values to sort. + * @return array Sorted array. + */ + protected function deep_sort_with_accents( $array ) { + if ( ! is_array( $array ) || empty( $array ) ) { + return $array; + } + + if ( is_array( reset( $array ) ) ) { + return array_map( [ $this, 'deep_sort_with_accents' ], $array ); + } + + $array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) ); + asort( $array_without_accents ); + return array_replace( $array_without_accents, $array ); + } + + /** + * Hydrate the cart block with data from the API. + */ + protected function hydrate_from_api() { + $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' ); + } + + /** + * Render skeleton markup for the cart block. + */ + protected function get_skeleton() { + return ' + + ' . $this->get_skeleton_inline_script(); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/CartI2.php b/packages/woocommerce-blocks/src/BlockTypes/CartI2.php new file mode 100644 index 0000000..076d15e --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/CartI2.php @@ -0,0 +1,165 @@ + 'wc-' . $this->block_name . '-block', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), + 'dependencies' => [ 'wc-blocks' ], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Get the frontend script handle for this block type. + * + * @see $this->register_block_type() + * @param string $key Data to get, or default to everything. + * @return array|string + */ + protected function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'wc-' . $this->block_name . '-block-frontend', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), + 'dependencies' => [], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Enqueue frontend assets for this block, just in time for rendering. + * + * @param array $attributes Any attributes that currently are available from the block. + */ + protected function enqueue_assets( array $attributes ) { + do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_before' ); + parent::enqueue_assets( $attributes ); + do_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after' ); + } + + /** + * Append frontend scripts when rendering the Cart block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + // Deregister core cart scripts and styles. + wp_dequeue_script( 'wc-cart' ); + wp_dequeue_script( 'wc-password-strength-meter' ); + wp_dequeue_script( 'selectWoo' ); + wp_dequeue_style( 'select2' ); + + return $this->inject_html_data_attributes( $content, $attributes ); + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + + $this->asset_data_registry->add( + 'shippingCountries', + function() { + return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() ); + }, + true + ); + $this->asset_data_registry->add( + 'shippingStates', + function() { + return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() ); + }, + true + ); + $this->asset_data_registry->add( + 'countryLocale', + function() { + // Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944. + $country_locale = wc()->countries->get_country_locale(); + $states = wc()->countries->get_states(); + + foreach ( $states as $country => $states ) { + if ( empty( $states ) ) { + $country_locale[ $country ]['state']['required'] = false; + $country_locale[ $country ]['state']['hidden'] = true; + } + } + return $country_locale; + }, + true + ); + $this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true ); + $this->asset_data_registry->add( 'isShippingCalculatorEnabled', filter_var( get_option( 'woocommerce_enable_shipping_calc' ), FILTER_VALIDATE_BOOLEAN ), true ); + $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true ); + $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true ); + $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled(), true ); + $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled(), true ); + $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true ); + $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true ); + $this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 ); + + // Hydrate the following data depending on admin or frontend context. + if ( ! is_admin() && ! WC()->is_rest_api_request() ) { + $this->hydrate_from_api(); + } + + do_action( 'woocommerce_blocks_cart_enqueue_data' ); + } + + /** + * Removes accents from an array of values, sorts by the values, then returns the original array values sorted. + * + * @param array $array Array of values to sort. + * @return array Sorted array. + */ + protected function deep_sort_with_accents( $array ) { + if ( ! is_array( $array ) || empty( $array ) ) { + return $array; + } + + if ( is_array( reset( $array ) ) ) { + return array_map( [ $this, 'deep_sort_with_accents' ], $array ); + } + + $array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) ); + asort( $array_without_accents ); + return array_replace( $array_without_accents, $array ); + } + + /** + * Hydrate the cart block with data from the API. + */ + protected function hydrate_from_api() { + $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/Checkout.php b/packages/woocommerce-blocks/src/BlockTypes/Checkout.php new file mode 100644 index 0000000..ef84342 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/Checkout.php @@ -0,0 +1,340 @@ + 'wc-' . $this->block_name . '-block', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), + 'dependencies' => [ 'wc-blocks' ], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Get the frontend script handle for this block type. + * + * @see $this->register_block_type() + * @param string $key Data to get, or default to everything. + * @return array|string + */ + protected function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'wc-' . $this->block_name . '-block-frontend', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), + 'dependencies' => [], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Enqueue frontend assets for this block, just in time for rendering. + * + * @param array $attributes Any attributes that currently are available from the block. + */ + protected function enqueue_assets( array $attributes ) { + do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' ); + parent::enqueue_assets( $attributes ); + do_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after' ); + } + + /** + * Append frontend scripts when rendering the block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + if ( $this->is_checkout_endpoint() ) { + // Note: Currently the block only takes care of the main checkout form -- if an endpoint is set, refer to the + // legacy shortcode instead and do not render block. + return '[woocommerce_checkout]'; + } + + // Deregister core checkout scripts and styles. + wp_dequeue_script( 'wc-checkout' ); + wp_dequeue_script( 'wc-password-strength-meter' ); + wp_dequeue_script( 'selectWoo' ); + wp_dequeue_style( 'select2' ); + + // If the content is empty, we may have transformed from an older checkout block. Insert the default list of blocks. + $regex_for_empty_block = '/
    <\/div>/mi'; + + $is_empty = preg_match( $regex_for_empty_block, $content ); + + if ( $is_empty ) { + $inner_blocks_html = '
    '; + + $content = str_replace( '
    ', $inner_blocks_html . '', $content ); + } + + return $this->inject_html_data_attributes( $content, $attributes ); + } + + /** + * Check if we're viewing a checkout page endpoint, rather than the main checkout page itself. + * + * @return boolean + */ + protected function is_checkout_endpoint() { + return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' ); + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + + $this->asset_data_registry->add( + 'allowedCountries', + function() { + return $this->deep_sort_with_accents( WC()->countries->get_allowed_countries() ); + }, + true + ); + $this->asset_data_registry->add( + 'allowedStates', + function() { + return $this->deep_sort_with_accents( WC()->countries->get_allowed_country_states() ); + }, + true + ); + $this->asset_data_registry->add( + 'shippingCountries', + function() { + return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() ); + }, + true + ); + $this->asset_data_registry->add( + 'shippingStates', + function() { + return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() ); + }, + true + ); + $this->asset_data_registry->add( + 'countryLocale', + function() { + // Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944. + $country_locale = wc()->countries->get_country_locale(); + $states = wc()->countries->get_states(); + + foreach ( $states as $country => $states ) { + if ( empty( $states ) ) { + $country_locale[ $country ]['state']['required'] = false; + $country_locale[ $country ]['state']['hidden'] = true; + } + } + return $country_locale; + }, + true + ); + $this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true ); + $this->asset_data_registry->add( + 'checkoutAllowsGuest', + false === filter_var( + WC()->checkout()->is_registration_required(), + FILTER_VALIDATE_BOOLEAN + ), + true + ); + $this->asset_data_registry->add( + 'checkoutAllowsSignup', + filter_var( + WC()->checkout()->is_registration_enabled(), + FILTER_VALIDATE_BOOLEAN + ), + true + ); + $this->asset_data_registry->add( 'checkoutShowLoginReminder', filter_var( get_option( 'woocommerce_enable_checkout_login_reminder' ), FILTER_VALIDATE_BOOLEAN ), true ); + $this->asset_data_registry->add( 'displayCartPricesIncludingTax', 'incl' === get_option( 'woocommerce_tax_display_cart' ), true ); + $this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true ); + $this->asset_data_registry->add( 'taxesEnabled', wc_tax_enabled(), true ); + $this->asset_data_registry->add( 'couponsEnabled', wc_coupons_enabled(), true ); + $this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true ); + $this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true ); + $this->asset_data_registry->register_page_id( isset( $attributes['cartPageId'] ) ? $attributes['cartPageId'] : 0 ); + + $is_block_editor = $this->is_block_editor(); + + // Hydrate the following data depending on admin or frontend context. + if ( $is_block_editor && ! $this->asset_data_registry->exists( 'shippingMethodsExist' ) ) { + $methods_exist = wc_get_shipping_method_count( false, true ) > 0; + $this->asset_data_registry->add( 'shippingMethodsExist', $methods_exist ); + } + + if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalShippingMethods' ) ) { + $shipping_methods = WC()->shipping()->get_shipping_methods(); + $formatted_shipping_methods = array_reduce( + $shipping_methods, + function( $acc, $method ) { + if ( $method->supports( 'settings' ) ) { + $acc[] = [ + 'id' => $method->id, + 'title' => $method->method_title, + 'description' => $method->method_description, + ]; + } + return $acc; + }, + [] + ); + $this->asset_data_registry->add( 'globalShippingMethods', $formatted_shipping_methods ); + } + + if ( $is_block_editor && ! $this->asset_data_registry->exists( 'activeShippingZones' ) && class_exists( '\WC_Shipping_Zones' ) ) { + $shipping_zones = \WC_Shipping_Zones::get_zones(); + $formatted_shipping_zones = array_reduce( + $shipping_zones, + function( $acc, $zone ) { + $acc[] = [ + 'id' => $zone['id'], + 'title' => $zone['zone_name'], + 'description' => $zone['formatted_zone_location'], + ]; + return $acc; + }, + [] + ); + $formatted_shipping_zones[] = [ + 'id' => 0, + 'title' => __( 'International', 'woocommerce' ), + 'description' => __( 'Locations outside all other zones', 'woocommerce' ), + ]; + $this->asset_data_registry->add( 'activeShippingZones', $formatted_shipping_zones ); + } + + if ( $is_block_editor && ! $this->asset_data_registry->exists( 'globalPaymentMethods' ) ) { + $payment_methods = WC()->payment_gateways->payment_gateways(); + $formatted_payment_methods = array_reduce( + $payment_methods, + function( $acc, $method ) { + if ( 'yes' === $method->enabled ) { + $acc[] = [ + 'id' => $method->id, + 'title' => $method->method_title, + 'description' => $method->method_description, + ]; + } + return $acc; + }, + [] + ); + $this->asset_data_registry->add( 'globalPaymentMethods', $formatted_payment_methods ); + } + + if ( ! is_admin() && ! WC()->is_rest_api_request() ) { + $this->hydrate_from_api(); + $this->hydrate_customer_payment_methods(); + } + + do_action( 'woocommerce_blocks_checkout_enqueue_data' ); + } + + /** + * Are we currently on the admin block editor screen? + */ + protected function is_block_editor() { + if ( ! is_admin() || ! function_exists( 'get_current_screen' ) ) { + return false; + } + $screen = get_current_screen(); + + return $screen && $screen->is_block_editor(); + } + + /** + * Removes accents from an array of values, sorts by the values, then returns the original array values sorted. + * + * @param array $array Array of values to sort. + * @return array Sorted array. + */ + protected function deep_sort_with_accents( $array ) { + if ( ! is_array( $array ) || empty( $array ) ) { + return $array; + } + + if ( is_array( reset( $array ) ) ) { + return array_map( [ $this, 'deep_sort_with_accents' ], $array ); + } + + $array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) ); + asort( $array_without_accents ); + return array_replace( $array_without_accents, $array ); + } + + /** + * Get customer payment methods for use in checkout. + */ + protected function hydrate_customer_payment_methods() { + if ( ! is_user_logged_in() || $this->asset_data_registry->exists( 'customerPaymentMethods' ) ) { + return; + } + add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 ); + $this->asset_data_registry->add( + 'customerPaymentMethods', + wc_get_customer_saved_methods_list( get_current_user_id() ) + ); + remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 ); + } + + /** + * Hydrate the checkout block with data from the API. + */ + protected function hydrate_from_api() { + // Print existing notices now, otherwise they are caught by the Cart + // Controller and converted to exceptions. + wc_print_notices(); + + add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); + $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' ); + $this->asset_data_registry->hydrate_api_request( '/wc/store/checkout' ); + remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' ); + } + + /** + * Callback for woocommerce_payment_methods_list_item filter to add token id + * to the generated list. + * + * @param array $list_item The current list item for the saved payment method. + * @param \WC_Token $token The token for the current list item. + * + * @return array The list item with the token id added. + */ + public static function include_token_id_with_payment_methods( $list_item, $token ) { + $list_item['tokenId'] = $token->get_id(); + $brand = ! empty( $list_item['method']['brand'] ) ? + strtolower( $list_item['method']['brand'] ) : + ''; + // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- need to match on translated value from core. + if ( ! empty( $brand ) && esc_html__( 'Credit card', 'woocommerce' ) !== $brand ) { + $list_item['method']['brand'] = wc_get_credit_card_type_label( $brand ); + } + return $list_item; + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php b/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php new file mode 100644 index 0000000..adfc78c --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php @@ -0,0 +1,181 @@ + 'none', + 'contentAlign' => 'center', + 'dimRatio' => 50, + 'focalPoint' => false, + 'height' => false, + 'mediaId' => 0, + 'mediaSrc' => '', + 'showDesc' => true, + ); + + /** + * Render the Featured Category block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + $id = absint( isset( $attributes['categoryId'] ) ? $attributes['categoryId'] : 0 ); + $category = get_term( $id, 'product_cat' ); + + if ( ! $category || is_wp_error( $category ) ) { + return ''; + } + + $attributes = wp_parse_args( $attributes, $this->defaults ); + + $attributes['height'] = $attributes['height'] ? $attributes['height'] : wc_get_theme_support( 'featured_block::default_height', 500 ); + + $title = sprintf( + '', + wp_kses_post( $category->name ) + ); + + $desc_str = sprintf( + '', + wc_format_content( wp_kses_post( $category->description ) ) + ); + + $output = sprintf( '
    ', esc_attr( $this->get_classes( $attributes ) ), esc_attr( $this->get_styles( $attributes, $category ) ) ); + $output .= ''; + $output .= '
    '; + return $output; + } + + /** + * Get the styles for the wrapper element (background image, color). + * + * @param array $attributes Block attributes. Default empty array. + * @param \WP_Term $category Term object. + * @return string + */ + public function get_styles( $attributes, $category ) { + $style = ''; + $image_size = 'large'; + if ( 'none' !== $attributes['align'] || $attributes['height'] > 800 ) { + $image_size = 'full'; + } + + if ( $attributes['mediaId'] ) { + $image = wp_get_attachment_image_url( $attributes['mediaId'], $image_size ); + } else { + $image = $this->get_image( $category, $image_size ); + } + + if ( ! empty( $image ) ) { + $style .= sprintf( 'background-image:url(%s);', esc_url( $image ) ); + } + + if ( isset( $attributes['customOverlayColor'] ) ) { + $style .= sprintf( 'background-color:%s;', esc_attr( $attributes['customOverlayColor'] ) ); + } + + if ( isset( $attributes['height'] ) ) { + $style .= sprintf( 'min-height:%dpx;', intval( $attributes['height'] ) ); + } + + if ( is_array( $attributes['focalPoint'] ) && 2 === count( $attributes['focalPoint'] ) ) { + $style .= sprintf( + 'background-position: %s%% %s%%', + $attributes['focalPoint']['x'] * 100, + $attributes['focalPoint']['y'] * 100 + ); + } + + return $style; + } + + /** + * Get class names for the block container. + * + * @param array $attributes Block attributes. Default empty array. + * @return string + */ + public function get_classes( $attributes ) { + $classes = array( 'wc-block-' . $this->block_name ); + + if ( isset( $attributes['align'] ) ) { + $classes[] = "align{$attributes['align']}"; + } + + if ( isset( $attributes['dimRatio'] ) && ( 0 !== $attributes['dimRatio'] ) ) { + $classes[] = 'has-background-dim'; + + if ( 50 !== $attributes['dimRatio'] ) { + $classes[] = 'has-background-dim-' . 10 * round( $attributes['dimRatio'] / 10 ); + } + } + + if ( isset( $attributes['contentAlign'] ) && 'center' !== $attributes['contentAlign'] ) { + $classes[] = "has-{$attributes['contentAlign']}-content"; + } + + if ( isset( $attributes['overlayColor'] ) ) { + $classes[] = "has-{$attributes['overlayColor']}-background-color"; + } + + if ( isset( $attributes['className'] ) ) { + $classes[] = $attributes['className']; + } + + return implode( ' ', $classes ); + } + + /** + * Returns the main product image URL. + * + * @param \WP_Term $category Term object. + * @param string $size Image size, defaults to 'full'. + * @return string + */ + public function get_image( $category, $size = 'full' ) { + $image = ''; + $image_id = get_term_meta( $category->term_id, 'thumbnail_id', true ); + + if ( $image_id ) { + $image = wp_get_attachment_image_url( $image_id, $size ); + } + + return $image; + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + $this->asset_data_registry->add( 'min_height', wc_get_theme_support( 'featured_block::min_height', 500 ), true ); + $this->asset_data_registry->add( 'default_height', wc_get_theme_support( 'featured_block::default_height', 500 ), true ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php b/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php new file mode 100644 index 0000000..fda5df8 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php @@ -0,0 +1,199 @@ + 'none', + 'contentAlign' => 'center', + 'dimRatio' => 50, + 'focalPoint' => false, + 'height' => false, + 'mediaId' => 0, + 'mediaSrc' => '', + 'showDesc' => true, + 'showPrice' => true, + ); + + /** + * Render the Featured Product block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + $id = absint( isset( $attributes['productId'] ) ? $attributes['productId'] : 0 ); + $product = wc_get_product( $id ); + if ( ! $product ) { + return ''; + } + $attributes = wp_parse_args( $attributes, $this->defaults ); + + $attributes['height'] = $attributes['height'] ? $attributes['height'] : wc_get_theme_support( 'featured_block::default_height', 500 ); + + $title = sprintf( + '', + wp_kses_post( $product->get_title() ) + ); + + if ( $product->is_type( 'variation' ) ) { + $title .= sprintf( + '', + wp_kses_post( wc_get_formatted_variation( $product, true, true, false ) ) + ); + } + + $desc_str = sprintf( + '', + wc_format_content( wp_kses_post( $product->get_short_description() ? $product->get_short_description() : wc_trim_string( $product->get_description(), 400 ) ) ) + ); + + $price_str = sprintf( + '', + wp_kses_post( $product->get_price_html() ) + ); + + $output = sprintf( '
    ', esc_attr( $this->get_classes( $attributes ) ), esc_attr( $this->get_styles( $attributes, $product ) ) ); + $output .= ''; + $output .= '
    '; + + return $output; + } + + /** + * Get the styles for the wrapper element (background image, color). + * + * @param array $attributes Block attributes. Default empty array. + * @param \WC_Product $product Product object. + * @return string + */ + public function get_styles( $attributes, $product ) { + $style = ''; + $image_size = 'large'; + if ( 'none' !== $attributes['align'] || $attributes['height'] > 800 ) { + $image_size = 'full'; + } + + if ( $attributes['mediaId'] ) { + $image = wp_get_attachment_image_url( $attributes['mediaId'], $image_size ); + } else { + $image = $this->get_image( $product, $image_size ); + } + + if ( ! empty( $image ) ) { + $style .= sprintf( 'background-image:url(%s);', esc_url( $image ) ); + } + + if ( isset( $attributes['customOverlayColor'] ) ) { + $style .= sprintf( 'background-color:%s;', esc_attr( $attributes['customOverlayColor'] ) ); + } + + if ( isset( $attributes['height'] ) ) { + $style .= sprintf( 'min-height:%dpx;', intval( $attributes['height'] ) ); + } + + if ( is_array( $attributes['focalPoint'] ) && 2 === count( $attributes['focalPoint'] ) ) { + $style .= sprintf( + 'background-position: %s%% %s%%', + $attributes['focalPoint']['x'] * 100, + $attributes['focalPoint']['y'] * 100 + ); + } + + return $style; + } + + /** + * Get class names for the block container. + * + * @param array $attributes Block attributes. Default empty array. + * @return string + */ + public function get_classes( $attributes ) { + $classes = array( 'wc-block-' . $this->block_name ); + + if ( isset( $attributes['align'] ) ) { + $classes[] = "align{$attributes['align']}"; + } + + if ( isset( $attributes['dimRatio'] ) && ( 0 !== $attributes['dimRatio'] ) ) { + $classes[] = 'has-background-dim'; + + if ( 50 !== $attributes['dimRatio'] ) { + $classes[] = 'has-background-dim-' . 10 * round( $attributes['dimRatio'] / 10 ); + } + } + + if ( isset( $attributes['contentAlign'] ) && 'center' !== $attributes['contentAlign'] ) { + $classes[] = "has-{$attributes['contentAlign']}-content"; + } + + if ( isset( $attributes['overlayColor'] ) ) { + $classes[] = "has-{$attributes['overlayColor']}-background-color"; + } + + if ( isset( $attributes['className'] ) ) { + $classes[] = $attributes['className']; + } + + return implode( ' ', $classes ); + } + + /** + * Returns the main product image URL. + * + * @param \WC_Product $product Product object. + * @param string $size Image size, defaults to 'full'. + * @return string + */ + public function get_image( $product, $size = 'full' ) { + $image = ''; + if ( $product->get_image_id() ) { + $image = wp_get_attachment_image_url( $product->get_image_id(), $size ); + } elseif ( $product->get_parent_id() ) { + $parent_product = wc_get_product( $product->get_parent_id() ); + if ( $parent_product ) { + $image = wp_get_attachment_image_url( $parent_product->get_image_id(), $size ); + } + } + + return $image; + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + $this->asset_data_registry->add( 'min_height', wc_get_theme_support( 'featured_block::min_height', 500 ), true ); + $this->asset_data_registry->add( 'default_height', wc_get_theme_support( 'featured_block::default_height', 500 ), true ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php b/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php new file mode 100644 index 0000000..24193ff --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php @@ -0,0 +1,62 @@ +attributes['products'] ); + + $query_args['post__in'] = $ids; + $query_args['posts_per_page'] = count( $ids ); + } + + /** + * Set visibility query args. Handpicked products will show hidden products if chosen. + * + * @param array $query_args Query args. + */ + protected function set_visibility_query_args( &$query_args ) { + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $product_visibility_terms = wc_get_product_visibility_term_ids(); + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'term_taxonomy_id', + 'terms' => array( $product_visibility_terms['outofstock'] ), + 'operator' => 'NOT IN', + ); + } + } + + /** + * Get block attributes. + * + * @return array + */ + protected function get_block_type_attributes() { + return array( + 'align' => $this->get_schema_align(), + 'alignButtons' => $this->get_schema_boolean( false ), + 'className' => $this->get_schema_string(), + 'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ), + 'editMode' => $this->get_schema_boolean( true ), + 'orderby' => $this->get_schema_orderby(), + 'products' => $this->get_schema_list_ids(), + 'contentVisibility' => $this->get_schema_content_visibility(), + 'isPreview' => $this->get_schema_boolean( false ), + ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php b/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php new file mode 100644 index 0000000..655bba8 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php @@ -0,0 +1,313 @@ + 'wc-' . $this->block_name . '-block', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), + 'dependencies' => [ 'wc-blocks' ], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Get the frontend script handle for this block type. + * + * @see $this->register_block_type() + * @param string $key Data to get, or default to everything. + * @return array|string + */ + protected function get_block_type_script( $key = null ) { + if ( is_cart() || is_checkout() ) { + return; + } + + $script = [ + 'handle' => 'wc-' . $this->block_name . '-block-frontend', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-frontend' ), + 'dependencies' => [], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + if ( is_cart() || is_checkout() ) { + return; + } + + parent::enqueue_data( $attributes ); + + // Hydrate the following data depending on admin or frontend context. + if ( ! is_admin() && ! WC()->is_rest_api_request() ) { + $this->hydrate_from_api(); + } + + $script_data = $this->asset_api->get_script_data( 'build/mini-cart-component-frontend.js' ); + + $num_dependencies = count( $script_data['dependencies'] ); + $wp_scripts = wp_scripts(); + + for ( $i = 0; $i < $num_dependencies; $i++ ) { + $dependency = $script_data['dependencies'][ $i ]; + + foreach ( $wp_scripts->registered as $script ) { + if ( $script->handle === $dependency ) { + $this->append_script_and_deps_src( $script ); + break; + } + } + } + + $this->scripts_to_lazy_load['wc-block-mini-cart-component-frontend'] = array( + 'src' => $script_data['src'], + 'version' => $script_data['version'], + ); + + $this->asset_data_registry->add( + 'mini_cart_block_frontend_dependencies', + $this->scripts_to_lazy_load, + true + ); + + $this->asset_data_registry->add( + 'displayCartPricesIncludingTax', + 'incl' === get_option( 'woocommerce_tax_display_cart' ), + true + ); + + do_action( 'woocommerce_blocks_cart_enqueue_data' ); + } + + /** + * Hydrate the cart block with data from the API. + */ + protected function hydrate_from_api() { + $this->asset_data_registry->hydrate_api_request( '/wc/store/cart' ); + } + + /** + * Returns the script data given its handle. + * + * @param string $handle Handle of the script. + * + * @return array Array containing the script data. + */ + protected function get_script_from_handle( $handle ) { + $wp_scripts = wp_scripts(); + foreach ( $wp_scripts->registered as $script ) { + if ( $script->handle === $handle ) { + return $script; + } + } + + return ''; + } + + /** + * Recursively appends a scripts and its dependencies into the + * scripts_to_lazy_load array. + * + * @param string $script Array containing script data. + */ + protected function append_script_and_deps_src( $script ) { + $wp_scripts = wp_scripts(); + // This script and its dependencies have already been appended. + if ( array_key_exists( $script->handle, $this->scripts_to_lazy_load ) ) { + return; + } + + if ( count( $script->deps ) > 0 ) { + foreach ( $script->deps as $dep ) { + if ( ! array_key_exists( $dep, $this->scripts_to_lazy_load ) ) { + $dep_script = $this->get_script_from_handle( $dep ); + $this->append_script_and_deps_src( $dep_script ); + } + } + } + $this->scripts_to_lazy_load[ $script->handle ] = array( + 'src' => $script->src, + 'version' => $script->ver, + 'before' => $wp_scripts->print_inline_script( $script->handle, 'before', false ), + 'after' => $wp_scripts->print_inline_script( $script->handle, 'after', false ), + 'translations' => $wp_scripts->print_translations( $script->handle, false ), + ); + } + + /** + * Append frontend scripts when rendering the Mini Cart block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + return $this->inject_html_data_attributes( $content . $this->get_markup(), $attributes ); + } + + /** + * Render the markup for the Mini Cart block. + * + * @return string The HTML markup. + */ + protected function get_markup() { + if ( is_admin() || WC()->is_rest_api_request() ) { + // In the editor we will display the placeholder, so no need to load + // real cart data and to print the markup. + return ''; + } + $cart_controller = new CartController(); + $cart = $cart_controller->get_cart_instance(); + $cart_contents_count = $cart->get_cart_contents_count(); + $cart_contents = $cart->get_cart(); + $cart_contents_total = $cart->get_subtotal(); + + if ( $cart->display_prices_including_tax() ) { + $cart_contents_total += $cart->get_subtotal_tax(); + } + + $button_text = sprintf( + /* translators: %d is the number of products in the cart. */ + _n( + '%d item', + '%d items', + $cart_contents_count, + 'woocommerce' + ), + $cart_contents_count + ); + $aria_label = sprintf( + /* translators: %1$d is the number of products in the cart. %2$s is the cart total */ + _n( + '%1$d item in cart, total price of %2$s', + '%1$d items in cart, total price of %2$s', + $cart_contents_count, + 'woocommerce' + ), + $cart_contents_count, + wp_strip_all_tags( wc_price( $cart_contents_total ) ) + ); + $title = sprintf( + /* translators: %d is the count of items in the cart. */ + _n( + 'Your cart (%d item)', + 'Your cart (%d items)', + $cart_contents_count, + 'woocommerce' + ), + $cart_contents_count + ); + + if ( is_cart() || is_checkout() ) { + return '
    + +
    '; + } + + return '
    + + +
    '; + } + + /** + * Render the markup of the Cart contents. + * + * @param array $cart_contents Array of contents in the cart. + * + * @return string The HTML markup. + */ + protected function get_cart_contents_markup( $cart_contents ) { + // Force mobile styles. + return ' + + + + + + + + ' . implode( array_map( array( $this, 'get_cart_item_markup' ), $cart_contents ) ) . ' +
    '; + } + + /** + * Render the skeleton of a Cart item. + * + * @return string The skeleton HTML markup. + */ + protected function get_cart_item_markup() { + return ' + + + + +
    +
    + +
    +
    + + + +
    + +
    + + +
    +
    +
    + + '; + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php b/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php new file mode 100644 index 0000000..0c9ab50 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php @@ -0,0 +1,15 @@ + true, + 'hasImage' => false, + 'hasEmpty' => false, + 'isDropdown' => false, + 'isHierarchical' => true, + ); + + /** + * Get block attributes. + * + * @return array + */ + protected function get_block_type_attributes() { + return array_merge( + parent::get_block_type_attributes(), + array( + 'align' => $this->get_schema_align(), + 'className' => $this->get_schema_string(), + 'hasCount' => $this->get_schema_boolean( true ), + 'hasImage' => $this->get_schema_boolean( false ), + 'hasEmpty' => $this->get_schema_boolean( false ), + 'isDropdown' => $this->get_schema_boolean( false ), + 'isHierarchical' => $this->get_schema_boolean( true ), + ) + ); + } + + /** + * Render the Product Categories List block. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + $uid = uniqid( 'product-categories-' ); + $categories = $this->get_categories( $attributes ); + + if ( empty( $categories ) ) { + return ''; + } + + if ( ! empty( $content ) ) { + // Deal with legacy attributes (before this was an SSR block) that differ from defaults. + if ( strstr( $content, 'data-has-count="false"' ) ) { + $attributes['hasCount'] = false; + } + if ( strstr( $content, 'data-is-dropdown="true"' ) ) { + $attributes['isDropdown'] = true; + } + if ( strstr( $content, 'data-is-hierarchical="false"' ) ) { + $attributes['isHierarchical'] = false; + } + if ( strstr( $content, 'data-has-empty="true"' ) ) { + $attributes['hasEmpty'] = true; + } + } + + $classes = $this->get_container_classes( $attributes ); + + $output = '
    '; + $output .= ! empty( $attributes['isDropdown'] ) ? $this->renderDropdown( $categories, $attributes, $uid ) : $this->renderList( $categories, $attributes, $uid ); + $output .= '
    '; + + return $output; + } + + /** + * Get the list of classes to apply to this block. + * + * @param array $attributes Block attributes. Default empty array. + * @return string space-separated list of classes. + */ + protected function get_container_classes( $attributes = array() ) { + $classes = array( 'wc-block-product-categories' ); + + if ( isset( $attributes['align'] ) ) { + $classes[] = "align{$attributes['align']}"; + } + + if ( ! empty( $attributes['className'] ) ) { + $classes[] = $attributes['className']; + } + + if ( $attributes['isDropdown'] ) { + $classes[] = 'is-dropdown'; + } else { + $classes[] = 'is-list'; + } + + return implode( ' ', $classes ); + } + + /** + * Get categories (terms) from the db. + * + * @param array $attributes Block attributes. Default empty array. + * @return array + */ + protected function get_categories( $attributes ) { + $hierarchical = wc_string_to_bool( $attributes['isHierarchical'] ); + $categories = get_terms( + 'product_cat', + [ + 'hide_empty' => ! $attributes['hasEmpty'], + 'pad_counts' => true, + 'hierarchical' => true, + ] + ); + + if ( ! is_array( $categories ) || empty( $categories ) ) { + return []; + } + + // This ensures that no categories with a product count of 0 is rendered. + if ( ! $attributes['hasEmpty'] ) { + $categories = array_filter( + $categories, + function( $category ) { + return 0 !== $category->count; + } + ); + } + + return $hierarchical ? $this->build_category_tree( $categories ) : $categories; + } + + /** + * Build hierarchical tree of categories. + * + * @param array $categories List of terms. + * @return array + */ + protected function build_category_tree( $categories ) { + $categories_by_parent = []; + + foreach ( $categories as $category ) { + if ( ! isset( $categories_by_parent[ 'cat-' . $category->parent ] ) ) { + $categories_by_parent[ 'cat-' . $category->parent ] = []; + } + $categories_by_parent[ 'cat-' . $category->parent ][] = $category; + } + + $tree = $categories_by_parent['cat-0']; + unset( $categories_by_parent['cat-0'] ); + + foreach ( $tree as $category ) { + if ( ! empty( $categories_by_parent[ 'cat-' . $category->term_id ] ) ) { + $category->children = $this->fill_category_children( $categories_by_parent[ 'cat-' . $category->term_id ], $categories_by_parent ); + } + } + + return $tree; + } + + /** + * Build hierarchical tree of categories by appending children in the tree. + * + * @param array $categories List of terms. + * @param array $categories_by_parent List of terms grouped by parent. + * @return array + */ + protected function fill_category_children( $categories, $categories_by_parent ) { + foreach ( $categories as $category ) { + if ( ! empty( $categories_by_parent[ 'cat-' . $category->term_id ] ) ) { + $category->children = $this->fill_category_children( $categories_by_parent[ 'cat-' . $category->term_id ], $categories_by_parent ); + } + } + return $categories; + } + + /** + * Render the category list as a dropdown. + * + * @param array $categories List of terms. + * @param array $attributes Block attributes. Default empty array. + * @param int $uid Unique ID for the rendered block, used for HTML IDs. + * @return string Rendered output. + */ + protected function renderDropdown( $categories, $attributes, $uid ) { + $aria_label = empty( $attributes['hasCount'] ) ? + __( 'List of categories', 'woocommerce' ) : + __( 'List of categories with their product counts', 'woocommerce' ); + + $output = ' +
    + + +
    + + '; + return $output; + } + + /** + * Render dropdown options list. + * + * @param array $categories List of terms. + * @param array $attributes Block attributes. Default empty array. + * @param int $uid Unique ID for the rendered block, used for HTML IDs. + * @param int $depth Current depth. + * @return string Rendered output. + */ + protected function renderDropdownOptions( $categories, $attributes, $uid, $depth = 0 ) { + $output = ''; + + foreach ( $categories as $category ) { + $output .= ' + + ' . ( ! empty( $category->children ) ? $this->renderDropdownOptions( $category->children, $attributes, $uid, $depth + 1 ) : '' ) . ' + '; + } + + return $output; + } + + /** + * Render the category list as a list. + * + * @param array $categories List of terms. + * @param array $attributes Block attributes. Default empty array. + * @param int $uid Unique ID for the rendered block, used for HTML IDs. + * @param int $depth Current depth. + * @return string Rendered output. + */ + protected function renderList( $categories, $attributes, $uid, $depth = 0 ) { + $classes = [ + 'wc-block-product-categories-list', + 'wc-block-product-categories-list--depth-' . absint( $depth ), + ]; + if ( ! empty( $attributes['hasImage'] ) ) { + $classes[] = 'wc-block-product-categories-list--has-images'; + } + $output = '
      ' . $this->renderListItems( $categories, $attributes, $uid, $depth ) . '
    '; + + return $output; + } + + /** + * Render a list of terms. + * + * @param array $categories List of terms. + * @param array $attributes Block attributes. Default empty array. + * @param int $uid Unique ID for the rendered block, used for HTML IDs. + * @param int $depth Current depth. + * @return string Rendered output. + */ + protected function renderListItems( $categories, $attributes, $uid, $depth = 0 ) { + $output = ''; + + foreach ( $categories as $category ) { + $output .= ' +
  • + ' . $this->get_image_html( $category, $attributes ) . esc_html( $category->name ) . ' + ' . $this->getCount( $category, $attributes ) . ' + ' . ( ! empty( $category->children ) ? $this->renderList( $category->children, $attributes, $uid, $depth + 1 ) : '' ) . ' +
  • + '; + } + + return preg_replace( '/\r|\n/', '', $output ); + } + + /** + * Returns the category image html + * + * @param \WP_Term $category Term object. + * @param array $attributes Block attributes. Default empty array. + * @param string $size Image size, defaults to 'woocommerce_thumbnail'. + * @return string + */ + public function get_image_html( $category, $attributes, $size = 'woocommerce_thumbnail' ) { + if ( empty( $attributes['hasImage'] ) ) { + return ''; + } + + $image_id = get_term_meta( $category->term_id, 'thumbnail_id', true ); + + if ( ! $image_id ) { + return '' . wc_placeholder_img( 'woocommerce_thumbnail' ) . ''; + } + + return '' . wp_get_attachment_image( $image_id, 'woocommerce_thumbnail' ) . ''; + } + + /** + * Get the count, if displaying. + * + * @param object $category Term object. + * @param array $attributes Block attributes. Default empty array. + * @return string + */ + protected function getCount( $category, $attributes ) { + if ( empty( $attributes['hasCount'] ) ) { + return ''; + } + + if ( $attributes['isDropdown'] ) { + return '(' . absint( $category->count ) . ')'; + } + + $screen_reader_text = sprintf( + /* translators: %s number of products in cart. */ + _n( '%d product', '%d products', absint( $category->count ), 'woocommerce' ), + absint( $category->count ) + ); + + return '' + . '' + . '' . esc_html( $screen_reader_text ) . '' + . ''; + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php b/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php new file mode 100644 index 0000000..f0ae248 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php @@ -0,0 +1,37 @@ + $this->get_schema_string(), + 'orderby' => $this->get_schema_orderby(), + 'editMode' => $this->get_schema_boolean( true ), + ) + ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php b/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php new file mode 100644 index 0000000..d2ac9ac --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php @@ -0,0 +1,25 @@ + $this->get_schema_string(), + 'orderby' => $this->get_schema_orderby(), + ) + ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php b/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php new file mode 100644 index 0000000..b28a6d8 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php @@ -0,0 +1,127 @@ + true, + 'align' => '', + 'className' => '', + 'label' => __( 'Search', 'woocommerce' ), + 'placeholder' => __( 'Search products…', 'woocommerce' ), + ) + ); + + /** + * Product Search event. + * + * Listens for product search form submission, and on submission fires a WP Hook named + * `experimental__woocommerce_blocks-product-search`. This can be used by tracking extensions such as Google + * Analytics to track searches. + */ + $this->asset_api->add_inline_script( + 'wp-hooks', + " + window.addEventListener( 'DOMContentLoaded', () => { + const forms = document.querySelectorAll( '.wc-block-product-search form' ); + + for ( const form of forms ) { + form.addEventListener( 'submit', ( event ) => { + const field = form.querySelector( '.wc-block-product-search__field' ); + + if ( field && field.value ) { + wp.hooks.doAction( 'experimental__woocommerce_blocks-product-search', { event: event, searchTerm: field.value } ); + } + } ); + } + } ); + ", + 'after' + ); + + $input_id = 'wc-block-search__input-' . ( ++$instance_id ); + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'class' => implode( + ' ', + array_filter( + [ + 'wc-block-product-search', + $attributes['align'] ? 'align' . $attributes['align'] : '', + ] + ) + ), + ) + ); + + $label_markup = $attributes['hasLabel'] ? sprintf( + '', + esc_attr( $input_id ), + esc_html( $attributes['label'] ) + ) : sprintf( + '', + esc_attr( $input_id ), + esc_html( $attributes['label'] ) + ); + + $input_markup = sprintf( + '', + esc_attr( $input_id ), + esc_attr( $attributes['placeholder'] ) + ); + $button_markup = sprintf( + '', + esc_attr__( 'Search', 'woocommerce' ) + ); + + $field_markup = ' +
    + ' . $input_markup . $button_markup . ' + +
    + '; + + return sprintf( + '
    %s
    ', + $wrapper_attributes, + esc_url( home_url( '/' ) ), + $label_markup . $field_markup + ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php b/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php new file mode 100644 index 0000000..f4be1af --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php @@ -0,0 +1,69 @@ +attributes['tags'] ) ) { + $query_args['tax_query'][] = array( + 'taxonomy' => 'product_tag', + 'terms' => array_map( 'absint', $this->attributes['tags'] ), + 'field' => 'id', + 'operator' => isset( $this->attributes['tagOperator'] ) && 'any' === $this->attributes['tagOperator'] ? 'IN' : 'AND', + ); + } + } + + /** + * Get block attributes. + * + * @return array + */ + protected function get_block_type_attributes() { + return array( + 'className' => $this->get_schema_string(), + 'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ), + 'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ), + 'contentVisibility' => $this->get_schema_content_visibility(), + 'align' => $this->get_schema_align(), + 'alignButtons' => $this->get_schema_boolean( false ), + 'orderby' => $this->get_schema_orderby(), + 'tags' => $this->get_schema_list_ids(), + 'tagOperator' => array( + 'type' => 'string', + 'default' => 'any', + ), + 'isPreview' => $this->get_schema_boolean( false ), + ); + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + + $tag_count = wp_count_terms( 'product_tag' ); + + $this->asset_data_registry->add( 'hasTags', $tag_count > 0, true ); + $this->asset_data_registry->add( 'limitTags', $tag_count > 100, true ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php b/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php new file mode 100644 index 0000000..db3cfbc --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php @@ -0,0 +1,24 @@ +attributes['attributes'] ) ) { + $taxonomy = sanitize_title( $this->attributes['attributes'][0]['attr_slug'] ); + $terms = wp_list_pluck( $this->attributes['attributes'], 'id' ); + + $query_args['tax_query'][] = array( + 'taxonomy' => $taxonomy, + 'terms' => array_map( 'absint', $terms ), + 'field' => 'term_id', + 'operator' => 'all' === $this->attributes['attrOperator'] ? 'AND' : 'IN', + ); + } + } + + /** + * Get block attributes. + * + * @return array + */ + protected function get_block_type_attributes() { + return array( + 'align' => $this->get_schema_align(), + 'alignButtons' => $this->get_schema_boolean( false ), + 'attributes' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'number', + ), + 'attr_slug' => array( + 'type' => 'string', + ), + ), + ), + 'default' => array(), + ), + 'attrOperator' => array( + 'type' => 'string', + 'default' => 'any', + ), + 'className' => $this->get_schema_string(), + 'columns' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_columns', 3 ) ), + 'contentVisibility' => $this->get_schema_content_visibility(), + 'editMode' => $this->get_schema_boolean( true ), + 'orderby' => $this->get_schema_orderby(), + 'rows' => $this->get_schema_number( wc_get_theme_support( 'product_blocks::default_rows', 3 ) ), + 'isPreview' => $this->get_schema_boolean( false ), + ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php b/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php new file mode 100644 index 0000000..1189e22 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php @@ -0,0 +1,43 @@ +register_block_type() + * @param string $key Data to get, or default to everything. + * @return array|string + */ + protected function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'wc-reviews-block-frontend', + 'path' => $this->asset_api->get_block_asset_build_path( 'reviews-frontend' ), + 'dependencies' => [], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + $this->asset_data_registry->add( 'reviewRatingsEnabled', wc_review_ratings_enabled(), true ); + $this->asset_data_registry->add( 'showAvatars', '1' === get_option( 'show_avatars' ), true ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php b/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php new file mode 100644 index 0000000..6053c40 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php @@ -0,0 +1,43 @@ +register_block_type() + * @param string $key Data to get, or default to everything. + * @return array|string + */ + protected function get_block_type_script( $key = null ) { + $script = [ + 'handle' => 'wc-reviews-block-frontend', + 'path' => $this->asset_api->get_block_asset_build_path( 'reviews-frontend' ), + 'dependencies' => [], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Extra data passed through from server to client for block. + * + * @param array $attributes Any attributes that currently are available from the block. + * Note, this will be empty in the editor context when the block is + * not in the post content on editor load. + */ + protected function enqueue_data( array $attributes = [] ) { + parent::enqueue_data( $attributes ); + $this->asset_data_registry->add( 'reviewRatingsEnabled', wc_review_ratings_enabled(), true ); + $this->asset_data_registry->add( 'showAvatars', '1' === get_option( 'show_avatars' ), true ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php b/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php new file mode 100644 index 0000000..8a4f760 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php @@ -0,0 +1,40 @@ + 'wc-' . $this->block_name . '-block', + 'path' => $this->asset_api->get_block_asset_build_path( $this->block_name ), + 'dependencies' => [ 'wc-blocks' ], + ]; + return $key ? $script[ $key ] : $script; + } + + /** + * Render the block on the frontend. + * + * @param array $attributes Block attributes. + * @param string $content Block content. + * @return string Rendered block type output. + */ + protected function render( $attributes, $content ) { + return $this->inject_html_data_attributes( $content, $attributes ); + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php b/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php new file mode 100644 index 0000000..8fc29e1 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php @@ -0,0 +1,28 @@ +asset_data_registry->add( 'stockStatusOptions', wc_get_product_stock_status_options(), true ); + $this->asset_data_registry->add( 'hideOutOfStockItems', 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ), true ); + + } +} diff --git a/packages/woocommerce-blocks/src/BlockTypesController.php b/packages/woocommerce-blocks/src/BlockTypesController.php new file mode 100644 index 0000000..df11595 --- /dev/null +++ b/packages/woocommerce-blocks/src/BlockTypesController.php @@ -0,0 +1,228 @@ +asset_api = $asset_api; + $this->asset_data_registry = $asset_data_registry; + $this->init(); + } + + /** + * Initialize class features. + */ + protected function init() { + add_action( 'init', array( $this, 'register_blocks' ) ); + add_filter( 'render_block', array( $this, 'add_data_attributes' ), 10, 2 ); + add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) ); + add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) ); + } + + /** + * Register blocks, hooking up assets and render functions as needed. + */ + public function register_blocks() { + $block_types = $this->get_block_types(); + + foreach ( $block_types as $block_type ) { + $block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type; + $block_type_instance = new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() ); + } + + foreach ( self::get_atomic_blocks() as $block_type ) { + $block_type_instance = new AtomicBlock( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry(), $block_type ); + } + } + + /** + * Add data- attributes to blocks when rendered if the block is under the woocommerce/ namespace. + * + * @param string $content Block content. + * @param array $block Parsed block data. + * @return string + */ + public function add_data_attributes( $content, $block ) { + $block_name = $block['blockName']; + $block_namespace = strtok( $block_name, '/' ); + + /** + * WooCommerce Blocks Namespaces + * + * This hook defines which block namespaces should have block name and attribute data- attributes appended on render. + * + * @param array $allowed_namespaces List of namespaces. + */ + $allowed_namespaces = array_merge( [ 'woocommerce', 'woocommerce-checkout' ], (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_namespace', [] ) ); + + /** + * WooCommerce Blocks Block Names + * + * This hook defines which block names should have block name and attribute data- attributes appended on render. + * + * @param array $allowed_namespaces List of namespaces. + */ + $allowed_blocks = (array) apply_filters( '__experimental_woocommerce_blocks_add_data_attributes_to_block', [] ); + + if ( ! in_array( $block_namespace, $allowed_namespaces, true ) && ! in_array( $block_name, $allowed_blocks, true ) ) { + return $content; + } + + $attributes = (array) $block['attrs']; + $escaped_data_attributes = [ + 'data-block-name="' . esc_attr( $block['blockName'] ) . '"', + ]; + + foreach ( $attributes as $key => $value ) { + if ( is_bool( $value ) ) { + $value = $value ? 'true' : 'false'; + } + if ( ! is_scalar( $value ) ) { + $value = wp_json_encode( $value ); + } + $escaped_data_attributes[] = 'data-' . esc_attr( strtolower( preg_replace( '/(?'; // phpcs:ignore WordPress.Security.NonceVerification + } + + /** + * Hide legacy widgets with a feature complete block equivalent in the inserter + * and prevent them from showing as an option in the Legacy Widget block. + * + * @param array $widget_types An array of widgets hidden in core. + * @return array $widget_types An array inluding the WooCommerce widgets to hide. + */ + public function hide_legacy_widgets_with_block_equivalent( $widget_types ) { + array_push( $widget_types, 'woocommerce_product_search', 'woocommerce_product_categories', 'woocommerce_recent_reviews' ); + return $widget_types; + } + + /** + * Get list of block types. + * + * @return array + */ + protected function get_block_types() { + global $wp_version, $pagenow; + + $block_types = [ + 'AllReviews', + 'FeaturedCategory', + 'FeaturedProduct', + 'HandpickedProducts', + 'ProductBestSellers', + 'ProductCategories', + 'ProductCategory', + 'ProductNew', + 'ProductOnSale', + 'ProductsByAttribute', + 'ProductTopRated', + 'ReviewsByProduct', + 'ReviewsByCategory', + 'ProductSearch', + 'ProductTag', + 'AllProducts', + 'PriceFilter', + 'AttributeFilter', + 'StockFilter', + 'ActiveFilters', + ]; + + if ( Package::feature()->is_feature_plugin_build() ) { + $block_types[] = 'Checkout'; + $block_types[] = 'Cart'; + } + + if ( Package::feature()->is_experimental_build() ) { + $block_types[] = 'SingleProduct'; + $block_types[] = 'CartI2'; + $block_types[] = 'MiniCart'; + } + + /** + * This disables specific blocks in Widget Areas by not registering them. + */ + if ( in_array( $pagenow, [ 'widgets.php', 'themes.php', 'customize.php' ], true ) ) { + $block_types = array_diff( + $block_types, + [ + 'AllProducts', + 'PriceFilter', + 'AttributeFilter', + 'StockFilter', + 'ActiveFilters', + 'Cart', + 'Checkout', + ] + ); + } + + return $block_types; + } + + /** + * Get atomic blocks types. + * + * @return array + */ + protected function get_atomic_blocks() { + return [ + 'product-title', + 'product-button', + 'product-image', + 'product-price', + 'product-rating', + 'product-sale-badge', + 'product-summary', + 'product-sku', + 'product-category-list', + 'product-tag-list', + 'product-stock-indicator', + 'product-add-to-cart', + ]; + } +} diff --git a/packages/woocommerce-blocks/src/Domain/Bootstrap.php b/packages/woocommerce-blocks/src/Domain/Bootstrap.php new file mode 100644 index 0000000..79cade1 --- /dev/null +++ b/packages/woocommerce-blocks/src/Domain/Bootstrap.php @@ -0,0 +1,333 @@ +container = $container; + $this->package = $container->get( Package::class ); + if ( $this->has_core_dependencies() ) { + $this->init(); + /** + * Usable as a safe event hook for when the plugin has been loaded. + */ + do_action( 'woocommerce_blocks_loaded' ); + } + } + + /** + * Init the package - load the blocks library and define constants. + */ + protected function init() { + $this->register_dependencies(); + $this->register_payment_methods(); + + add_action( + 'admin_init', + function() { + InboxNotifications::create_surface_cart_checkout_blocks_notification(); + }, + 10, + 0 + ); + + $is_rest = wc()->is_rest_api_request(); + + // Load assets in admin and on the frontend. + if ( ! $is_rest ) { + $this->add_build_notice(); + $this->container->get( AssetDataRegistry::class ); + $this->container->get( Installer::class ); + $this->container->get( AssetsController::class ); + } + $this->container->get( DraftOrders::class )->init(); + $this->container->get( CreateAccount::class )->init(); + $this->container->get( ExtendRestApi::class ); + $this->container->get( RestApi::class ); + $this->container->get( GoogleAnalytics::class ); + $this->container->get( BlockTypesController::class ); + if ( $this->package->feature()->is_feature_plugin_build() ) { + $this->container->get( PaymentsApi::class ); + } + } + + /** + * Check core dependencies exist. + * + * @return boolean + */ + protected function has_core_dependencies() { + $has_needed_dependencies = class_exists( 'WooCommerce', false ); + if ( $has_needed_dependencies ) { + $plugin_data = \get_file_data( + $this->package->get_path( 'woocommerce-gutenberg-products-block.php' ), + [ + 'RequiredWCVersion' => 'WC requires at least', + ] + ); + if ( isset( $plugin_data['RequiredWCVersion'] ) && version_compare( \WC()->version, $plugin_data['RequiredWCVersion'], '<' ) ) { + $has_needed_dependencies = false; + add_action( + 'admin_notices', + function() { + if ( should_display_compatibility_notices() ) { + ?> +
    +

    +
    + package->get_path( 'build/featured-product.js' ) + ); + } + + /** + * Add a notice stating that the build has not been done yet. + */ + protected function add_build_notice() { + if ( $this->is_built() ) { + return; + } + add_action( + 'admin_notices', + function() { + echo '

    '; + printf( + /* translators: %1$s is the install command, %2$s is the build command, %3$s is the watch command. */ + esc_html__( 'WooCommerce Blocks development mode requires files to be built. From the plugin directory, run %1$s to install dependencies, %2$s to build the files or %3$s to build the files and watch for changes.', 'woocommerce' ), + 'npm install', + 'npm run build', + 'npm start' + ); + echo '

    '; + } + ); + } + + /** + * Register core dependencies with the container. + */ + protected function register_dependencies() { + $this->container->register( + FeatureGating::class, + function ( Container $container ) { + return new FeatureGating(); + } + ); + $this->container->register( + AssetApi::class, + function ( Container $container ) { + return new AssetApi( $container->get( Package::class ) ); + } + ); + $this->container->register( + AssetDataRegistry::class, + function( Container $container ) { + return new AssetDataRegistry( $container->get( AssetApi::class ) ); + } + ); + $this->container->register( + AssetsController::class, + function( Container $container ) { + return new AssetsController( $container->get( AssetApi::class ) ); + } + ); + $this->container->register( + PaymentMethodRegistry::class, + function( Container $container ) { + return new PaymentMethodRegistry(); + } + ); + $this->container->register( + RestApi::class, + function ( Container $container ) { + return new RestApi( $container->get( RoutesController::class ) ); + } + ); + $this->container->register( + Installer::class, + function ( Container $container ) { + return new Installer(); + } + ); + $this->container->register( + BlockTypesController::class, + function ( Container $container ) { + $asset_api = $container->get( AssetApi::class ); + $asset_data_registry = $container->get( AssetDataRegistry::class ); + return new BlockTypesController( $asset_api, $asset_data_registry ); + } + ); + $this->container->register( + DraftOrders::class, + function( Container $container ) { + return new DraftOrders( $container->get( Package::class ) ); + } + ); + $this->container->register( + CreateAccount::class, + function( Container $container ) { + return new CreateAccount( $container->get( Package::class ) ); + } + ); + $this->container->register( + Formatters::class, + function( Container $container ) { + $formatters = new Formatters(); + $formatters->register( 'money', MoneyFormatter::class ); + $formatters->register( 'html', HtmlFormatter::class ); + $formatters->register( 'currency', CurrencyFormatter::class ); + return $formatters; + } + ); + $this->container->register( + SchemaController::class, + function( Container $container ) { + return new SchemaController( $container->get( ExtendRestApi::class ) ); + } + ); + $this->container->register( + RoutesController::class, + function( Container $container ) { + return new RoutesController( $container->get( SchemaController::class ) ); + } + ); + $this->container->register( + ExtendRestApi::class, + function( Container $container ) { + return new ExtendRestApi( $container->get( Package::class ), $container->get( Formatters::class ) ); + } + ); + $this->container->register( + GoogleAnalytics::class, + function( Container $container ) { + // Require Google Analytics Integration to be activated. + if ( ! class_exists( 'WC_Google_Analytics_Integration', false ) ) { + return; + } + $asset_api = $container->get( AssetApi::class ); + return new GoogleAnalytics( $asset_api ); + } + ); + if ( $this->package->feature()->is_feature_plugin_build() ) { + $this->container->register( + PaymentsApi::class, + function ( Container $container ) { + $payment_method_registry = $container->get( PaymentMethodRegistry::class ); + $asset_data_registry = $container->get( AssetDataRegistry::class ); + return new PaymentsApi( $payment_method_registry, $asset_data_registry ); + } + ); + } + } + + /** + * Register payment method integrations with the container. + * + * @internal Stripe is a temporary method that is used for setting up payment method integrations with Cart and + * Checkout blocks. This logic should get moved to the payment gateway extensions. + */ + protected function register_payment_methods() { + $this->container->register( + Stripe::class, + function( Container $container ) { + $asset_api = $container->get( AssetApi::class ); + return new Stripe( $asset_api ); + } + ); + $this->container->register( + Cheque::class, + function( Container $container ) { + $asset_api = $container->get( AssetApi::class ); + return new Cheque( $asset_api ); + } + ); + $this->container->register( + PayPal::class, + function( Container $container ) { + $asset_api = $container->get( AssetApi::class ); + return new PayPal( $asset_api ); + } + ); + $this->container->register( + BankTransfer::class, + function( Container $container ) { + $asset_api = $container->get( AssetApi::class ); + return new BankTransfer( $asset_api ); + } + ); + $this->container->register( + CashOnDelivery::class, + function( Container $container ) { + $asset_api = $container->get( AssetApi::class ); + return new CashOnDelivery( $asset_api ); + } + ); + } +} diff --git a/packages/woocommerce-blocks/src/Domain/Package.php b/packages/woocommerce-blocks/src/Domain/Package.php new file mode 100644 index 0000000..d559a71 --- /dev/null +++ b/packages/woocommerce-blocks/src/Domain/Package.php @@ -0,0 +1,110 @@ +version = $version; + $this->path = $plugin_path; + $this->feature_gating = $feature_gating; + } + + /** + * Returns the version of the plugin. + * + * @return string + */ + public function get_version() { + return $this->version; + } + + /** + * Returns the path to the plugin directory. + * + * @param string $relative_path If provided, the relative path will be + * appended to the plugin path. + * + * @return string + */ + public function get_path( $relative_path = '' ) { + return trailingslashit( $this->path ) . $relative_path; + } + + /** + * Returns the url to the blocks plugin directory. + * + * @param string $relative_url If provided, the relative url will be + * appended to the plugin url. + * + * @return string + */ + public function get_url( $relative_url = '' ) { + // Append index.php so WP does not return the parent directory. + return plugin_dir_url( $this->path . '/index.php' ) . $relative_url; + } + + /** + * Returns an instance of the the FeatureGating class. + * + * @return FeatureGating + */ + public function feature() { + return $this->feature_gating; + } + + /** + * Checks if we're executing the code in an experimental build mode. + * + * @return boolean + */ + public function is_experimental_build() { + return $this->feature()->is_experimental_build(); + } + + /** + * Checks if we're executing the code in an feature plugin or experimental build mode. + * + * @return boolean + */ + public function is_feature_plugin_build() { + return $this->feature()->is_feature_plugin_build(); + } +} diff --git a/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php b/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php new file mode 100644 index 0000000..ad33f22 --- /dev/null +++ b/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php @@ -0,0 +1,263 @@ +package = $package; + } + + /** + * Single method for feature gating logic. Used to gate all non-private methods. + * + * @return True if Checkout sign-up feature should be made available. + */ + private function is_feature_enabled() { + // Checkout signup is feature gated to WooCommerce 4.7 and newer; + // uses updated my-account/lost-password screen from 4.7+ for + // setting initial password. + // This service is feature gated to plugin only, to match the + // availability of the Checkout block (feature plugin only). + return $this->package->feature()->is_feature_plugin_build() && defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '4.7', '>=' ); + } + + /** + * Init - register handlers for WooCommerce core email hooks. + */ + public function init() { + if ( ! self::is_feature_enabled() ) { + return; + } + + // Override core email handlers to add our new improved "new account" email. + add_action( + 'woocommerce_email', + function ( $wc_emails_instance ) { + // Remove core "new account" handler; we are going to replace it. + remove_action( 'woocommerce_created_customer_notification', array( $wc_emails_instance, 'customer_new_account' ), 10, 3 ); + + // Add custom "new account" handler. + add_action( + 'woocommerce_created_customer_notification', + function( $customer_id, $new_customer_data = array(), $password_generated = false ) use ( $wc_emails_instance ) { + // If this is a block-based signup, send a new email + // with password reset link (no password in email). + if ( isset( $new_customer_data['is_checkout_block_customer_signup'] ) ) { + $this->customer_new_account( $customer_id, $new_customer_data ); + return; + } + + // Otherwise, trigger the existing legacy email (with new password inline). + $wc_emails_instance->customer_new_account( $customer_id, $new_customer_data, $password_generated ); + }, + 10, + 3 + ); + } + ); + } + + /** + * Trigger new account email. + * This is intended as a replacement to WC_Emails::customer_new_account(), + * with a set password link instead of emailing the new password in email + * content. + * + * @param int $customer_id The ID of the new customer account. + * @param array $new_customer_data Assoc array of data for the new account. + */ + public function customer_new_account( $customer_id = 0, array $new_customer_data = array() ) { + if ( ! self::is_feature_enabled() ) { + return; + } + + if ( ! $customer_id ) { + return; + } + + $new_account_email = new CustomerNewAccount( $this->package ); + $new_account_email->trigger( $customer_id, $new_customer_data ); + } + + /** + * Create a user account for specified request (if necessary). + * If a new account is created: + * - The user is logged in. + * + * @param \WP_REST_Request $request The current request object being handled. + * + * @throws Exception On error. + * @return int The new user id, or 0 if no user was created. + */ + public function from_order_request( \WP_REST_Request $request ) { + if ( ! self::is_feature_enabled() || ! $this->should_create_customer_account( $request ) ) { + return 0; + } + + $customer_id = $this->create_customer_account( + $request['billing_address']['email'], + $request['billing_address']['first_name'], + $request['billing_address']['last_name'] + ); + // Log the customer in and associate with the order. + wc_set_customer_auth_cookie( $customer_id ); + + return $customer_id; + } + + /** + * Check request options and store (shop) config to determine if a user account + * should be created as part of order processing. + * + * @param \WP_REST_Request $request The current request object being handled. + * + * @return boolean True if a new user account should be created. + */ + protected function should_create_customer_account( \WP_REST_Request $request ) { + if ( is_user_logged_in() ) { + // User is already logged in - no need to create an account. + return false; + } + + // From here we know that the shopper is not logged in. + // check for whether account creation is enabled at the global level. + $checkout = WC()->checkout(); + if ( ! $checkout instanceof \WC_Checkout ) { + // If checkout class is not available, we have major problems, don't create account. + return false; + } + + if ( false === filter_var( $checkout->is_registration_enabled(), FILTER_VALIDATE_BOOLEAN ) ) { + // Registration is not enabled for the store, so return false. + return false; + } + + if ( true === filter_var( $checkout->is_registration_required(), FILTER_VALIDATE_BOOLEAN ) ) { + // Store requires an account for all checkouts (purchases). + // Create an account independent of shopper option in $request. + // Note - checkbox is not displayed to shopper in this case. + return true; + } + + // From here we know that the store allows guest checkout; + // shopper can choose whether they sign up (`should_create_account`). + + if ( true === filter_var( $request['should_create_account'], FILTER_VALIDATE_BOOLEAN ) ) { + // User has requested an account as part of checkout processing. + return true; + } + + return false; + } + + /** + * Convert an account creation error to an exception. + * + * @param \WP_Error $error An error object. + * + * @return Exception. + */ + private function map_create_account_error( \WP_Error $error ) { + switch ( $error->get_error_code() ) { + // WordPress core error codes. + case 'empty_username': + case 'invalid_username': + case 'empty_email': + case 'invalid_email': + case 'email_exists': + case 'registerfail': + return new \Exception( 'woocommerce_rest_checkout_create_account_failure' ); + } + + return new \Exception( 'woocommerce_rest_checkout_create_account_failure' ); + } + + /** + * Create a new account for a customer (using a new blocks-specific PHP API). + * + * The account is created with a generated username. The customer is sent + * an email notifying them about the account and containing a link to set + * their (initial) password. + * + * Intended as a replacement for wc_create_new_customer in WC core. + * + * @throws \Exception If an error is encountered when creating the user account. + * + * @param string $user_email The email address to use for the new account. + * @param string $first_name The first name to use for the new account. + * @param string $last_name The last name to use for the new account. + * + * @return int User id if successful + */ + private function create_customer_account( $user_email, $first_name, $last_name ) { + if ( empty( $user_email ) || ! is_email( $user_email ) ) { + throw new \Exception( 'registration-error-invalid-email' ); + } + + if ( email_exists( $user_email ) ) { + throw new \Exception( 'registration-error-email-exists' ); + } + + $username = wc_create_new_customer_username( $user_email ); + + // Handle password creation. + $password = wp_generate_password(); + $password_generated = true; + + // Use WP_Error to handle registration errors. + $errors = new \WP_Error(); + + do_action( 'woocommerce_register_post', $username, $user_email, $errors ); + + $errors = apply_filters( 'woocommerce_registration_errors', $errors, $username, $user_email ); + + if ( $errors->get_error_code() ) { + throw new \Exception( $errors->get_error_code() ); + } + + $new_customer_data = apply_filters( + 'woocommerce_new_customer_data', + array( + 'is_checkout_block_customer_signup' => true, + 'user_login' => $username, + 'user_pass' => $password, + 'user_email' => $user_email, + 'first_name' => $first_name, + 'last_name' => $last_name, + 'role' => 'customer', + ) + ); + + $customer_id = wp_insert_user( $new_customer_data ); + + if ( is_wp_error( $customer_id ) ) { + throw $this->map_create_account_error( $customer_id ); + } + + // Set account flag to remind customer to update generated password. + update_user_option( $customer_id, 'default_password_nag', true, true ); + + do_action( 'woocommerce_created_customer', $customer_id, $new_customer_data, $password_generated ); + + return $customer_id; + } +} diff --git a/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php b/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php new file mode 100644 index 0000000..4ae73e9 --- /dev/null +++ b/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php @@ -0,0 +1,259 @@ +package = $package; + } + + /** + * Set all hooks related to adding Checkout Draft order functionality to Woo Core. + */ + public function init() { + if ( $this->package->feature()->is_feature_plugin_build() ) { + add_filter( 'wc_order_statuses', [ $this, 'register_draft_order_status' ] ); + add_filter( 'woocommerce_register_shop_order_post_statuses', [ $this, 'register_draft_order_post_status' ] ); + add_filter( 'woocommerce_analytics_excluded_order_statuses', [ $this, 'append_draft_order_post_status' ] ); + add_filter( 'woocommerce_valid_order_statuses_for_payment', [ $this, 'append_draft_order_post_status' ] ); + add_filter( 'woocommerce_valid_order_statuses_for_payment_complete', [ $this, 'append_draft_order_post_status' ] ); + // Hook into the query to retrieve My Account orders so draft status is excluded. + add_action( 'woocommerce_my_account_my_orders_query', [ $this, 'delete_draft_order_post_status_from_args' ] ); + add_action( 'woocommerce_cleanup_draft_orders', [ $this, 'delete_expired_draft_orders' ] ); + add_action( 'admin_init', [ $this, 'install' ] ); + } else { + // Maybe remove existing cronjob if present because it shouldn't be needed in the environment. + add_action( 'admin_init', [ $this, 'uninstall' ] ); + } + } + + /** + * Installation related logic for Draft order functionality. + * + * @internal + */ + public function install() { + $this->maybe_create_cronjobs(); + } + + /** + * Remove cronjobs if they exist (but only from admin). + * + * @internal + */ + public function uninstall() { + $this->maybe_remove_cronjobs(); + } + + /** + * Maybe create cron events. + */ + protected function maybe_create_cronjobs() { + if ( function_exists( 'as_next_scheduled_action' ) && false === as_next_scheduled_action( 'woocommerce_cleanup_draft_orders' ) ) { + as_schedule_recurring_action( strtotime( 'midnight tonight' ), DAY_IN_SECONDS, 'woocommerce_cleanup_draft_orders' ); + } + } + + /** + * Unschedule cron jobs that are present. + */ + protected function maybe_remove_cronjobs() { + if ( function_exists( 'as_next_scheduled_action' ) && as_next_scheduled_action( 'woocommerce_cleanup_draft_orders' ) ) { + as_unschedule_all_actions( 'woocommerce_cleanup_draft_orders' ); + } + } + + /** + * Register custom order status for orders created via the API during checkout. + * + * Draft order status is used before payment is attempted, during checkout, when a cart is converted to an order. + * + * @param array $statuses Array of statuses. + * @internal + * @return array + */ + public function register_draft_order_status( array $statuses ) { + $statuses[ self::DB_STATUS ] = _x( 'Draft', 'Order status', 'woocommerce' ); + return $statuses; + } + + /** + * Register custom order post status for orders created via the API during checkout. + * + * @param array $statuses Array of statuses. + * @internal + + * @return array + */ + public function register_draft_order_post_status( array $statuses ) { + $statuses[ self::DB_STATUS ] = $this->get_post_status_properties(); + return $statuses; + } + + /** + * Returns the properties of this post status for registration. + * + * @return array + */ + private function get_post_status_properties() { + return [ + 'label' => _x( 'Draft', 'Order status', 'woocommerce' ), + 'public' => false, + 'exclude_from_search' => false, + 'show_in_admin_all_list' => false, + 'show_in_admin_status_list' => true, + /* translators: %s: number of orders */ + 'label_count' => _n_noop( 'Drafts (%s)', 'Drafts (%s)', 'woocommerce' ), + ]; + } + + /** + * Remove draft status from the 'status' argument of an $args array. + * + * @param array $args Array of arguments containing statuses in the status key. + * @internal + * @return array + */ + public function delete_draft_order_post_status_from_args( $args ) { + if ( ! array_key_exists( 'status', $args ) ) { + $statuses = []; + foreach ( wc_get_order_statuses() as $key => $label ) { + if ( self::DB_STATUS !== $key ) { + $statuses[] = str_replace( 'wc-', '', $key ); + } + } + $args['status'] = $statuses; + } elseif ( self::DB_STATUS === $args['status'] ) { + $args['status'] = ''; + } elseif ( is_array( $args['status'] ) ) { + $args['status'] = array_diff_key( $args['status'], array( self::STATUS => null ) ); + } + + return $args; + } + + /** + * Append draft status to a list of statuses. + * + * @param array $statuses Array of statuses. + * @internal + + * @return array + */ + public function append_draft_order_post_status( $statuses ) { + $statuses[] = self::STATUS; + return $statuses; + } + + /** + * Delete draft orders older than a day in batches of 20. + * + * Ran on a daily cron schedule. + * + * @internal + */ + public function delete_expired_draft_orders() { + $count = 0; + $batch_size = 20; + $this->ensure_draft_status_registered(); + $orders = wc_get_orders( + [ + 'date_modified' => '<=' . strtotime( '-1 DAY' ), + 'limit' => $batch_size, + 'status' => self::DB_STATUS, + 'type' => 'shop_order', + ] + ); + + // do we bail because the query results are unexpected? + try { + $this->assert_order_results( $orders, $batch_size ); + if ( $orders ) { + foreach ( $orders as $order ) { + $order->delete( true ); + $count ++; + } + } + if ( $batch_size === $count && function_exists( 'as_enqueue_async_action' ) ) { + as_enqueue_async_action( 'woocommerce_cleanup_draft_orders' ); + } + } catch ( Exception $error ) { + wc_caught_exception( $error, __METHOD__ ); + } + } + + /** + * Since it's possible for third party code to clobber the `$wp_post_statuses` global, + * we need to do a final check here to make sure the draft post status is + * registered with the global so that it is not removed by WP_Query status + * validation checks. + */ + private function ensure_draft_status_registered() { + $is_registered = get_post_stati( [ 'name' => self::DB_STATUS ] ); + if ( empty( $is_registered ) ) { + register_post_status( + self::DB_STATUS, + $this->get_post_status_properties() + ); + } + } + + /** + * Asserts whether incoming order results are expected given the query + * this service class executes. + * + * @param WC_Order[] $order_results The order results being asserted. + * @param int $expected_batch_size The expected batch size for the results. + * @throws Exception If any assertions fail, an exception is thrown. + */ + private function assert_order_results( $order_results, $expected_batch_size ) { + // if not an array, then just return because it won't get handled + // anyways. + if ( ! is_array( $order_results ) ) { + return; + } + + $suffix = ' This is an indicator that something is filtering WooCommerce or WordPress queries and modifying the query parameters.'; + + // if count is greater than our expected batch size, then that's a problem. + if ( count( $order_results ) > 20 ) { + throw new Exception( 'There are an unexpected number of results returned from the query.' . $suffix ); + } + + // if any of the returned orders are not draft (or not a WC_Order), then that's a problem. + foreach ( $order_results as $order ) { + if ( ! ( $order instanceof WC_Order ) ) { + throw new Exception( 'The returned results contain a value that is not a WC_Order.' . $suffix ); + } + if ( ! $order->has_status( self::STATUS ) ) { + throw new Exception( 'The results contain an order that is not a `wc-checkout-draft` status in the results.' . $suffix ); + } + } + } +} diff --git a/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php b/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php new file mode 100644 index 0000000..3647f49 --- /dev/null +++ b/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php @@ -0,0 +1,176 @@ + Emails) + // apply to this email (consistent with the core email). + $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 blocks.', 'woocommerce' ); + $this->template_html = 'emails/customer-new-account-blocks.php'; + $this->template_plain = 'emails/plain/customer-new-account-blocks.php'; + $this->default_template_path = $package->get_path( '/templates/' ); + + // Call parent constructor. + parent::__construct(); + } + + /** + * Get email subject. + * + * @since 3.1.0 + * @return string + */ + public function get_default_subject() { + return __( 'Your {site_title} account has been created!', 'woocommerce' ); + } + + /** + * Get email heading. + * + * @since 3.1.0 + * @return string + */ + public function get_default_heading() { + return __( 'Welcome to {site_title}', 'woocommerce' ); + } + + /** + * Trigger. + * + * @param int $user_id User ID. + * @param string $user_pass User password. + * @param bool $password_generated Whether the password was generated automatically or not. + */ + public function trigger( $user_id, $user_pass = '', $password_generated = false ) { + $this->setup_locale(); + + if ( $user_id ) { + $this->object = new \WP_User( $user_id ); + + // Generate a magic link so user can set initial password. + $key = get_password_reset_key( $this->object ); + if ( ! is_wp_error( $key ) ) { + $action = 'newaccount'; + $this->set_password_url = wc_get_account_endpoint_url( 'lost-password' ) . "?action=$action&key=$key&login=" . rawurlencode( $this->object->user_login ); + } + + $this->user_login = stripslashes( $this->object->user_login ); + $this->user_email = stripslashes( $this->object->user_email ); + $this->recipient = $this->user_email; + } + + if ( $this->is_enabled() && $this->get_recipient() ) { + $this->send( $this->get_recipient(), $this->get_subject(), $this->get_content(), $this->get_headers(), $this->get_attachments(), $this->set_password_url ); + } + + $this->restore_locale(); + } + + /** + * Get content html. + * + * @return string + */ + public function get_content_html() { + return wc_get_template_html( + $this->template_html, + array( + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'user_login' => $this->user_login, + 'blogname' => $this->get_blogname(), + 'set_password_url' => $this->set_password_url, + 'sent_to_admin' => false, + 'plain_text' => false, + 'email' => $this, + ), + '', + $this->default_template_path + ); + } + + /** + * Get content plain. + * + * @return string + */ + public function get_content_plain() { + return wc_get_template_html( + $this->template_plain, + array( + 'email_heading' => $this->get_heading(), + 'additional_content' => $this->get_additional_content(), + 'user_login' => $this->user_login, + 'blogname' => $this->get_blogname(), + 'set_password_url' => $this->set_password_url, + 'sent_to_admin' => false, + 'plain_text' => true, + 'email' => $this, + ), + '', + $this->default_template_path + ); + } + + /** + * Default content to show below main email content. + * + * @since 3.7.0 + * @return string + */ + public function get_default_additional_content() { + return __( 'We look forward to seeing you soon.', 'woocommerce' ); + } +} diff --git a/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php b/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php new file mode 100644 index 0000000..64abc20 --- /dev/null +++ b/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php @@ -0,0 +1,379 @@ +package = $package; + $this->formatters = $formatters; + } + + /** + * Returns a formatter instance. + * + * @param string $name Formatter name. + * @return FormatterInterface + */ + public function get_formatter( $name ) { + return $this->formatters->$name; + } + + /** + * Data to be extended + * + * @var array + */ + private $extend_data = []; + + /** + * Data to be extended + * + * @var array + */ + private $callback_methods = []; + + /** + * Array of payment requirements + * + * @var array + */ + private $payment_requirements = []; + + /** + * An endpoint that validates registration method call + * + * @param array $args { + * An array of elements that make up a post to update or insert. + * + * @type string $endpoint The endpoint to extend. + * @type string $namespace Plugin namespace. + * @type callable $schema_callback Callback executed to add schema data. + * @type callable $data_callback Callback executed to add endpoint data. + * @type string $schema_type The type of data, object or array. + * } + * + * @throws Exception On failure to register. + * @return boolean True on success. + */ + public function register_endpoint_data( $args ) { + if ( ! is_string( $args['namespace'] ) ) { + $this->throw_exception( 'You must provide a plugin namespace when extending a Store REST endpoint.' ); + } + + if ( ! is_string( $args['endpoint'] ) || ! in_array( $args['endpoint'], $this->endpoints, true ) ) { + $this->throw_exception( + sprintf( 'You must provide a valid Store REST endpoint to extend, valid endpoints are: %1$s. You provided %2$s.', implode( ', ', $this->endpoints ), $args['endpoint'] ) + ); + } + + if ( isset( $args['schema_callback'] ) && ! is_callable( $args['schema_callback'] ) ) { + $this->throw_exception( '$schema_callback must be a callable function.' ); + } + + if ( isset( $args['data_callback'] ) && ! is_callable( $args['data_callback'] ) ) { + $this->throw_exception( '$data_callback must be a callable function.' ); + } + + if ( isset( $args['schema_type'] ) && ! in_array( $args['schema_type'], [ ARRAY_N, ARRAY_A ], true ) ) { + $this->throw_exception( + sprintf( 'Data type must be either ARRAY_N for a numeric array or ARRAY_A for an object like array. You provided %1$s.', $args['schema_type'] ) + ); + } + + $this->extend_data[ $args['endpoint'] ][ $args['namespace'] ] = [ + 'schema_callback' => isset( $args['schema_callback'] ) ? $args['schema_callback'] : null, + 'data_callback' => isset( $args['data_callback'] ) ? $args['data_callback'] : null, + 'schema_type' => isset( $args['schema_type'] ) ? $args['schema_type'] : ARRAY_A, + ]; + + return true; + } + + /** + * Add callback functions that can be executed by the cart/extensions endpoint. + * + * @param array $args { + * An array of elements that make up the callback configuration. + * + * @type string $endpoint The endpoint to extend. + * @type string $namespace Plugin namespace. + * @type callable $callback The function/callable to execute. + * } + * + * @throws RouteException On failure to register. + * @returns boolean True on success. + */ + public function register_update_callback( $args ) { + if ( ! array_key_exists( 'namespace', $args ) || ! is_string( $args['namespace'] ) ) { + throw new RouteException( + 'woocommerce_rest_cart_extensions_error', + 'You must provide a plugin namespace when extending a Store REST endpoint.', + 400 + ); + } + + if ( ! array_key_exists( 'callback', $args ) || ! is_callable( $args['callback'] ) ) { + throw new RouteException( + 'woocommerce_rest_cart_extensions_error', + 'There is no valid callback supplied to register_update_callback.', + 400 + ); + } + + $this->callback_methods[ $args['namespace'] ] = [ + 'callback' => $args['callback'], + ]; + return true; + } + + /** + * Get callback for a specific endpoint and namespace. + * + * @param string $namespace The namespace to get callbacks for. + * + * @return callable The callback registered by the extension. + * @throws RouteException When callback is not callable or parameters are incorrect. + */ + public function get_update_callback( $namespace ) { + $method = null; + if ( ! is_string( $namespace ) ) { + throw new RouteException( + 'woocommerce_rest_cart_extensions_error', + 'You must provide a plugin namespace when extending a Store REST endpoint.', + 400 + ); + } + + if ( ! array_key_exists( $namespace, $this->callback_methods ) ) { + throw new RouteException( + 'woocommerce_rest_cart_extensions_error', + sprintf( 'There is no such namespace registered: %1$s.', $namespace ), + 400 + ); + } + + if ( ! array_key_exists( 'callback', $this->callback_methods[ $namespace ] ) || ! is_callable( $this->callback_methods[ $namespace ]['callback'] ) ) { + throw new RouteException( + 'woocommerce_rest_cart_extensions_error', + sprintf( 'There is no valid callback registered for: %1$s.', $namespace ), + 400 + ); + } + return $this->callback_methods[ $namespace ]['callback']; + } + + /** + * Returns the registered endpoint data + * + * @param string $endpoint A valid identifier. + * @param array $passed_args Passed arguments from the Schema class. + * @return object Returns an casted object with registered endpoint data. + * @throws Exception If a registered callback throws an error, or silently logs it. + */ + public function get_endpoint_data( $endpoint, array $passed_args = [] ) { + $registered_data = []; + if ( ! isset( $this->extend_data[ $endpoint ] ) ) { + return (object) $registered_data; + } + foreach ( $this->extend_data[ $endpoint ] as $namespace => $callbacks ) { + $data = []; + + if ( is_null( $callbacks['data_callback'] ) ) { + continue; + } + + try { + $data = $callbacks['data_callback']( ...$passed_args ); + + if ( ! is_array( $data ) ) { + throw new Exception( '$data_callback must return an array.' ); + } + } catch ( Throwable $e ) { + $this->throw_exception( $e ); + continue; + } + + $registered_data[ $namespace ] = $data; + } + return (object) $registered_data; + } + + /** + * Returns the registered endpoint schema + * + * @param string $endpoint A valid identifier. + * @param array $passed_args Passed arguments from the Schema class. + * @return array Returns an array with registered schema data. + * @throws Exception If a registered callback throws an error, or silently logs it. + */ + public function get_endpoint_schema( $endpoint, array $passed_args = [] ) { + $registered_schema = []; + if ( ! isset( $this->extend_data[ $endpoint ] ) ) { + return (object) $registered_schema; + } + + foreach ( $this->extend_data[ $endpoint ] as $namespace => $callbacks ) { + $schema = []; + + if ( is_null( $callbacks['schema_callback'] ) ) { + continue; + } + + try { + $schema = $callbacks['schema_callback']( ...$passed_args ); + + if ( ! is_array( $schema ) ) { + throw new Exception( '$schema_callback must return an array.' ); + } + } catch ( Throwable $e ) { + $this->throw_exception( $e ); + continue; + } + + $schema = $this->format_extensions_properties( $namespace, $schema, $callbacks['schema_type'] ); + + $registered_schema[ $namespace ] = $schema; + } + return (object) $registered_schema; + } + + /** + * Registers and validates payment requirements callbacks. + * + * @param array $args { + * Array of registration data. + * + * @type callable $data_callback Callback executed to add payment requirements data. + * } + * + * @throws Exception On failure to register. + * @return boolean True on success. + */ + public function register_payment_requirements( $args ) { + if ( ! is_callable( $args['data_callback'] ) ) { + $this->throw_exception( '$data_callback must be a callable function.' ); + } + + $this->payment_requirements[] = $args['data_callback']; + + return true; + } + + /** + * Returns the additional payment requirements. + * + * @param array $initial_requirements list of requirements that should be added to the collected requirements. + * @return array Returns a list of payment requirements. + * @throws Exception If a registered callback throws an error, or silently logs it. + */ + public function get_payment_requirements( array $initial_requirements = [ 'products' ] ) { + $requirements = $initial_requirements; + if ( empty( $this->payment_requirements ) ) { + return $initial_requirements; + } + + foreach ( $this->payment_requirements as $callback ) { + $data = []; + + try { + $data = $callback(); + + if ( ! is_array( $data ) ) { + throw new Exception( '$data_callback must return an array.' ); + } + } catch ( Throwable $e ) { + $this->throw_exception( $e ); + continue; + } + $requirements = array_merge( $requirements, $data ); + } + + return array_unique( $requirements ); + } + + /** + * Throws error and/or silently logs it. + * + * @param string|Throwable $exception_or_error Error message or Exception. + * @throws Exception An error to throw if we have debug enabled and user is admin. + */ + private function throw_exception( $exception_or_error ) { + if ( is_string( $exception_or_error ) ) { + $exception = new Exception( $exception_or_error ); + } else { + $exception = $exception_or_error; + } + // Always log an error. + wc_caught_exception( $exception ); + if ( defined( 'WP_DEBUG' ) && WP_DEBUG && current_user_can( 'manage_woocommerce' ) ) { + throw $exception; + } + } + + /** + * Format schema for an extension. + * + * @param string $namespace Error message or Exception. + * @param array $schema An error to throw if we have debug enabled and user is admin. + * @param string $schema_type How should data be shaped. + * + * @return array Formatted schema. + */ + private function format_extensions_properties( $namespace, $schema, $schema_type ) { + if ( ARRAY_N === $schema_type ) { + return [ + /* translators: %s: extension namespace */ + 'description' => sprintf( __( 'Extension data registered by %s', 'woocommerce' ), $namespace ), + 'type' => 'array', + 'context' => [ 'view', 'edit' ], + 'items' => $schema, + ]; + } + return [ + /* translators: %s: extension namespace */ + 'description' => sprintf( __( 'Extension data registered by %s', 'woocommerce' ), $namespace ), + 'type' => 'object', + 'context' => [ 'view', 'edit' ], + 'properties' => $schema, + ]; + } +} diff --git a/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php b/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php new file mode 100644 index 0000000..4431fdf --- /dev/null +++ b/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php @@ -0,0 +1,140 @@ +flag = $flag; + $this->environment = $environment; + $this->load_flag(); + $this->load_environment(); + } + + /** + * Set correct flag. + */ + public function load_flag() { + if ( 0 === $this->flag ) { + $default_flag = defined( 'WC_BLOCKS_IS_FEATURE_PLUGIN' ) ? self::FEATURE_PLUGIN_FLAG : self::CORE_FLAG; + + if ( file_exists( __DIR__ . '/../../../blocks.ini' ) ) { + $allowed_flags = [ self::EXPERIMENTAL_FLAG, self::FEATURE_PLUGIN_FLAG, self::CORE_FLAG ]; + $woo_options = parse_ini_file( __DIR__ . '/../../../blocks.ini' ); + $this->flag = is_array( $woo_options ) && in_array( intval( $woo_options['woocommerce_blocks_phase'] ), $allowed_flags, true ) ? $woo_options['woocommerce_blocks_phase'] : $default_flag; + } else { + $this->flag = $default_flag; + } + } + } + + /** + * Set correct environment. + */ + public function load_environment() { + if ( 'unset' === $this->environment ) { + if ( file_exists( __DIR__ . '/../../../blocks.ini' ) ) { + $allowed_environments = [ self::PRODUCTION_ENVIRONMENT, self::DEVELOPMENT_ENVIRONMENT, self::TEST_ENVIRONMENT ]; + $woo_options = parse_ini_file( __DIR__ . '/../../../blocks.ini' ); + $this->environment = is_array( $woo_options ) && in_array( $woo_options['woocommerce_blocks_env'], $allowed_environments, true ) ? $woo_options['woocommerce_blocks_env'] : self::PRODUCTION_ENVIRONMENT; + } else { + $this->environment = self::PRODUCTION_ENVIRONMENT; + } + } + } + + /** + * Returns the current flag value. + * + * @return int + */ + public function get_flag() { + return $this->flag; + } + + /** + * Checks if we're executing the code in an experimental build mode. + * + * @return boolean + */ + public function is_experimental_build() { + return $this->flag >= self::EXPERIMENTAL_FLAG; + } + + /** + * Checks if we're executing the code in an feature plugin or experimental build mode. + * + * @return boolean + */ + public function is_feature_plugin_build() { + return $this->flag >= self::FEATURE_PLUGIN_FLAG; + } + + /** + * Returns the current environment value. + * + * @return string + */ + public function get_environment() { + return $this->environment; + } + + /** + * Checks if we're executing the code in an development environment. + * + * @return boolean + */ + public function is_development_environment() { + return self::DEVELOPMENT_ENVIRONMENT === $this->environment; + } + + /** + * Checks if we're executing the code in a production environment. + * + * @return boolean + */ + public function is_production_environment() { + return self::PRODUCTION_ENVIRONMENT === $this->environment; + } + + /** + * Checks if we're executing the code in a test environment. + * + * @return boolean + */ + public function is_test_environment() { + return self::TEST_ENVIRONMENT === $this->environment; + } +} diff --git a/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php b/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php new file mode 100644 index 0000000..445e700 --- /dev/null +++ b/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php @@ -0,0 +1,103 @@ +asset_api = $asset_api; + $this->init(); + } + + /** + * Hook into WP. + */ + protected function init() { + add_action( 'init', array( $this, 'register_assets' ) ); + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); + add_filter( 'script_loader_tag', array( $this, 'async_script_loader_tags' ), 10, 3 ); + } + + /** + * Register scripts. + */ + public function register_assets() { + $this->asset_api->register_script( 'wc-blocks-google-analytics', 'build/wc-blocks-google-analytics.js', [ 'google-tag-manager' ] ); + } + + /** + * Enqueue the Google Tag Manager script if prerequisites are met. + */ + public function enqueue_scripts() { + $settings = $this->get_google_analytics_settings(); + + // Require tracking to be enabled with a valid GA ID. + if ( ! stristr( $settings['ga_id'], 'G-' ) || apply_filters( 'woocommerce_ga_disable_tracking', ! wc_string_to_bool( $settings['ga_event_tracking_enabled'] ) ) ) { + return; + } + + if ( ! wp_script_is( 'google-tag-manager', 'registered' ) ) { + // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion + wp_register_script( 'google-tag-manager', 'https://www.googletagmanager.com/gtag/js?id=' . $settings['ga_id'], [], null, false ); + wp_add_inline_script( + 'google-tag-manager', + " + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + gtag('config', '" . esc_js( $settings['ga_id'] ) . "', { 'send_page_view': false });" + ); + } + wp_enqueue_script( 'wc-blocks-google-analytics' ); + } + + /** + * Get settings from the GA integration extension. + * + * @return array + */ + private function get_google_analytics_settings() { + return wp_parse_args( + get_option( 'woocommerce_google_analytics_settings' ), + [ + 'ga_id' => '', + 'ga_event_tracking_enabled' => 'no', + ] + ); + } + + /** + * Add async to script tags with defined handles. + * + * @param string $tag HTML for the script tag. + * @param string $handle Handle of script. + * @param string $src Src of script. + * @return string + */ + public function async_script_loader_tags( $tag, $handle, $src ) { + if ( ! in_array( $handle, array( 'google-tag-manager' ), true ) ) { + return $tag; + } + // If script was output manually in wp_head, abort. + if ( did_action( 'woocommerce_gtag_snippet' ) ) { + return ''; + } + return str_replace( '\n", $output ); // phpcs:ignore + + // Finally, short circuit the pre_load_script_translations hook by returning + // the translation JSON from the feature plugin, if it exists so this hook + // does not run again for the current handle. + $path_md5 = md5( 'build/' . $handle_filename ); + $json_file = $lang_dir . '/' . $domain . '-' . $locale . '-' . $path_md5 . '.json'; + $translations = is_file( $json_file ) && is_readable( $json_file ) ? file_get_contents( $json_file ) : false; // phpcs:ignore + + if ( $translations ) { + return $translations; + } + + // Return valid empty Jed locale. + return '{ "locale_data": { "messages": { "": {} } } }'; +} + +add_filter( 'pre_load_script_translations', 'woocommerce_blocks_get_i18n_data_json', 10, 4 ); + +/** + * Filter translations so we can retrieve translations from Core when the original and the translated + * texts are the same (which happens when translations are missing). + * + * @param string $translation Translated text based on WC Blocks translations. + * @param string $text Text to translate. + * @param string $domain The text domain. + * @return string WC Blocks translation. In case it's the same as $text, Core translation. + */ +function woocommerce_blocks_get_php_translation_from_core( $translation, $text, $domain ) { + if ( 'woo-gutenberg-products-block' !== $domain ) { + return $translation; + } + + // When translation is the same, that could mean the string is not translated. + // In that case, load it from core. + if ( $translation === $text ) { + return translate( $text, 'woocommerce' ); // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction, WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.TextDomainMismatch + } + return $translation; +} + +add_filter( 'gettext', 'woocommerce_blocks_get_php_translation_from_core', 10, 3 ); diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..7a8a857 --- /dev/null +++ b/readme.txt @@ -0,0 +1,220 @@ +=== WooCommerce === +Contributors: automattic, mikejolley, jameskoster, claudiosanches, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman, sadowski, wpmuguru, royho, barryhughes-1 +Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, downloads, payments, paypal, storefront, stripe, woo commerce +Requires at least: 5.6 +Tested up to: 5.8 +Requires PHP: 7.0 +Stable tag: 5.9.0 +License: GPLv3 +License URI: https://www.gnu.org/licenses/gpl-3.0.html + +WooCommerce is the world’s most popular open-source eCommerce solution. + +== Description == + +WooCommerce is [the world’s most popular](https://trends.builtwith.com/shop) open-source eCommerce solution. + +Our core platform is free, flexible, and amplified by a global community. The freedom of open-source means you retain full ownership of your store’s content and data forever. + +Whether you’re launching a business, taking brick-and-mortar retail online, or developing sites for clients, use WooCommerce for a store that powerfully blends content and commerce. + +- **Create beautiful, enticing storefronts** with [themes](https://woocommerce.com/product-category/themes/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) suited to your brand and industry. +- **Customize pages in minutes** using modular [product blocks](https://docs.woocommerce.com/document/woocommerce-blocks/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). +- Showcase physical and digital goods, product variations, custom configurations, instant downloads, and affiliate items. [Bookings](https://woocommerce.com/products/woocommerce-bookings/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [memberships](https://woocommerce.com/products/woocommerce-memberships/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [subscriptions](https://woocommerce.com/products/woocommerce-subscriptions/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), and [dynamic pricing](https://woocommerce.com/products/dynamic-pricing/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) rules are only an extension away. +- **Rise to the top of search results** by leveraging [WordPress’ SEO advantage](https://www.searchenginejournal.com/wordpress-best-cms-seo/). + +Built-in tools and popular integrations help you efficiently manage your business operations. Many services are free to add with a single click via the optional [Setup Wizard](https://docs.woocommerce.com/document/woocommerce-setup-wizard/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + +- **Choose how you want to get paid**. Conveniently manage payments from the comfort of your store with [WooCommerce Payments](https://woocommerce.com/payments/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) (Available in the U.S., U.K., Ireland, Australia, New Zealand, Canada, and now: Spain, France, Germany, and Italy). Securely accept cards, mobile wallets, bank transfers, and cash thanks to [100+ payment gateways](https://woocommerce.com/product-category/woocommerce-extensions/payment-gateways/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) – including [Stripe](https://woocommerce.com/products/stripe/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [PayPal](https://woocommerce.com/products/woocommerce-gateway-paypal-checkout/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), and [Square](https://woocommerce.com/products/square/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). +- **Configure your shipping options**. Print USPS labels right from your dashboard and even schedule a pickup with [WooCommerce Shipping](https://woocommerce.com/products/shipping/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) (U.S.-only). Connect with [well-known carriers](https://woocommerce.com/product-category/woocommerce-extensions/shipping-methods/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) such as UPS, FedEx, and ShipStation – plus a wide variety of delivery, inventory, and fulfillment solutions for your locale. +- **Simplify sales tax**. Add [WooCommerce Tax](https://woocommerce.com/products/tax/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) or [similar integrated services](https://woocommerce.com/product-category/woocommerce-extensions/tax?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) to make automated calculations a reality. + += Grow your business, add features, and monitor your store on the go = + +WooCommerce means business. Keep tabs on the performance metrics most important to you with [WooCommerce Admin](https://wordpress.org/plugins/woocommerce-admin/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) – a powerful, customizable central dashboard for your store. + +Expand your audience across marketing and social channels with [Google Ads](https://woocommerce.com/products/google-ads/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [HubSpot](https://woocommerce.com/products/hubspot-for-woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), [Mailchimp](https://woocommerce.com/products/mailchimp-for-woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), and [Facebook](https://woocommerce.com/products/facebook/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) integrations. You can always check out the in-dashboard [Marketing Hub](https://docs.woocommerce.com/document/marketing-hub/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) for fresh ideas and tips to help you succeed. + +Enhance store functionality with hundreds of free and paid extensions from the [official WooCommerce Marketplace](https://woocommerce.com/products/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). Our developers [vet each new extension](https://docs.woocommerce.com/document/marketplace-overview/#section-6?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) and regularly review existing inventory to maintain Marketplace quality standards. We are actively [looking for products that help store builders create successful stores](https://docs.woocommerce.com/document/marketplace-overview/#section-2?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + +Manage your store from anywhere with the free WooCommerce [mobile app](https://woocommerce.com/mobile/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) (Android and iOS). Spoiler alert: Keep an ear out for the slightly addictive "cha-ching" notification sound each time you make a new sale! + += Own and control your store data – forever = + +With WooCommerce, your data belongs to you. Always. + +If you opt to share [usage data](https://woocommerce.com/usage-tracking/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) with us, you can feel confident knowing that it’s anonymized and kept secure. Choose to opt-out at any time without impacting your store. + +Unlike hosted eCommerce solutions, WooCommerce store data is future-proof; should you wish to migrate to a different platform, you’re free to export all your content and take your site wherever you choose. No restrictions. + += Why developers choose (and love) WooCommerce = + +Developers can use WooCommerce to create, customize, and scale a store to meet a client’s exact specifications, making enhancements through extensions or custom solutions. + +- Leverage [hooks and filters](https://docs.woocommerce.com/document/introduction-to-hooks-actions-and-filters/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) to modify or create functionality. +- Integrate virtually any service using a robust [REST API](https://docs.woocommerce.com/document/woocommerce-rest-api/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) and webhooks. +- Design and build custom content blocks with React. +- [Inspect and modify](https://docs.woocommerce.com/documentation/plugins/woocommerce/woocommerce-codex/extending/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) any aspect of the core plugin code. +- Speed up development with a lightning-fast [CLI](https://woocommerce.github.io/code-reference/classes/wc-cli-rest-command.html?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + +The core platform is tested rigorously and often, supported by a dedicated development team working across time zones. Comprehensive documentation is updated with each release, empowering you to build exactly the store required. + += Be part of our growing international community = + +WooCommerce has a large, passionate community dedicated to helping merchants succeed, and it’s growing fast. + +There are [WooCommerce Meetups](https://woocommerce.com/meetups/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) in locations around the world that you can attend for free and even get involved in running. These events are a great way to learn from others, share your expertise, and connect with like-minded folks. + +WooCommerce also has a regular presence at WordCamps across the globe – we’d love to meet you. + += Contribute and translate = + +WooCommerce is developed and supported by Automattic, the creators of WordPress.com and Jetpack. We also have hundreds of independent contributors, and there’s always room for more. Head to the [WooCommerce GitHub Repository](https://github.com/woocommerce/woocommerce?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) to find out how you can pitch in. + +WooCommerce is translated into multiple languages, including Danish, Ukrainian, and Persian. Help localize WooCommerce even further by adding your locale – visit [translate.wordpress.org](https://translate.wordpress.org/projects/wp-plugins/woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + +== Frequently Asked Questions == + += Where can I find WooCommerce documentation and user guides? = + +For help setting up and configuring WooCommerce, please refer to [Getting Started](https://docs.woocommerce.com/documentation/plugins/woocommerce/getting-started/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) and the [New WooCommerce Store Owner Guide](https://woocommerce.com/guides/new-store/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + +For extending or theming WooCommerce, see our [codex](https://docs.woocommerce.com/documentation/plugins/woocommerce/woocommerce-codex/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), as well as the [Plugin Developer Handbook](https://docs.woocommerce.com/document/create-a-plugin/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + += Where can I get help or talk to other users about WooCommerce Core? = + +If you get stuck, you can ask for help in the [WooCommerce Support Forum](https://wordpress.org/support/plugin/woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) by following [these guidelines](https://wordpress.org/support/topic/guide-to-the-woocommerce-forum/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), reach out via the [WooCommerce Community Slack](https://woocommerce.com/community-slack/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing), or post in the [WooCommerce Community group](https://www.facebook.com/groups/advanced.woocommerce?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) on Facebook. + += Where can I get help for extensions I have purchased on WooCommerce.com? = + +For assistance with paid extensions from the WooCommerce.com Marketplace: first, review our [self-service troubleshooting guide](https://docs.woocommerce.com/document/woocommerce-self-service-guide/). If the problem persists, kindly log a support ticket via [our helpdesk](https://woocommerce.com/my-account/create-a-ticket/). Our dedicated Happiness Engineers aim to respond within 24 hours. + += I’m having trouble logging in to WooCommerce.com – what now? = + +First, troubleshoot common login issues using this helpful [step-by-step guide](https://docs.woocommerce.com/document/log-into-woocommerce-com-with-wordpress-com/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). Still not working? [Get in touch with us](https://woocommerce.com/contact-us/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + += Will WooCommerce work with my theme? = + +Yes! WooCommerce will work with any theme but may require some additional styling. If you’re looking for a theme featuring deep WooCommerce integration, we recommend [Storefront](https://woocommerce.com/storefront/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + += How do I update WooCommerce? = + +We have a detailed guide on [How To Update WooCommerce](https://docs.woocommerce.com/document/how-to-update-woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + += My site broke – what do I do? = + +Start by diagnosing the issue using our helpful [troubleshooting guide](https://docs.woocommerce.com/documentation/get-help/troubleshooting-get-help/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + +If you noticed the error after updating a theme or plugin, there might be compatibility issues between it and WooCommerce. If the issue appeared after updating WooCommerce, there could be a conflict between WooCommerce and an outdated theme or plugin. + +In both instances, we recommend running a conflict test using [Health Check](https://docs.woocommerce.com/document/troubleshooting-using-health-check/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) (which allows you to disable themes and plugins without affecting your visitors) or troubleshooting the issue using a [staging site](https://docs.woocommerce.com/document/how-to-test-for-conflicts/#section-3?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). + += Where can I report bugs? = + +Report bugs on the [WooCommerce GitHub repository](https://github.com/woocommerce/woocommerce/issues?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). You can also notify us via our support forum – be sure to search the forums to confirm that the error has not already been reported. + += Where can I request new features, themes, and extensions? = + +Request new features and extensions and vote on existing suggestions on our official [ideas board](https://ideas.woocommerce.com/forums/133476-woocommerce?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing). Our Product teams regularly review requests and consider them valuable for product planning. + += WooCommerce is awesome! Can I contribute? = + +Yes, you can! Join in on our [GitHub repository](https://github.com/woocommerce/woocommerce/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) and follow the [development blog](https://woocommerce.wordpress.com/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) to stay up-to-date with everything happening in the project. + += Where can I find REST API documentation? = + +Extensive [WooCommerce REST API Documentation](https://woocommerce.github.io/woocommerce-rest-api-docs/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) is available on GitHub. + += My question is not listed here. Where can I find more answers? = + +Check out [Frequently Asked Questions](https://docs.woocommerce.com/document/frequently-asked-questions/?utm_medium=referral&utm_source=wordpress.org&utm_campaign=wp_org_repo_listing) for more. + +== Installation == + += Minimum Requirements = + +* PHP 7.2 or greater is recommended +* MySQL 5.6 or greater is recommended + +Visit the [WooCommerce server requirements documentation](https://docs.woocommerce.com/document/server-requirements/?utm_source=wp%20org%20repo%20listing&utm_content=3.6) for a detailed list of server requirements. + += Automatic installation = + +Automatic installation is the easiest option -- WordPress will handles the file transfer, and you won’t need to leave your web browser. To do an automatic install of WooCommerce, log in to your WordPress dashboard, navigate to the Plugins menu, and click “Add New.” + +In the search field type “WooCommerce,” then click “Search Plugins.” Once you’ve found us, you can view details about it such as the point release, rating, and description. Most importantly of course, you can install it by! Click “Install Now,” and WordPress will take it from there. + += Manual installation = + +Manual installation method requires downloading the WooCommerce plugin and uploading it to your web server via your favorite FTP application. The WordPress codex contains [instructions on how to do this here](https://wordpress.org/support/article/managing-plugins/#manual-plugin-installation). + += Updating = + +Automatic updates should work smoothly, but we still recommend you back up your site. + +If you encounter issues with the shop/category pages after an update, flush the permalinks by going to WordPress > Settings > Permalinks and hitting “Save.” That should return things to normal. + += Sample data = + +WooCommerce comes with some sample data you can use to see how products look; import sample_products.xml via the [WordPress importer](https://wordpress.org/plugins/wordpress-importer/). You can also use the core [CSV importer](https://docs.woocommerce.com/document/product-csv-importer-exporter/?utm_source=wp%20org%20repo%20listing&utm_content=3.6) or our [CSV Import Suite extension](https://woocommerce.com/products/product-csv-import-suite/?utm_source=wp%20org%20repo%20listing&utm_content=3.6) to import sample_products.csv + +== Changelog == + += 5.9.0 2021-11-09 = + +**WooCommerce** + +* Fix - Bug in the handling of remote file names for downloadable files. +* Fix - Remove the absolute path to the currency-info.php from within locale-info.php. #31036 +* Fix - wc_get_price_excluding_tax when an order with no customer is passed. #31015 +* Fix - Rename transient used to cache data for Featured page of In-App Marketplace. #31002 +* Fix - Variable product price caching bug with VAT exemption. #30889 +* Fix - Allow to pass null as the email for billing addresses in REST API. #30850 +* Fix - Ensure woocommerce_cancel_unpaid_orders event is always re-scheduled. #30830 +* Fix - Use a more standard way to check if the product attributes lookup table exists. #30745 +* Fix - Undefined variable notice when trying to add product in orders without specifying a product. #30739 +* Fix - Use proper location for taxes when adding products via admin. #30692 +* Dev - Add mobile data to WCTracker. #30415 +* Tweak - Remove hardcode category banners in Settings > Marketplace and use the WooCommerce.com API instead. #30938 +* Tweak - Show a search again message when marketplace results are empty. #30642 +* Tweak - Add promoted cards styling to marketplace section. #30861 +* Enhancement - Add ratings, reviews and icons into Marketplace's Product Cards. #30840 +* Enhancement - Update Storefront banner width and track links in the marketplace page. #30882 +* Enhancement - Revamp the WooCommerce Marketplace page. #30900 + +**WooCommerce Admin - 2.8.0 ** + +* Fix - Issue where stock activity panel was not rendering correctly. #7817 +* Fix - Increase CSS specificity to avoid conflicts and broken panel styling. #7813 +* Fix - Updated link to WooCommerce Developers Blog in readme.txt. #7824 +* Fix - Fixed navigation menu text color after Gutenberg 11.6.0. #7771 +* Fix - Add status param to notes/delete/all REST endpoint, to correctly delete all notes. #7743 +* Fix - Allow already installed marketing extensions to be activated. #7740 +* Fix - Add missing title text for marketing task. #7640 +* Fix - Assign parent order status as children order status if refund order. #7253 +* Fix - Fix category lookup logic to update children correctly. #7709 +* Fix - Fixing an unwanted page refresh when using Woo Navigation. #7615 +* Fix - Fix naming of event names and properties. #7677 +* Fix - Fix white screen for variation analytic data without a name. #7686 +* Add - Store Profiler and Product task - include Subscriptions. #7734 +* Update - Update WC pay supported country list for the default free extensions. #7873 +* Update - Update back up copy of free extension for Google Listing & Ads plugin. #7798 +* Update - Update Eway payment gateway capitalization (was eWAY). #7678 +* Update - Enable Square in France. #7679 +* Enhancement - Only load tasks during rest api requests. #7856 +* Enhancement - Add experiment for promoting WooCommerce Payments in payment methods table. #7666 + +**WooCommerce Blocks - 6.0.0 & 6.0.1 & 6.0.2 & 6.1.0** + +* Fix - Infinite recursion when removing an attribute filter from the Active filters block. #4816 +* Fix - Update All Reviews block so it honors 'ratings enabled' and 'show avatars' preferences. #4764 +* Fix - Products by Category: Moved renderEmptyResponsePlaceholder to separate method to prevent unnecessary rerender. #4751 +* Fix - Calculation of number of reviews in the Reviews by Category block. #4729 +* Fix - Dropdown list in Product Category List Block for nested categories #4920 +* Fix - String translations within the All Products Block. #4897 +* Fix - Filter By Price: Update aria values to be more representative of the actual values presented. #4839 +* Fix - Filter button from Filter Products by Attribute block is not aligned with the input field. #4814 +* Fix - Remove IntersectionObserver shim in favor of dropping IE11 support. #4808 +* Enhancement - Added global styles to All Reviews, Reviews by Category and Reviews by Product blocks. Now it's possible to change the text color and font size of those blocks. #4323 + +[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/changelog.txt). diff --git a/sample-data/sample_products.csv b/sample-data/sample_products.csv new file mode 100644 index 0000000..dfb25e8 --- /dev/null +++ b/sample-data/sample_products.csv @@ -0,0 +1,26 @@ +ID,Type,SKU,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL" +44,variable,woo-vneck-tee,"V-Neck T-Shirt",1,1,visible,"This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.5,24,1,2,1,,,,"Clothing > Tshirts",,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg",,,,,,,,,0,Color,"Blue, Green, Red",1,1,Size,"Large, Medium, Small",1,1,1,,,, +45,variable,woo-hoodie,Hoodie,1,0,visible,"This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1.5,10,8,3,1,,,,"Clothing > Hoodies",,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,,,,,,0,Color,"Blue, Green, Red",1,1,Logo,"Yes, No",1,0,1,,,, +46,simple,woo-hoodie-with-logo,"Hoodie with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,2,10,6,3,1,,,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,,,,,,,0,Color,Blue,1,1,,,,,1,,,, +47,simple,woo-tshirt,T-Shirt,1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.8,8,6,1,1,,,18,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/tshirt-2.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, +48,simple,woo-beanie,Beanie,1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,4,5,.5,1,,18,20,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-2.jpg,,,,,,,,,0,Color,Red,1,1,,,,,1,,,, +58,simple,woo-belt,Belt,1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1.2,12,2,1.5,1,,55,65,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/belt-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, +60,simple,woo-cap,Cap,1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,0.6,8,6.5,4,1,,16,18,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/cap-2.jpg,,,,,,,,,0,Color,Yellow,1,1,,,,,1,,,, +62,simple,woo-sunglasses,Sunglasses,1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,4,1.4,1,1,,,90,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/sunglasses-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, +64,simple,woo-hoodie-with-pocket,"Hoodie with Pocket",1,1,hidden,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,3,10,8,2,1,,35,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-pocket-2.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, +66,simple,woo-hoodie-with-zipper,"Hoodie with Zipper",1,1,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,2,8,6,2,1,,,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-zipper-2.jpg,,,,,,,,,0,,,,,,,,,1,,,, +68,simple,woo-long-sleeve-tee,"Long Sleeve Tee",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,1,7,5,1,1,,,25,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/long-sleeve-tee-2.jpg,,,,,,,,,0,Color,Green,1,1,,,,,1,,,, +70,simple,woo-polo,Polo,1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.8,6,5,1,1,,,20,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/polo-2.jpg,,,,,,,,,0,Color,Blue,1,1,,,,,1,,,, +73,"simple, downloadable, virtual",woo-album,Album,1,0,visible,"This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,1,,,15,Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg,1,1,,,,,,,0,,,,,,,,,1,"Single 1",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg,"Single 2",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg +75,"simple, downloadable, virtual",woo-single,Single,1,0,visible,"This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,1,,2,3,Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/single-1.jpg,1,1,,,,,,,0,,,,,,,,,1,Single,https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg,, +76,variation,woo-vneck-tee-red,"V-Neck T-Shirt - Red",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,20,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg,,,woo-vneck-tee,,,,,,0,Color,Red,,1,Size,,,1,,,,, +77,variation,woo-vneck-tee-green,"V-Neck T-Shirt - Green",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,20,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg,,,woo-vneck-tee,,,,,,0,Color,Green,,1,Size,,,1,,,,, +78,variation,woo-vneck-tee-blue,"V-Neck T-Shirt - Blue",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,15,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg,,,woo-vneck-tee,,,,,,0,Color,Blue,,1,Size,,,1,,,,, +79,variation,woo-hoodie-red,"Hoodie - Red, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,42,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg,,,woo-hoodie,,,,,,1,Color,Red,,1,Logo,No,,0,,,,, +80,variation,woo-hoodie-green,"Hoodie - Green, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg,,,woo-hoodie,,,,,,2,Color,Green,,1,Logo,No,,0,,,,, +81,variation,woo-hoodie-blue,"Hoodie - Blue, No",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg,,,woo-hoodie,,,,,,3,Color,Blue,,1,Logo,No,,0,,,,, +83,simple,Woo-tshirt-logo,"T-Shirt with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.5,10,12,.5,1,,,18,"Clothing > Tshirts",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg,,,,,,,,,0,Color,Gray,1,1,,,,,1,,,, +85,simple,Woo-beanie-logo,"Beanie with Logo",1,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.2,6,4,1,1,,18,20,"Clothing > Accessories",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg,,,,,,,,,0,Color,Red,1,1,,,,,1,,,, +87,grouped,logo-collection,"Logo Collection",1,0,visible,"This is a grouped product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,,,,,1,,,,Clothing,,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg",,,,"woo-hoodie-with-logo, woo-tshirt, woo-beanie",,,,,0,,,,,,,,,1,,,, +89,external,wp-pennant,"WordPress Pennant",1,0,visible,"This is an external product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,,,,,1,,,11.05,Decor,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/pennant-1.jpg,,,,,,,https://mercantile.wordpress.org/product/wordpress-pennant/,"Buy on the WordPress swag store!",0,,,,,,,,,1,,,, +90,variation,woo-hoodie-blue-logo,"Hoodie - Blue, Yes",1,0,visible,,"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,0,,,45,,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,woo-hoodie,,,,,,0,Color,Blue,,1,Logo,Yes,,0,,,,, diff --git a/sample-data/sample_products.xml b/sample-data/sample_products.xml new file mode 100644 index 0000000..e8e57ec --- /dev/null +++ b/sample-data/sample_products.xml @@ -0,0 +1,4854 @@ + + + + + +WooCommerce Demo Store +http: +Just another WooCommerce store +Wed, 16 Jan 2019 13:09:24 +0000 +en-US +1.2 +http: +http: + + 1 + shopmanager + info@woocommerce.com + + + + +https://wordpress.org/?v=5.0.3 + + V-Neck T-Shirt + https://woocommercecore.mystagingwebsite.com/product/v-neck-t-shirt/ + Wed, 16 Jan 2019 13:01:52 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/v-neck-t-shirt/ + + + + 6 + 2019-01-16 13:01:52 + 2019-01-16 13:01:52 + open + closed + v-neck-t-shirt + publish + 0 + 0 + product + + 0 + + + + + + + + + + + _sku + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _thumbnail_id + + + + _price + + + + _price + + + + _regular_price + + + + _sale_price + + + + + Hoodie + https://woocommercecore.mystagingwebsite.com/product/hoodie/ + Wed, 16 Jan 2019 13:01:52 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/hoodie/ + + + + 7 + 2019-01-16 13:01:52 + 2019-01-16 13:01:52 + open + closed + hoodie + publish + 0 + 0 + product + + 0 + + + + + + + _sku + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _thumbnail_id + + + + _price + + + + _price + + + + _regular_price + + + + _sale_price + + + + + Hoodie with Logo + https://woocommercecore.mystagingwebsite.com/product/hoodie-with-logo/ + Wed, 16 Jan 2019 13:01:52 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/hoodie-with-logo/ + + + + 8 + 2019-01-16 13:01:52 + 2019-01-16 13:01:52 + open + closed + hoodie-with-logo + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + T-Shirt + https://woocommercecore.mystagingwebsite.com/product/t-shirt/ + Wed, 16 Jan 2019 13:01:52 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/t-shirt/ + + + + 9 + 2019-01-16 13:01:52 + 2019-01-16 13:01:52 + open + closed + t-shirt + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Beanie + https://woocommercecore.mystagingwebsite.com/product/beanie/ + Wed, 16 Jan 2019 13:01:52 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/beanie/ + + + + 10 + 2019-01-16 13:01:52 + 2019-01-16 13:01:52 + open + closed + beanie + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Belt + https://woocommercecore.mystagingwebsite.com/product/belt/ + Wed, 16 Jan 2019 13:01:52 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/belt/ + + + + 11 + 2019-01-16 13:01:52 + 2019-01-16 13:01:52 + open + closed + belt + publish + 0 + 0 + product + + 0 + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Cap + https://woocommercecore.mystagingwebsite.com/product/cap/ + Wed, 16 Jan 2019 13:01:53 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/cap/ + + + + 12 + 2019-01-16 13:01:53 + 2019-01-16 13:01:53 + open + closed + cap + publish + 0 + 0 + product + + 0 + + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Sunglasses + https://woocommercecore.mystagingwebsite.com/product/sunglasses/ + Wed, 16 Jan 2019 13:01:53 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/sunglasses/ + + + + 13 + 2019-01-16 13:01:53 + 2019-01-16 13:01:53 + open + closed + sunglasses + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Hoodie with Pocket + https://woocommercecore.mystagingwebsite.com/product/hoodie-with-pocket/ + Wed, 16 Jan 2019 13:01:53 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/hoodie-with-pocket/ + + + + 14 + 2019-01-16 13:01:53 + 2019-01-16 13:01:53 + open + closed + hoodie-with-pocket + publish + 0 + 0 + product + + 0 + + + + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Hoodie with Zipper + https://woocommercecore.mystagingwebsite.com/product/hoodie-with-zipper/ + Wed, 16 Jan 2019 13:01:53 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/hoodie-with-zipper/ + + + + 15 + 2019-01-16 13:01:53 + 2019-01-16 13:01:53 + open + closed + hoodie-with-zipper + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Long Sleeve Tee + https://woocommercecore.mystagingwebsite.com/product/long-sleeve-tee/ + Wed, 16 Jan 2019 13:01:53 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/long-sleeve-tee/ + + + + 16 + 2019-01-16 13:01:53 + 2019-01-16 13:01:53 + open + closed + long-sleeve-tee + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Polo + https://woocommercecore.mystagingwebsite.com/product/polo/ + Wed, 16 Jan 2019 13:01:53 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/polo/ + + + + 17 + 2019-01-16 13:01:53 + 2019-01-16 13:01:53 + open + closed + polo + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Album + https://woocommercecore.mystagingwebsite.com/product/album/ + Wed, 16 Jan 2019 13:01:54 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/album/ + + + + 18 + 2019-01-16 13:01:54 + 2019-01-16 13:01:54 + open + closed + album + publish + 0 + 0 + product + + 0 + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Single + https://woocommercecore.mystagingwebsite.com/product/single/ + Wed, 16 Jan 2019 13:01:54 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/single/ + + + + 19 + 2019-01-16 13:01:54 + 2019-01-16 13:01:54 + open + closed + single + publish + 0 + 0 + product + + 0 + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + V-Neck T-Shirt - Red + https://woocommercecore.mystagingwebsite.com/product/v-neck-t-shirt/?attribute_pa_color=red + Wed, 16 Jan 2019 13:01:54 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/v-neck-t-shirt-red/ + + + + 20 + 2019-01-16 13:01:54 + 2019-01-16 13:01:54 + closed + closed + v-neck-t-shirt-red + publish + 6 + 0 + product_variation + + 0 + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _variation_description + + + + _thumbnail_id + + + + attribute_pa_color + + + + attribute_pa_size + + + + + V-Neck T-Shirt - Green + https://woocommercecore.mystagingwebsite.com/product/v-neck-t-shirt/?attribute_pa_color=green + Wed, 16 Jan 2019 13:01:54 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/v-neck-t-shirt-green/ + + + + 21 + 2019-01-16 13:01:54 + 2019-01-16 13:01:54 + closed + closed + v-neck-t-shirt-green + publish + 6 + 0 + product_variation + + 0 + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _variation_description + + + + _thumbnail_id + + + + attribute_pa_color + + + + attribute_pa_size + + + + + V-Neck T-Shirt - Blue + https://woocommercecore.mystagingwebsite.com/product/v-neck-t-shirt/?attribute_pa_color=blue + Wed, 16 Jan 2019 13:01:54 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/v-neck-t-shirt-blue/ + + + + 22 + 2019-01-16 13:01:54 + 2019-01-16 13:01:54 + closed + closed + v-neck-t-shirt-blue + publish + 6 + 0 + product_variation + + 0 + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _wpcom_is_markdown + + + + _wp_old_slug + + + + _variation_description + + + + _thumbnail_id + + + + attribute_pa_color + + + + attribute_pa_size + + + + + Hoodie - Red, No + https://woocommercecore.mystagingwebsite.com/product/hoodie/?attribute_pa_color=red&attribute_logo=no + Wed, 16 Jan 2019 13:01:54 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/hoodie-red-no + + + + 23 + 2019-01-16 13:01:54 + 2019-01-16 13:01:54 + closed + closed + hoodie-red-no + publish + 7 + 1 + product_variation + + 0 + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _variation_description + + + + _thumbnail_id + + + + attribute_pa_color + + + + attribute_logo + + + + + Hoodie - Green, No + https://woocommercecore.mystagingwebsite.com/product/hoodie/?attribute_pa_color=green&attribute_logo=No + Wed, 16 Jan 2019 13:01:54 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/hoodie-green-no/ + + + + 24 + 2019-01-16 13:01:54 + 2019-01-16 13:01:54 + closed + closed + hoodie-green-no + publish + 7 + 2 + product_variation + + 0 + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _variation_description + + + + _thumbnail_id + + + + attribute_pa_color + + + + attribute_logo + + + + + Hoodie - Blue, No + https://woocommercecore.mystagingwebsite.com/product/hoodie/?attribute_pa_color=blue&attribute_logo=No + Wed, 16 Jan 2019 13:01:55 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/hoodie-blue-no + + + + 25 + 2019-01-16 13:01:55 + 2019-01-16 13:01:55 + closed + closed + hoodie-blue-no + publish + 7 + 3 + product_variation + + 0 + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _variation_description + + + + _thumbnail_id + + + + attribute_pa_color + + + + attribute_logo + + + + + T-Shirt with Logo + https://woocommercecore.mystagingwebsite.com/product/t-shirt-with-logo/ + Wed, 16 Jan 2019 13:01:55 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/t-shirt-with-logo/ + + + + 26 + 2019-01-16 13:01:55 + 2019-01-16 13:01:55 + open + closed + t-shirt-with-logo + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Beanie with Logo + https://woocommercecore.mystagingwebsite.com/product/beanie-with-logo/ + Wed, 16 Jan 2019 13:01:55 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/beanie-with-logo/ + + + + 27 + 2019-01-16 13:01:55 + 2019-01-16 13:01:55 + open + closed + beanie-with-logo + publish + 0 + 0 + product + + 0 + + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + + + + + + + + + + + + + + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + + Logo Collection + https://woocommercecore.mystagingwebsite.com/product/logo-collection/ + Wed, 16 Jan 2019 13:01:55 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/logo-collection/ + + + + 28 + 2019-01-16 13:01:55 + 2019-01-16 13:01:55 + open + closed + logo-collection + publish + 0 + 0 + product + + 0 + + + + _sku + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _children + + + + _thumbnail_id + + + + _price + + + + _price + + + + + WordPress Pennant + https://woocommercecore.mystagingwebsite.com/product/wordpress-pennant/ + Wed, 16 Jan 2019 13:01:55 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/wordpress-pennant/ + + + + 29 + 2019-01-16 13:01:55 + 2019-01-16 13:01:55 + open + closed + wordpress-pennant + publish + 0 + 0 + product + + 0 + + + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _thumbnail_id + + + + _product_url + + + + _button_text + + + + + Hoodie - Blue, Yes + https://woocommercecore.mystagingwebsite.com/product/hoodie/?attribute_pa_color=blue&attribute_logo=Yes + Wed, 16 Jan 2019 13:01:55 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/product/hoodie-blue-yes/ + + + + 30 + 2019-01-16 13:01:55 + 2019-01-16 13:01:55 + closed + closed + hoodie-blue-yes + publish + 7 + 0 + product_variation + + 0 + + _sku + + + + _regular_price + + + + _sale_price + + + + _sale_price_dates_from + + + + _sale_price_dates_to + + + + total_sales + + + + _tax_status + + + + _tax_class + + + + _manage_stock + + + + _backorders + + + + _low_stock_amount + + + + _sold_individually + + + + _weight + + + + _length + + + + _width + + + + _height + + + + _upsell_ids + + + + _crosssell_ids + + + + _purchase_note + + + + _default_attributes + + + + _virtual + + + + _downloadable + + + + _product_image_gallery + + + + _download_limit + + + + _download_expiry + + + + _stock + + + + _stock_status + + + + _wc_average_rating + + + + _wc_rating_count + + + + _wc_review_count + + + + _downloadable_files + + + + _product_attributes + + + + _product_version + + + + _price + + + + _variation_description + + + + _thumbnail_id + + + + attribute_pa_color + + + + attribute_logo + + + + + vneck-tee-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=31 + Wed, 16 Jan 2019 13:01:56 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg + + + + 31 + 2019-01-16 13:01:56 + 2019-01-16 13:01:56 + open + closed + vneck-tee-2-jpg + inherit + 6 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg + + _wc_attachment_source + + + + + vnech-tee-green-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=32 + Wed, 16 Jan 2019 13:01:57 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg + + + + 32 + 2019-01-16 13:01:57 + 2019-01-16 13:01:57 + open + closed + vnech-tee-green-1-jpg + inherit + 6 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg + + _wc_attachment_source + + + + + vnech-tee-blue-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=33 + Wed, 16 Jan 2019 13:01:58 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg + + + + 33 + 2019-01-16 13:01:58 + 2019-01-16 13:01:58 + open + closed + vnech-tee-blue-1-jpg + inherit + 6 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg + + _wc_attachment_source + + + + + hoodie-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=34 + Wed, 16 Jan 2019 13:01:58 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg + + + + 34 + 2019-01-16 13:01:58 + 2019-01-16 13:01:58 + open + closed + hoodie-2-jpg + inherit + 7 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-2.jpg + + _wc_attachment_source + + + + + hoodie-blue-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=35 + Wed, 16 Jan 2019 13:01:59 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg + + + + 35 + 2019-01-16 13:01:59 + 2019-01-16 13:01:59 + open + closed + hoodie-blue-1-jpg + inherit + 7 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-blue-1.jpg + + _wc_attachment_source + + + + + hoodie-green-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=36 + Wed, 16 Jan 2019 13:02:00 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg + + + + 36 + 2019-01-16 13:02:00 + 2019-01-16 13:02:00 + open + closed + hoodie-green-1-jpg + inherit + 7 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-green-1.jpg + + _wc_attachment_source + + + + + hoodie-with-logo-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=37 + Wed, 16 Jan 2019 13:02:01 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg + + + + 37 + 2019-01-16 13:02:01 + 2019-01-16 13:02:01 + open + closed + hoodie-with-logo-2-jpg + inherit + 7 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg + + _wc_attachment_source + + + + + tshirt-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=38 + Wed, 16 Jan 2019 13:02:02 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/tshirt-2.jpg + + + + 38 + 2019-01-16 13:02:02 + 2019-01-16 13:02:02 + open + closed + tshirt-2-jpg + inherit + 9 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/tshirt-2.jpg + + _wc_attachment_source + + + + + beanie-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=39 + Wed, 16 Jan 2019 13:02:02 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-2.jpg + + + + 39 + 2019-01-16 13:02:02 + 2019-01-16 13:02:02 + open + closed + beanie-2-jpg + inherit + 10 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-2.jpg + + _wc_attachment_source + + + + + belt-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=40 + Wed, 16 Jan 2019 13:02:03 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/belt-2.jpg + + + + 40 + 2019-01-16 13:02:03 + 2019-01-16 13:02:03 + open + closed + belt-2-jpg + inherit + 11 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/belt-2.jpg + + _wc_attachment_source + + + + + cap-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=41 + Wed, 16 Jan 2019 13:02:04 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/cap-2.jpg + + + + 41 + 2019-01-16 13:02:04 + 2019-01-16 13:02:04 + open + closed + cap-2-jpg + inherit + 12 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/cap-2.jpg + + _wc_attachment_source + + + + + sunglasses-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=42 + Wed, 16 Jan 2019 13:02:05 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/sunglasses-2.jpg + + + + 42 + 2019-01-16 13:02:05 + 2019-01-16 13:02:05 + open + closed + sunglasses-2-jpg + inherit + 13 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/sunglasses-2.jpg + + _wc_attachment_source + + + + + hoodie-with-pocket-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=43 + Wed, 16 Jan 2019 13:02:06 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-pocket-2.jpg + + + + 43 + 2019-01-16 13:02:06 + 2019-01-16 13:02:06 + open + closed + hoodie-with-pocket-2-jpg + inherit + 14 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-pocket-2.jpg + + _wc_attachment_source + + + + + hoodie-with-zipper-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=44 + Wed, 16 Jan 2019 13:02:06 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-zipper-2.jpg + + + + 44 + 2019-01-16 13:02:06 + 2019-01-16 13:02:06 + open + closed + hoodie-with-zipper-2-jpg + inherit + 15 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-zipper-2.jpg + + _wc_attachment_source + + + + + long-sleeve-tee-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=45 + Wed, 16 Jan 2019 13:02:07 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/long-sleeve-tee-2.jpg + + + + 45 + 2019-01-16 13:02:07 + 2019-01-16 13:02:07 + open + closed + long-sleeve-tee-2-jpg + inherit + 16 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/long-sleeve-tee-2.jpg + + _wc_attachment_source + + + + + polo-2.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=46 + Wed, 16 Jan 2019 13:02:08 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/polo-2.jpg + + + + 46 + 2019-01-16 13:02:08 + 2019-01-16 13:02:08 + open + closed + polo-2-jpg + inherit + 17 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/polo-2.jpg + + _wc_attachment_source + + + + + album-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=47 + Wed, 16 Jan 2019 13:02:09 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg + + + + 47 + 2019-01-16 13:02:09 + 2019-01-16 13:02:09 + open + closed + album-1-jpg + inherit + 18 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg + + _wc_attachment_source + + + + + single-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=48 + Wed, 16 Jan 2019 13:02:10 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/single-1.jpg + + + + 48 + 2019-01-16 13:02:10 + 2019-01-16 13:02:10 + open + closed + single-1-jpg + inherit + 19 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/single-1.jpg + + _wc_attachment_source + + + + + t-shirt-with-logo-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=49 + Wed, 16 Jan 2019 13:02:11 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg + + + + 49 + 2019-01-16 13:02:11 + 2019-01-16 13:02:11 + open + closed + t-shirt-with-logo-1-jpg + inherit + 26 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/t-shirt-with-logo-1.jpg + + _wc_attachment_source + + + + + beanie-with-logo-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=50 + Wed, 16 Jan 2019 13:02:12 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg + + + + 50 + 2019-01-16 13:02:12 + 2019-01-16 13:02:12 + open + closed + beanie-with-logo-1-jpg + inherit + 27 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/beanie-with-logo-1.jpg + + _wc_attachment_source + + + + + logo-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=51 + Wed, 16 Jan 2019 13:02:13 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/logo-1.jpg + + + + 51 + 2019-01-16 13:02:13 + 2019-01-16 13:02:13 + open + closed + logo-1-jpg + inherit + 28 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/logo-1.jpg + + _wc_attachment_source + + + + + pennant-1.jpg + https://woocommercecore.mystagingwebsite.com/?attachment_id=52 + Wed, 16 Jan 2019 13:02:13 +0000 + shopmanager + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/pennant-1.jpg + + + + 52 + 2019-01-16 13:02:13 + 2019-01-16 13:02:13 + open + closed + pennant-1-jpg + inherit + 29 + 0 + attachment + + 0 + https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/pennant-1.jpg + + _wc_attachment_source + + + + + diff --git a/sample-data/sample_tax_rates.csv b/sample-data/sample_tax_rates.csv new file mode 100644 index 0000000..02038b5 --- /dev/null +++ b/sample-data/sample_tax_rates.csv @@ -0,0 +1,6 @@ +Country Code,State Code,ZIP/Postcode,City,Rate %,Tax Name,Priority,Compound,Shipping,Tax Class +GB,*,*,*,20.0000,VAT,1,1,1, +GB,*,*,*,5.0000,VAT,1,1,1,reduced-rate +GB,*,*,*,0.0000,VAT,1,1,1,zero-rate +US,*,*,*,10.0000,US,1,1,1, +US,AL,12345; 123456,*,2.0000,US AL,2,1,1, \ No newline at end of file diff --git a/src/Autoloader.php b/src/Autoloader.php new file mode 100644 index 0000000..53f802e --- /dev/null +++ b/src/Autoloader.php @@ -0,0 +1,74 @@ + +
    +

    + ', + '' + ); + ?> +

    +
    + enabled = get_option( 'woocommerce_schema_version', 0 ) >= 430; + } + + /** + * Is stock reservation enabled? + * + * @return boolean + */ + protected function is_enabled() { + return $this->enabled; + } + + /** + * Query for any existing holds on stock for this item. + * + * @param \WC_Product $product Product to get reserved stock for. + * @param integer $exclude_order_id Optional order to exclude from the results. + * + * @return integer Amount of stock already reserved. + */ + public function get_reserved_stock( $product, $exclude_order_id = 0 ) { + global $wpdb; + + if ( ! $this->is_enabled() ) { + return 0; + } + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared + return (int) $wpdb->get_var( $this->get_query_for_reserved_stock( $product->get_stock_managed_by_id(), $exclude_order_id ) ); + } + + /** + * Put a temporary hold on stock for an order if enough is available. + * + * @throws ReserveStockException If stock cannot be reserved. + * + * @param \WC_Order $order Order object. + * @param int $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes. + */ + public function reserve_stock_for_order( $order, $minutes = 0 ) { + $minutes = $minutes ? $minutes : (int) get_option( 'woocommerce_hold_stock_minutes', 60 ); + + if ( ! $minutes || ! $this->is_enabled() ) { + return; + } + + try { + $items = array_filter( + $order->get_items(), + function( $item ) { + return $item->is_type( 'line_item' ) && $item->get_product() instanceof \WC_Product && $item->get_quantity() > 0; + } + ); + $rows = array(); + + foreach ( $items as $item ) { + $product = $item->get_product(); + + if ( ! $product->is_in_stock() ) { + throw new ReserveStockException( + 'woocommerce_product_out_of_stock', + sprintf( + /* translators: %s: product name */ + __( '"%s" is out of stock and cannot be purchased.', 'woocommerce' ), + $product->get_name() + ), + 403 + ); + } + + // If stock management is off, no need to reserve any stock here. + if ( ! $product->managing_stock() || $product->backorders_allowed() ) { + continue; + } + + $managed_by_id = $product->get_stock_managed_by_id(); + + /** + * Filter order item quantity. + * + * @param int|float $quantity Quantity. + * @param WC_Order $order Order data. + * @param WC_Order_Item_Product $item Order item data. + */ + $item_quantity = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); + + $rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item_quantity : $item_quantity; + } + + if ( ! empty( $rows ) ) { + foreach ( $rows as $product_id => $quantity ) { + $this->reserve_stock_for_product( $product_id, $quantity, $order, $minutes ); + } + } + } catch ( ReserveStockException $e ) { + $this->release_stock_for_order( $order ); + throw $e; + } + } + + /** + * Release a temporary hold on stock for an order. + * + * @param \WC_Order $order Order object. + */ + public function release_stock_for_order( $order ) { + global $wpdb; + + if ( ! $this->is_enabled() ) { + return; + } + + $wpdb->delete( + $wpdb->wc_reserved_stock, + array( + 'order_id' => $order->get_id(), + ) + ); + } + + /** + * Reserve stock for a product by inserting rows into the DB. + * + * @throws ReserveStockException If a row cannot be inserted. + * + * @param int $product_id Product ID which is having stock reserved. + * @param int $stock_quantity Stock amount to reserve. + * @param \WC_Order $order Order object which contains the product. + * @param int $minutes How long to reserve stock in minutes. + */ + private function reserve_stock_for_product( $product_id, $stock_quantity, $order, $minutes ) { + global $wpdb; + + $product_data_store = \WC_Data_Store::load( 'product' ); + $query_for_stock = $product_data_store->get_query_for_stock( $product_id ); + $query_for_reserved_stock = $this->get_query_for_reserved_stock( $product_id, $order->get_id() ); + + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared + $result = $wpdb->query( + $wpdb->prepare( + " + INSERT INTO {$wpdb->wc_reserved_stock} ( `order_id`, `product_id`, `stock_quantity`, `timestamp`, `expires` ) + SELECT %d, %d, %d, NOW(), ( NOW() + INTERVAL %d MINUTE ) FROM DUAL + WHERE ( $query_for_stock FOR UPDATE ) - ( $query_for_reserved_stock FOR UPDATE ) >= %d + ON DUPLICATE KEY UPDATE `expires` = VALUES( `expires` ), `stock_quantity` = VALUES( `stock_quantity` ) + ", + $order->get_id(), + $product_id, + $stock_quantity, + $minutes, + $stock_quantity + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared + + if ( ! $result ) { + $product = wc_get_product( $product_id ); + throw new ReserveStockException( + 'woocommerce_product_not_enough_stock', + sprintf( + /* translators: %s: product name */ + __( 'Not enough units of %s are available in stock to fulfil this order.', 'woocommerce' ), + $product ? $product->get_name() : '#' . $product_id + ), + 403 + ); + } + } + + /** + * Returns query statement for getting reserved stock of a product. + * + * @param int $product_id Product ID. + * @param integer $exclude_order_id Optional order to exclude from the results. + * @return string|void Query statement. + */ + private function get_query_for_reserved_stock( $product_id, $exclude_order_id = 0 ) { + global $wpdb; + $query = $wpdb->prepare( + " + SELECT COALESCE( SUM( stock_table.`stock_quantity` ), 0 ) FROM $wpdb->wc_reserved_stock stock_table + LEFT JOIN $wpdb->posts posts ON stock_table.`order_id` = posts.ID + WHERE posts.post_status IN ( 'wc-checkout-draft', 'wc-pending' ) + AND stock_table.`expires` > NOW() + AND stock_table.`product_id` = %d + AND stock_table.`order_id` != %d + ", + $product_id, + $exclude_order_id + ); + + /** + * Filter: woocommerce_query_for_reserved_stock + * Allows to filter the query for getting reserved stock of a product. + * + * @since 4.5.0 + * @param string $query The query for getting reserved stock of a product. + * @param int $product_id Product ID. + * @param int $exclude_order_id Order to exclude from the results. + */ + return apply_filters( 'woocommerce_query_for_reserved_stock', $query, $product_id, $exclude_order_id ); + } +} diff --git a/src/Checkout/Helpers/ReserveStockException.php b/src/Checkout/Helpers/ReserveStockException.php new file mode 100644 index 0000000..0122ff9 --- /dev/null +++ b/src/Checkout/Helpers/ReserveStockException.php @@ -0,0 +1,60 @@ +error_code = $code; + $this->error_data = $data; + + parent::__construct( $message, $http_status_code ); + } + + /** + * Returns the error code. + * + * @return string + */ + public function getErrorCode() { + return $this->error_code; + } + + /** + * Returns error data. + * + * @return array + */ + public function getErrorData() { + return $this->error_data; + } +} diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..b0b99b9 --- /dev/null +++ b/src/Container.php @@ -0,0 +1,97 @@ +container = new ExtendedContainer(); + + // Add ourselves as the shared instance of ContainerInterface, + // register everything else using service providers. + + $this->container->share( \Psr\Container\ContainerInterface::class, $this ); + + foreach ( $this->service_providers as $service_provider_class ) { + $this->container->addServiceProvider( $service_provider_class ); + } + } + + /** + * Finds an entry of the container by its identifier and returns it. + * + * @param string $id Identifier of the entry to look for. + * + * @throws NotFoundExceptionInterface No entry was found for **this** identifier. + * @throws Psr\Container\ContainerExceptionInterface Error while retrieving the entry. + * + * @return mixed Entry. + */ + public function get( $id ) { + return $this->container->get( $id ); + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * `has($id)` returning true does not mean that `get($id)` will not throw an exception. + * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. + * + * @param string $id Identifier of the entry to look for. + * + * @return bool + */ + public function has( $id ) { + return $this->container->has( $id ); + } +} diff --git a/src/Internal/AssignDefaultCategory.php b/src/Internal/AssignDefaultCategory.php new file mode 100644 index 0000000..34968e9 --- /dev/null +++ b/src/Internal/AssignDefaultCategory.php @@ -0,0 +1,73 @@ +queue()->schedule_single( + time(), + 'wc_schedule_update_product_default_cat', + array(), + 'wc_update_product_default_cat' + ); + } + + /** + * Assigns default product category for products + * that have no categories. + * + * @return void + */ + public function maybe_assign_default_product_cat() { + global $wpdb; + + $default_category = get_option( 'default_product_cat', 0 ); + + if ( $default_category ) { + $wpdb->query( + $wpdb->prepare( + "INSERT INTO {$wpdb->term_relationships} (object_id, term_taxonomy_id) + SELECT DISTINCT posts.ID, %s FROM {$wpdb->posts} posts + LEFT JOIN + ( + SELECT object_id FROM {$wpdb->term_relationships} term_relationships + LEFT JOIN {$wpdb->term_taxonomy} term_taxonomy ON term_relationships.term_taxonomy_id = term_taxonomy.term_taxonomy_id + WHERE term_taxonomy.taxonomy = 'product_cat' + ) AS tax_query + ON posts.ID = tax_query.object_id + WHERE posts.post_type = 'product' + AND tax_query.object_id IS NULL", + $default_category + ) + ); + wp_cache_flush(); + delete_transient( 'wc_term_counts' ); + wp_update_term_count_now( array( $default_category ), 'product_cat' ); + } + } +} diff --git a/src/Internal/DependencyManagement/AbstractServiceProvider.php b/src/Internal/DependencyManagement/AbstractServiceProvider.php new file mode 100644 index 0000000..bc35e90 --- /dev/null +++ b/src/Internal/DependencyManagement/AbstractServiceProvider.php @@ -0,0 +1,172 @@ +getContainer()`. + */ +abstract class AbstractServiceProvider extends BaseServiceProvider { + + /** + * Register a class in the container and use reflection to guess the injection method arguments. + * + * WARNING: this method uses reflection, so please have performance in mind when using it. + * + * @param string $class_name Class name to register. + * @param mixed $concrete The concrete to register. Can be a shared instance, a factory callback, or a class name. + * @param bool $shared Whether to register the class as shared (`get` always returns the same instance) or not. + * + * @return DefinitionInterface The generated container definition. + * + * @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint. + */ + protected function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface { + $definition = new Definition( $class_name, $concrete ); + + $function = $this->reflect_class_or_callable( $class_name, $concrete ); + + if ( ! is_null( $function ) ) { + $arguments = $function->getParameters(); + foreach ( $arguments as $argument ) { + if ( $argument->isDefaultValueAvailable() ) { + $default_value = $argument->getDefaultValue(); + $definition->addArgument( new RawArgument( $default_value ) ); + } else { + $argument_class = $this->get_class( $argument ); + if ( is_null( $argument_class ) ) { + throw new ContainerException( "Argument '{$argument->getName()}' of class '$class_name' doesn't have a type hint or has one that doesn't specify a class." ); + } + + $definition->addArgument( $argument_class->name ); + } + } + } + + // Register the definition only after being sure that no exception will be thrown. + $this->getContainer()->add( $definition->getAlias(), $definition, $shared ); + + return $definition; + } + + /** + * Gets the class of a parameter. + * + * This method is a replacement for ReflectionParameter::getClass, + * which is deprecated as of PHP 8. + * + * @param \ReflectionParameter $parameter The parameter to get the class for. + * + * @return \ReflectionClass|null The class of the parameter, or null if it hasn't any. + */ + private function get_class( \ReflectionParameter $parameter ) { + // TODO: Remove this 'if' block once minimum PHP version for WooCommerce is bumped to at least 7.1. + if ( version_compare( PHP_VERSION, '7.1', '<' ) ) { + return $parameter->getClass(); + } + + return $parameter->getType() && ! $parameter->getType()->isBuiltin() + ? new \ReflectionClass( $parameter->getType()->getName() ) + : null; + } + + /** + * Check if a combination of class name and concrete is valid for registration. + * Also return the class injection method if the concrete is either a class name or null (then use the supplied class name). + * + * @param string $class_name The class name to check. + * @param mixed $concrete The concrete to check. + * + * @return \ReflectionFunctionAbstract|null A reflection instance for the $class_name injection method or $concrete injection method or callable; null otherwise. + * @throws ContainerException Class has a private injection method, can't reflect class, or the concrete is invalid. + */ + private function reflect_class_or_callable( string $class_name, $concrete ) { + if ( ! isset( $concrete ) || is_string( $concrete ) && class_exists( $concrete ) ) { + try { + $class = $concrete ?? $class_name; + $method = new \ReflectionMethod( $class, Definition::INJECTION_METHOD ); + if ( ! isset( $method ) ) { + return null; + } + + $missing_modifiers = array(); + if ( ! $method->isFinal() ) { + $missing_modifiers[] = 'final'; + } + if ( ! $method->isPublic() ) { + $missing_modifiers[] = 'public'; + } + if ( ! empty( $missing_modifiers ) ) { + throw new ContainerException( "Method '" . Definition::INJECTION_METHOD . "' of class '$class' isn't '" . implode( ' ', $missing_modifiers ) . "', instances can't be created." ); + } + + return $method; + } catch ( \ReflectionException $ex ) { + return null; + } + } elseif ( is_callable( $concrete ) ) { + try { + return new \ReflectionFunction( $concrete ); + } catch ( \ReflectionException $ex ) { + throw new ContainerException( "Error when reflecting callable: {$ex->getMessage()}" ); + } + } + + return null; + } + + /** + * Register a class in the container and use reflection to guess the injection method arguments. + * The class is registered as shared, so `get` on the container always returns the same instance. + * + * WARNING: this method uses reflection, so please have performance in mind when using it. + * + * @param string $class_name Class name to register. + * @param mixed $concrete The concrete to register. Can be a shared instance, a factory callback, or a class name. + * + * @return DefinitionInterface The generated container definition. + * + * @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint. + */ + protected function share_with_auto_arguments( string $class_name, $concrete = null ) : DefinitionInterface { + return $this->add_with_auto_arguments( $class_name, $concrete, true ); + } + + /** + * Register an entry in the container. + * + * @param string $id Entry id (typically a class or interface name). + * @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation. + * @param bool|null $shared Whether to register the class as shared (`get` always returns the same instance) or not. + * + * @return DefinitionInterface The generated container definition. + */ + protected function add( string $id, $concrete = null, bool $shared = null ) : DefinitionInterface { + return $this->getContainer()->add( $id, $concrete, $shared ); + } + + /** + * Register a shared entry in the container (`get` always returns the same instance). + * + * @param string $id Entry id (typically a class or interface name). + * @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation. + * + * @return DefinitionInterface The generated container definition. + */ + protected function share( string $id, $concrete = null ) : DefinitionInterface { + return $this->add( $id, $concrete, true ); + } +} diff --git a/src/Internal/DependencyManagement/ContainerException.php b/src/Internal/DependencyManagement/ContainerException.php new file mode 100644 index 0000000..de8499e --- /dev/null +++ b/src/Internal/DependencyManagement/ContainerException.php @@ -0,0 +1,23 @@ +resolveArguments( $this->arguments ); + $concrete = new $concrete(); + + // Constructor injection causes backwards compatibility problems + // so we will rely on method injection via an internal method. + if ( method_exists( $concrete, static::INJECTION_METHOD ) ) { + call_user_func_array( array( $concrete, static::INJECTION_METHOD ), $resolved ); + } + + return $concrete; + } +} diff --git a/src/Internal/DependencyManagement/ExtendedContainer.php b/src/Internal/DependencyManagement/ExtendedContainer.php new file mode 100644 index 0000000..0615ec5 --- /dev/null +++ b/src/Internal/DependencyManagement/ExtendedContainer.php @@ -0,0 +1,162 @@ +is_class_allowed( $class_name ) ) { + throw new ContainerException( "You cannot add '$class_name', only classes in the {$this->woocommerce_namespace} namespace are allowed." ); + } + + $concrete_class = $this->get_class_from_concrete( $concrete ); + if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) ) { + throw new ContainerException( "You cannot add concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." ); + } + + // We want to use a definition class that does not support constructor injection to avoid accidental usage. + if ( ! $concrete instanceof DefinitionInterface ) { + $concrete = new Definition( $class_name, $concrete ); + } + + return parent::add( $class_name, $concrete, $shared ); + } + + /** + * Replace an existing registration with a different concrete. + * + * @param string $class_name The class name whose definition will be replaced. + * @param mixed $concrete The new concrete (same as "add"). + * + * @return DefinitionInterface The modified definition. + * @throws ContainerException Invalid parameters. + */ + public function replace( string $class_name, $concrete ) : DefinitionInterface { + if ( ! $this->has( $class_name ) ) { + throw new ContainerException( "The container doesn't have '$class_name' registered, please use 'add' instead of 'replace'." ); + } + + $concrete_class = $this->get_class_from_concrete( $concrete ); + if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) && ! $this->is_anonymous_class( $concrete_class ) ) { + throw new ContainerException( "You cannot use concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." ); + } + + return $this->extend( $class_name )->setConcrete( $concrete ); + } + + /** + * Reset all the cached resolutions, so any further "get" for shared definitions will generate the instance again. + */ + public function reset_all_resolved() { + foreach ( $this->definitions->getIterator() as $definition ) { + // setConcrete causes the cached resolved value to be forgotten. + $concrete = $definition->getConcrete(); + $definition->setConcrete( $concrete ); + } + } + + /** + * Get an instance of a registered class. + * + * @param string $id The class name. + * @param bool $new True to generate a new instance even if the class was registered as shared. + * + * @return object An instance of the requested class. + * @throws ContainerException Attempt to get an instance of a non-namespaced class. + */ + public function get( $id, bool $new = false ) { + if ( false === strpos( $id, '\\' ) ) { + throw new ContainerException( "Attempt to get an instance of the non-namespaced class '$id' from the container, did you forget to add a namespace import?" ); + } + + return parent::get( $id, $new ); + } + + /** + * Gets the class from the concrete regardless of type. + * + * @param mixed $concrete The concrete that we want the class from.. + * + * @return string|null The class from the concrete if one is available, null otherwise. + */ + protected function get_class_from_concrete( $concrete ) { + if ( is_object( $concrete ) && ! is_callable( $concrete ) ) { + if ( $concrete instanceof DefinitionInterface ) { + return $this->get_class_from_concrete( $concrete->getConcrete() ); + } + + return get_class( $concrete ); + } + + if ( is_string( $concrete ) && class_exists( $concrete ) ) { + return $concrete; + } + + return null; + } + + /** + * Checks to see whether or not a class is allowed to be registered. + * + * @param string $class_name The class to check. + * + * @return bool True if the class is allowed to be registered, false otherwise. + */ + protected function is_class_allowed( string $class_name ): bool { + return StringUtil::starts_with( $class_name, $this->woocommerce_namespace, false ) || in_array( $class_name, $this->registration_whitelist, true ); + } + + /** + * Check if a class name corresponds to an anonymous class. + * + * @param string $class_name The class name to check. + * @return bool True if the name corresponds to an anonymous class. + */ + protected function is_anonymous_class( string $class_name ): bool { + return StringUtil::starts_with( $class_name, 'class@anonymous' ); + } +} diff --git a/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php b/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php new file mode 100644 index 0000000..cba9e81 --- /dev/null +++ b/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php @@ -0,0 +1,31 @@ +share( AssignDefaultCategory::class ); + } +} diff --git a/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php b/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php new file mode 100644 index 0000000..126e689 --- /dev/null +++ b/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php @@ -0,0 +1,31 @@ +share( DownloadPermissionsAdjuster::class ); + } +} diff --git a/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php b/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php new file mode 100644 index 0000000..1736210 --- /dev/null +++ b/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php @@ -0,0 +1,37 @@ +share( DataRegenerator::class )->addArgument( LookupDataStore::class ); + $this->share( Filterer::class )->addArgument( LookupDataStore::class ); + $this->share( LookupDataStore::class ); + } +} diff --git a/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php b/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php new file mode 100644 index 0000000..4e2823d --- /dev/null +++ b/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php @@ -0,0 +1,34 @@ +share( ActionsProxy::class ); + $this->share_with_auto_arguments( LegacyProxy::class ); + } +} diff --git a/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php b/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php new file mode 100644 index 0000000..08d9f5e --- /dev/null +++ b/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php @@ -0,0 +1,31 @@ +share( RestockRefundedItemsAdjuster::class ); + } +} diff --git a/src/Internal/DownloadPermissionsAdjuster.php b/src/Internal/DownloadPermissionsAdjuster.php new file mode 100644 index 0000000..c2bbac1 --- /dev/null +++ b/src/Internal/DownloadPermissionsAdjuster.php @@ -0,0 +1,163 @@ +downloads_data_store = wc_get_container()->get( LegacyProxy::class )->get_instance_of( \WC_Data_Store::class, 'customer-download' ); + add_action( 'adjust_download_permissions', array( $this, 'adjust_download_permissions' ), 10, 1 ); + } + + /** + * Schedule a download permissions adjustment for a product if necessary. + * This should be executed whenever a product is saved. + * + * @param \WC_Product $product The product to schedule a download permission adjustments for. + */ + public function maybe_schedule_adjust_download_permissions( \WC_Product $product ) { + $children_ids = $product->get_children(); + if ( ! $children_ids ) { + return; + } + + $scheduled_action_args = array( $product->get_id() ); + + $already_scheduled_actions = + WC()->call_function( + 'as_get_scheduled_actions', + array( + 'hook' => 'adjust_download_permissions', + 'args' => $scheduled_action_args, + 'status' => \ActionScheduler_Store::STATUS_PENDING, + ), + 'ids' + ); + + if ( empty( $already_scheduled_actions ) ) { + WC()->call_function( + 'as_schedule_single_action', + WC()->call_function( 'time' ) + 1, + 'adjust_download_permissions', + $scheduled_action_args + ); + } + } + + /** + * Create additional download permissions for variations if necessary. + * + * When a simple downloadable product is converted to a variable product, + * existing download permissions are still present in the database but they don't apply anymore. + * This method creates additional download permissions for the variations based on + * the old existing ones for the main product. + * + * The procedure is as follows. For each existing download permission for the parent product, + * check if there's any variation offering the same file for download (the file URL, not name, is checked). + * If that is found, check if an equivalent permission exists (equivalent means for the same file and with + * the same order id and customer id). If no equivalent permission exists, create it. + * + * @param int $product_id The id of the product to check permissions for. + */ + public function adjust_download_permissions( int $product_id ) { + $product = wc_get_product( $product_id ); + if ( ! $product ) { + return; + } + + $children_ids = $product->get_children(); + if ( ! $children_ids ) { + return; + } + + $parent_downloads = $this->get_download_files_and_permissions( $product ); + if ( ! $parent_downloads ) { + return; + } + + $children_with_downloads = array(); + foreach ( $children_ids as $child_id ) { + $child = wc_get_product( $child_id ); + $children_with_downloads[ $child_id ] = $this->get_download_files_and_permissions( $child ); + } + + foreach ( $parent_downloads['permission_data_by_file_order_user'] as $parent_file_order_and_user => $parent_download_data ) { + foreach ( $children_with_downloads as $child_id => $child_download_data ) { + $file_url = $parent_download_data['file']; + + $must_create_permission = + // The variation offers the same file as the parent for download... + in_array( $file_url, array_keys( $child_download_data['download_ids_by_file_url'] ), true ) && + // ...but no equivalent download permission (same file URL, order id and user id) exists. + ! array_key_exists( $parent_file_order_and_user, $child_download_data['permission_data_by_file_order_user'] ); + + if ( $must_create_permission ) { + // The new child download permission is a copy of the parent's, + // but with the product and download ids changed to match those of the variation. + $new_download_data = $parent_download_data['data']; + $new_download_data['product_id'] = $child_id; + $new_download_data['download_id'] = $child_download_data['download_ids_by_file_url'][ $file_url ]; + $this->downloads_data_store->create_from_data( $new_download_data ); + } + } + } + } + + /** + * Get the existing downloadable files and download permissions for a given product. + * The returned value is an array with two keys: + * + * - download_ids_by_file_url: an associative array of file url => download_id. + * - permission_data_by_file_order_user: an associative array where key is "file_url:customer_id:order_id" and value is the full permission data set. + * + * @param \WC_Product $product The product to get the downloadable files and permissions for. + * @return array[] Information about the downloadable files and permissions for the product. + */ + private function get_download_files_and_permissions( \WC_Product $product ) { + $result = array( + 'permission_data_by_file_order_user' => array(), + 'download_ids_by_file_url' => array(), + ); + $downloads = $product->get_downloads(); + foreach ( $downloads as $download ) { + $result['download_ids_by_file_url'][ $download->get_file() ] = $download->get_id(); + } + + $permissions = $this->downloads_data_store->get_downloads( array( 'product_id' => $product->get_id() ) ); + foreach ( $permissions as $permission ) { + $permission_data = (array) $permission->data; + if ( array_key_exists( $permission_data['download_id'], $downloads ) ) { + $file = $downloads[ $permission_data['download_id'] ]->get_file(); + $data = array( + 'file' => $file, + 'data' => (array) $permission->data, + ); + $result['permission_data_by_file_order_user'][ "${file}:${permission_data['user_id']}:${permission_data['order_id']}" ] = $data; + } + } + + return $result; + } +} diff --git a/src/Internal/ProductAttributesLookup/DataRegenerator.php b/src/Internal/ProductAttributesLookup/DataRegenerator.php new file mode 100644 index 0000000..ec82a9c --- /dev/null +++ b/src/Internal/ProductAttributesLookup/DataRegenerator.php @@ -0,0 +1,391 @@ +lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup'; + + add_filter( + 'woocommerce_debug_tools', + function( $tools ) { + return $this->add_initiate_regeneration_entry_to_tools_array( $tools ); + }, + 1, + 999 + ); + + add_action( + 'woocommerce_run_product_attribute_lookup_regeneration_callback', + function () { + $this->run_regeneration_step_callback(); + } + ); + } + + /** + * Class initialization, invoked by the DI container. + * + * @internal + * @param LookupDataStore $data_store The data store to use. + */ + final public function init( LookupDataStore $data_store ) { + $this->data_store = $data_store; + } + + /** + * Initialize the regeneration procedure: + * deletes the lookup table and related options if they exist, + * then it creates the table and runs the first step of the regeneration process. + * + * This is the method that should be used as a callback for a data regeneration in wc-update-functions, e.g.: + * + * function wc_update_XX_regenerate_product_attributes_lookup_table() { + * wc_get_container()->get(DataRegenerator::class)->initiate_regeneration(); + * return false; + * } + * + * (Note how we are returning "false" since the class handles the step scheduling by itself). + */ + public function initiate_regeneration() { + $this->enable_or_disable_lookup_table_usage( false ); + + $this->delete_all_attributes_lookup_data(); + $products_exist = $this->initialize_table_and_data(); + if ( $products_exist ) { + $this->enqueue_regeneration_step_run(); + } else { + $this->finalize_regeneration(); + } + } + + /** + * Delete all the existing data related to the lookup table, including the table itself. + * + * Shortcut to run this method in case the debug tools UI isn't available or for quick debugging: + * + * wp eval "wc_get_container()->get(Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator::class)->delete_all_attributes_lookup_data();" + */ + public function delete_all_attributes_lookup_data() { + global $wpdb; + + delete_option( 'woocommerce_attribute_lookup_enabled' ); + delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); + delete_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); + $this->data_store->unset_regeneration_in_progress_flag(); + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( 'DROP TABLE IF EXISTS ' . $this->lookup_table_name ); + } + + /** + * Create the lookup table and initialize the options that will be temporarily used + * while the regeneration is in progress. + * + * @return bool True if there's any product at all in the database, false otherwise. + */ + private function initialize_table_and_data() { + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( + ' +CREATE TABLE ' . $this->lookup_table_name . '( + product_id bigint(20) NOT NULL, + product_or_parent_id bigint(20) NOT NULL, + taxonomy varchar(32) NOT NULL, + term_id bigint(20) NOT NULL, + is_variation_attribute tinyint(1) NOT NULL, + in_stock tinyint(1) NOT NULL + ); + ' + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + + $last_existing_product_id = + WC()->call_function( + 'wc_get_products', + array( + 'return' => 'ids', + 'limit' => 1, + 'orderby' => array( + 'ID' => 'DESC', + ), + ) + ); + + if ( ! $last_existing_product_id ) { + // No products exist, nothing to (re)generate. + return false; + } + + $this->data_store->set_regeneration_in_progress_flag(); + update_option( 'woocommerce_attribute_lookup_last_product_id_to_process', current( $last_existing_product_id ) ); + update_option( 'woocommerce_attribute_lookup_last_products_page_processed', 0 ); + + return true; + } + + /** + * Action scheduler callback, performs one regeneration step and then + * schedules the next step if necessary. + */ + private function run_regeneration_step_callback() { + if ( ! $this->data_store->regeneration_is_in_progress() ) { + return; + } + + $result = $this->do_regeneration_step(); + if ( $result ) { + $this->enqueue_regeneration_step_run(); + } else { + $this->finalize_regeneration(); + } + } + + /** + * Enqueue one regeneration step in action scheduler. + */ + private function enqueue_regeneration_step_run() { + $queue = WC()->get_instance_of( \WC_Queue::class ); + $queue->schedule_single( + WC()->call_function( 'time' ) + 1, + 'woocommerce_run_product_attribute_lookup_regeneration_callback', + array(), + 'woocommerce-db-updates' + ); + } + + /** + * Perform one regeneration step: grabs a chunk of products and creates + * the appropriate entries for them in the lookup table. + * + * @return bool True if more steps need to be run, false otherwise. + */ + private function do_regeneration_step() { + $last_products_page_processed = get_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); + $current_products_page = (int) $last_products_page_processed + 1; + + $product_ids = WC()->call_function( + 'wc_get_products', + array( + 'limit' => self::PRODUCTS_PER_GENERATION_STEP, + 'page' => $current_products_page, + 'orderby' => array( + 'ID' => 'ASC', + ), + 'return' => 'ids', + ) + ); + + if ( ! $product_ids ) { + return false; + } + + foreach ( $product_ids as $id ) { + $this->data_store->create_data_for_product( $id ); + } + + update_option( 'woocommerce_attribute_lookup_last_products_page_processed', $current_products_page ); + + $last_product_id_to_process = get_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); + return end( $product_ids ) < $last_product_id_to_process; + } + + /** + * Cleanup/final option setup after the regeneration has been completed. + */ + private function finalize_regeneration() { + delete_option( 'woocommerce_attribute_lookup_last_product_id_to_process' ); + delete_option( 'woocommerce_attribute_lookup_last_products_page_processed' ); + update_option( 'woocommerce_attribute_lookup_enabled', 'no' ); + $this->data_store->unset_regeneration_in_progress_flag(); + } + + /** + * Add a 'Regenerate product attributes lookup table' entry to the Status - Tools page. + * + * @param array $tools_array The tool definitions array that is passed ro the woocommerce_debug_tools filter. + * @return array The tools array with the entry added. + */ + private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ) { + if ( ! $this->data_store->is_feature_visible() ) { + return $tools_array; + } + + $lookup_table_exists = $this->data_store->check_lookup_table_exists(); + $generation_is_in_progress = $this->data_store->regeneration_is_in_progress(); + + // Regenerate table. + + if ( $lookup_table_exists ) { + $generate_item_name = __( 'Regenerate the product attributes lookup table', 'woocommerce' ); + $generate_item_desc = __( 'This tool will regenerate the product attributes lookup table data from existing product(s) data. This process may take a while.', 'woocommerce' ); + $generate_item_return = __( 'Product attributes lookup table data is regenerating', 'woocommerce' ); + $generate_item_button = __( 'Regenerate', 'woocommerce' ); + } else { + $generate_item_name = __( 'Create and fill product attributes lookup table', 'woocommerce' ); + $generate_item_desc = __( 'This tool will create the product attributes lookup table data and fill it with existing products data. This process may take a while.', 'woocommerce' ); + $generate_item_return = __( 'Product attributes lookup table is being filled', 'woocommerce' ); + $generate_item_button = __( 'Create', 'woocommerce' ); + } + + $entry = array( + 'name' => $generate_item_name, + 'desc' => $generate_item_desc, + 'requires_refresh' => true, + 'callback' => function() use ( $generate_item_return ) { + $this->initiate_regeneration_from_tools_page(); + return $generate_item_return; + }, + ); + + if ( $lookup_table_exists ) { + $entry['selector'] = array( + 'description' => __( 'Select a product to regenerate the data for, or leave empty for a full table regeneration:', 'woocommerce' ), + 'class' => 'wc-product-search', + 'search_action' => 'woocommerce_json_search_products', + 'name' => 'regenerate_product_attribute_lookup_data_product_id', + 'placeholder' => esc_attr__( 'Search for a product…', 'woocommerce' ), + ); + } + + if ( $generation_is_in_progress ) { + $entry['button'] = sprintf( + /* translators: %d: How many products have been processed so far. */ + __( 'Filling in progress (%d)', 'woocommerce' ), + get_option( 'woocommerce_attribute_lookup_last_products_page_processed', 0 ) * self::PRODUCTS_PER_GENERATION_STEP + ); + $entry['disabled'] = true; + } else { + $entry['button'] = $generate_item_button; + } + + $tools_array['regenerate_product_attributes_lookup_table'] = $entry; + + if ( $lookup_table_exists ) { + + // Delete the table. + + $tools_array['delete_product_attributes_lookup_table'] = array( + 'name' => __( 'Delete the product attributes lookup table', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This will delete the product attributes lookup table. You can create it again with the "Create and fill product attributes lookup table" tool.', 'woocommerce' ) + ), + 'button' => __( 'Delete', 'woocommerce' ), + 'requires_refresh' => true, + 'callback' => function () { + $this->delete_all_attributes_lookup_data(); + return __( 'Product attributes lookup table has been deleted.', 'woocommerce' ); + }, + ); + } + + return $tools_array; + } + + /** + * Callback to initiate the regeneration process from the Status - Tools page. + * + * @throws \Exception The regeneration is already in progress. + */ + private function initiate_regeneration_from_tools_page() { + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput + if ( ! isset( $_REQUEST['_wpnonce'] ) || false === wp_verify_nonce( $_REQUEST['_wpnonce'], 'debug_action' ) ) { + throw new \Exception( 'Invalid nonce' ); + } + + if ( isset( $_REQUEST['regenerate_product_attribute_lookup_data_product_id'] ) ) { + $product_id = (int) $_REQUEST['regenerate_product_attribute_lookup_data_product_id']; + $this->check_can_do_lookup_table_regeneration( $product_id ); + $this->data_store->create_data_for_product( $product_id ); + } else { + $this->check_can_do_lookup_table_regeneration(); + $this->initiate_regeneration(); + } + } + + /** + * Enable or disable the actual lookup table usage. + * + * @param bool $enable True to enable, false to disable. + * @throws \Exception A lookup table regeneration is currently in progress. + */ + private function enable_or_disable_lookup_table_usage( $enable ) { + if ( $this->data_store->regeneration_is_in_progress() ) { + throw new \Exception( "Can't enable or disable the attributes lookup table usage while it's regenerating." ); + } + + update_option( 'woocommerce_attribute_lookup_enabled', $enable ? 'yes' : 'no' ); + } + + /** + * Check if everything is good to go to perform a complete or per product lookup table data regeneration + * and throw an exception if not. + * + * @param mixed $product_id The product id to check the regeneration viability for, or null to check if a complete regeneration is possible. + * @throws \Exception Something prevents the regeneration from starting. + */ + private function check_can_do_lookup_table_regeneration( $product_id = null ) { + if ( ! $this->data_store->is_feature_visible() ) { + throw new \Exception( "Can't do product attribute lookup data regeneration: feature is not visible" ); + } + if ( $product_id && ! $this->data_store->check_lookup_table_exists() ) { + throw new \Exception( "Can't do product attribute lookup data regeneration: lookup table doesn't exist" ); + } + if ( $this->data_store->regeneration_is_in_progress() ) { + throw new \Exception( "Can't do product attribute lookup data regeneration: regeneration is already in progress" ); + } + if ( $product_id && ! wc_get_product( $product_id ) ) { + throw new \Exception( "Can't do product attribute lookup data regeneration: product doesn't exist" ); + } + } +} diff --git a/src/Internal/ProductAttributesLookup/Filterer.php b/src/Internal/ProductAttributesLookup/Filterer.php new file mode 100644 index 0000000..132d31f --- /dev/null +++ b/src/Internal/ProductAttributesLookup/Filterer.php @@ -0,0 +1,329 @@ +data_store = $data_store; + $this->lookup_table_name = $data_store->get_lookup_table_name(); + } + + /** + * Checks if the product attribute filtering via lookup table feature is enabled. + * + * @return bool + */ + public function filtering_via_lookup_table_is_active() { + return 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' ); + } + + /** + * Adds post clauses for filtering via lookup table. + * This method should be invoked within a 'posts_clauses' filter. + * + * @param array $args Product query clauses as supplied to the 'posts_clauses' filter. + * @param \WP_Query $wp_query Current product query as supplied to the 'posts_clauses' filter. + * @param array $attributes_to_filter_by Attribute filtering data as generated by WC_Query::get_layered_nav_chosen_attributes. + * @return array The updated product query clauses. + */ + public function filter_by_attribute_post_clauses( array $args, \WP_Query $wp_query, array $attributes_to_filter_by ) { + global $wpdb; + + if ( ! $wp_query->is_main_query() || ! $this->filtering_via_lookup_table_is_active() ) { + return $args; + } + + $clause_root = " {$wpdb->prefix}posts.ID IN ("; + if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { + $in_stock_clause = ' AND in_stock = 1'; + } else { + $in_stock_clause = ''; + } + + foreach ( $attributes_to_filter_by as $taxonomy => $data ) { + $all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); + $term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' ); + $term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) ); + $term_ids_to_filter_by = array_map( 'absint', $term_ids_to_filter_by ); + $term_ids_to_filter_by_list = '(' . join( ',', $term_ids_to_filter_by ) . ')'; + $is_and_query = 'and' === $data['query_type']; + + $count = count( $term_ids_to_filter_by ); + if ( 0 !== $count ) { + if ( $is_and_query ) { + $clauses[] = " + {$clause_root} + SELECT product_or_parent_id + FROM {$this->lookup_table_name} lt + WHERE is_variation_attribute=0 + {$in_stock_clause} + AND term_id in {$term_ids_to_filter_by_list} + GROUP BY product_id + HAVING COUNT(product_id)={$count} + UNION + SELECT product_or_parent_id + FROM {$this->lookup_table_name} lt + WHERE is_variation_attribute=1 + {$in_stock_clause} + AND term_id in {$term_ids_to_filter_by_list} + )"; + } else { + $clauses[] = " + {$clause_root} + SELECT product_or_parent_id + FROM {$this->lookup_table_name} lt + WHERE term_id in {$term_ids_to_filter_by_list} + {$in_stock_clause} + )"; + } + } + } + + if ( ! empty( $clauses ) ) { + $args['where'] .= ' AND (' . join( ' AND ', $clauses ) . ')'; + } elseif ( ! empty( $attributes_to_filter_by ) ) { + $args['where'] .= ' AND 1=0'; + } + + return $args; + } + + /** + * Count products within certain terms, taking the main WP query into consideration, + * for the WC_Widget_Layered_Nav widget. + * + * This query allows counts to be generated based on the viewed products, not all products. + * + * @param array $term_ids Term IDs. + * @param string $taxonomy Taxonomy. + * @param string $query_type Query Type. + * @return array + */ + public function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) { + global $wpdb; + + $use_lookup_table = $this->filtering_via_lookup_table_is_active(); + + $tax_query = \WC_Query::get_main_tax_query(); + $meta_query = \WC_Query::get_main_meta_query(); + if ( 'or' === $query_type ) { + foreach ( $tax_query as $key => $query ) { + if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) { + unset( $tax_query[ $key ] ); + } + } + } + + $meta_query = new \WP_Meta_Query( $meta_query ); + $tax_query = new \WP_Tax_Query( $tax_query ); + + if ( $use_lookup_table ) { + $query = $this->get_product_counts_query_using_lookup_table( $tax_query, $meta_query, $taxonomy, $term_ids ); + } else { + $query = $this->get_product_counts_query_not_using_lookup_table( $tax_query, $meta_query, $term_ids ); + } + + $query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query ); + $query_sql = implode( ' ', $query ); + + // We have a query - let's see if cached results of this query already exist. + $query_hash = md5( $query_sql ); + // Maybe store a transient of the count values. + $cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true ); + if ( true === $cache ) { + $cached_counts = (array) get_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ) ); + } else { + $cached_counts = array(); + } + if ( ! isset( $cached_counts[ $query_hash ] ) ) { + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + $results = $wpdb->get_results( $query_sql, ARRAY_A ); + $counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) ); + $cached_counts[ $query_hash ] = $counts; + if ( true === $cache ) { + set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS ); + } + } + return array_map( 'absint', (array) $cached_counts[ $query_hash ] ); + } + + /** + * Get the query for counting products by terms using the product attributes lookup table. + * + * @param \WP_Tax_Query $tax_query The current main tax query. + * @param \WP_Meta_Query $meta_query The current main meta query. + * @param string $taxonomy The attribute name to get the term counts for. + * @param string $term_ids The term ids to include in the search. + * @return array An array of SQL query parts. + */ + private function get_product_counts_query_using_lookup_table( $tax_query, $meta_query, $taxonomy, $term_ids ) { + global $wpdb; + + $meta_query_sql = $meta_query->get_sql( 'post', $this->lookup_table_name, 'product_or_parent_id' ); + $tax_query_sql = $tax_query->get_sql( $this->lookup_table_name, 'product_or_parent_id' ); + $hide_out_of_stock = 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ); + $in_stock_clause = $hide_out_of_stock ? ' AND in_stock = 1' : ''; + + $query['select'] = 'SELECT COUNT(DISTINCT product_or_parent_id) as term_count, term_id as term_count_id'; + $query['from'] = "FROM {$this->lookup_table_name}"; + $query['join'] = " + {$tax_query_sql['join']} {$meta_query_sql['join']} + INNER JOIN {$wpdb->posts} ON {$wpdb->posts}.ID = {$this->lookup_table_name}.product_or_parent_id"; + + $term_ids_sql = $this->get_term_ids_sql( $term_ids ); + $query['where'] = " + WHERE {$wpdb->posts}.post_type IN ( 'product' ) + AND {$wpdb->posts}.post_status = 'publish' + {$tax_query_sql['where']} {$meta_query_sql['where']} + AND {$this->lookup_table_name}.taxonomy='{$taxonomy}' + AND {$this->lookup_table_name}.term_id IN $term_ids_sql + {$in_stock_clause}"; + + if ( ! empty( $term_ids ) ) { + $attributes_to_filter_by = \WC_Query::get_layered_nav_chosen_attributes(); + + if ( ! empty( $attributes_to_filter_by ) ) { + $and_term_ids = array(); + $or_term_ids = array(); + + foreach ( $attributes_to_filter_by as $taxonomy => $data ) { + $all_terms = get_terms( $taxonomy, array( 'hide_empty' => false ) ); + $term_ids_by_slug = wp_list_pluck( $all_terms, 'term_id', 'slug' ); + $term_ids_to_filter_by = array_values( array_intersect_key( $term_ids_by_slug, array_flip( $data['terms'] ) ) ); + if ( 'and' === $data['query_type'] ) { + $and_term_ids = array_merge( $and_term_ids, $term_ids_to_filter_by ); + } else { + $or_term_ids = array_merge( $or_term_ids, $term_ids_to_filter_by ); + } + } + + if ( ! empty( $and_term_ids ) ) { + $terms_count = count( $and_term_ids ); + $term_ids_list = '(' . join( ',', $and_term_ids ) . ')'; + $query['where'] .= " + AND product_or_parent_id IN ( + SELECT product_or_parent_id + FROM {$this->lookup_table_name} lt + WHERE is_variation_attribute=0 + {$in_stock_clause} + AND term_id in {$term_ids_list} + GROUP BY product_id + HAVING COUNT(product_id)={$terms_count} + UNION + SELECT product_or_parent_id + FROM {$this->lookup_table_name} lt + WHERE is_variation_attribute=1 + {$in_stock_clause} + AND term_id in {$term_ids_list} + )"; + } + + if ( ! empty( $or_term_ids ) ) { + $term_ids_list = '(' . join( ',', $or_term_ids ) . ')'; + $query['where'] .= " + AND product_or_parent_id IN ( + SELECT product_or_parent_id FROM {$this->lookup_table_name} + WHERE term_id in {$term_ids_list} + {$in_stock_clause} + )"; + + } + } else { + $query['where'] .= $in_stock_clause; + } + } elseif ( $hide_out_of_stock ) { + $query['where'] .= " AND {$this->lookup_table_name}.in_stock=1"; + } + + $search_query_sql = \WC_Query::get_main_search_query_sql(); + if ( $search_query_sql ) { + $query['where'] .= ' AND ' . $search_query_sql; + } + + $query['group_by'] = 'GROUP BY terms.term_id'; + $query['group_by'] = "GROUP BY {$this->lookup_table_name}.term_id"; + + return $query; + } + + /** + * Get the query for counting products by terms NOT using the product attributes lookup table. + * + * @param \WP_Tax_Query $tax_query The current main tax query. + * @param \WP_Meta_Query $meta_query The current main meta query. + * @param string $term_ids The term ids to include in the search. + * @return array An array of SQL query parts. + */ + private function get_product_counts_query_not_using_lookup_table( $tax_query, $meta_query, $term_ids ) { + global $wpdb; + + $meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' ); + $tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' ); + + // Generate query. + $query = array(); + $query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) AS term_count, terms.term_id AS term_count_id"; + $query['from'] = "FROM {$wpdb->posts}"; + $query['join'] = " + INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id + INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id ) + INNER JOIN {$wpdb->terms} AS terms USING( term_id ) + " . $tax_query_sql['join'] . $meta_query_sql['join']; + + $term_ids_sql = $this->get_term_ids_sql( $term_ids ); + $query['where'] = " + WHERE {$wpdb->posts}.post_type IN ( 'product' ) + AND {$wpdb->posts}.post_status = 'publish' + {$tax_query_sql['where']} {$meta_query_sql['where']} + AND terms.term_id IN $term_ids_sql"; + + $search_query_sql = \WC_Query::get_main_search_query_sql(); + if ( $search_query_sql ) { + $query['where'] .= ' AND ' . $search_query_sql; + } + + $query['group_by'] = 'GROUP BY terms.term_id'; + + return $query; + } + + /** + * Formats a list of term ids as "(id,id,id)". + * + * @param array $term_ids The list of terms to format. + * @return string The formatted list. + */ + private function get_term_ids_sql( $term_ids ) { + return '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')'; + } +} diff --git a/src/Internal/ProductAttributesLookup/LookupDataStore.php b/src/Internal/ProductAttributesLookup/LookupDataStore.php new file mode 100644 index 0000000..377eb28 --- /dev/null +++ b/src/Internal/ProductAttributesLookup/LookupDataStore.php @@ -0,0 +1,682 @@ +lookup_table_name = $wpdb->prefix . 'wc_product_attributes_lookup'; + $this->is_feature_visible = false; + + $this->init_hooks(); + } + + /** + * Initialize the hooks used by the class. + */ + private function init_hooks() { + add_action( + 'woocommerce_run_product_attribute_lookup_update_callback', + function ( $product_id, $action ) { + $this->run_update_callback( $product_id, $action ); + }, + 10, + 2 + ); + + add_filter( + 'woocommerce_get_sections_products', + function ( $products ) { + if ( $this->is_feature_visible() && $this->check_lookup_table_exists() ) { + $products['advanced'] = __( 'Advanced', 'woocommerce' ); + } + return $products; + }, + 100, + 1 + ); + + add_filter( + 'woocommerce_get_settings_products', + function ( $settings, $section_id ) { + if ( 'advanced' === $section_id && $this->is_feature_visible() && $this->check_lookup_table_exists() ) { + $title_item = array( + 'title' => __( 'Product attributes lookup table', 'woocommerce' ), + 'type' => 'title', + ); + + $regeneration_is_in_progress = $this->regeneration_is_in_progress(); + + if ( $regeneration_is_in_progress ) { + $title_item['desc'] = __( 'These settings are not available while the lookup table regeneration is in progress.', 'woocommerce' ); + } + + $settings[] = $title_item; + + if ( ! $regeneration_is_in_progress ) { + $settings[] = array( + 'title' => __( 'Enable table usage', 'woocommerce' ), + 'desc' => __( 'Use the product attributes lookup table for catalog filtering.', 'woocommerce' ), + 'id' => 'woocommerce_attribute_lookup_enabled', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + ); + + $settings[] = array( + 'title' => __( 'Direct updates', 'woocommerce' ), + 'desc' => __( 'Update the table directly upon product changes, instead of scheduling a deferred update.', 'woocommerce' ), + 'id' => 'woocommerce_attribute_lookup_direct_updates', + 'default' => 'no', + 'type' => 'checkbox', + 'checkboxgroup' => 'start', + ); + } + + $settings[] = array( 'type' => 'sectionend' ); + } + return $settings; + }, + 100, + 2 + ); + } + + /** + * Check if the lookup table exists in the database. + * + * TODO: Remove this method and references to it once the lookup table is created via data migration. + * + * @return bool + */ + public function check_lookup_table_exists() { + global $wpdb; + + $query = $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $this->lookup_table_name ) ); + + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared + return $this->lookup_table_name === $wpdb->get_var( $query ); + } + + /** + * Checks if the feature is visible (so that dedicated entries will be added to the debug tools page). + * + * @return bool True if the feature is visible. + */ + public function is_feature_visible() { + return $this->is_feature_visible; + } + + /** + * Makes the feature visible, so that dedicated entries will be added to the debug tools page. + */ + public function show_feature() { + $this->is_feature_visible = true; + } + + /** + * Hides the feature, so that no entries will be added to the debug tools page. + */ + public function hide_feature() { + $this->is_feature_visible = false; + } + + /** + * Get the name of the lookup table. + * + * @return string + */ + public function get_lookup_table_name() { + return $this->lookup_table_name; + } + + /** + * Insert/update the appropriate lookup table entries for a new or modified product or variation. + * This must be invoked after a product or a variation is created (including untrashing and duplication) + * or modified. + * + * @param int|\WC_Product $product Product object or product id. + * @param null|array $changeset Changes as provided by 'get_changes' method in the product object, null if it's being created. + */ + public function on_product_changed( $product, $changeset = null ) { + if ( ! $this->check_lookup_table_exists() ) { + return; + } + + if ( ! is_a( $product, \WC_Product::class ) ) { + $product = WC()->call_function( 'wc_get_product', $product ); + } + + $action = $this->get_update_action( $changeset ); + if ( self::ACTION_NONE !== $action ) { + $this->maybe_schedule_update( $product->get_id(), $action ); + } + } + + /** + * Schedule an update of the product attributes lookup table for a given product. + * If an update for the same action is already scheduled, nothing is done. + * + * If the 'woocommerce_attribute_lookup_direct_update' option is set to 'yes', + * the update is done directly, without scheduling. + * + * @param int $product_id The product id to schedule the update for. + * @param int $action The action to perform, one of the ACTION_ constants. + */ + private function maybe_schedule_update( int $product_id, int $action ) { + if ( 'yes' === get_option( 'woocommerce_attribute_lookup_direct_updates' ) ) { + $this->run_update_callback( $product_id, $action ); + return; + } + + $args = array( $product_id, $action ); + + $queue = WC()->get_instance_of( \WC_Queue::class ); + $already_scheduled = $queue->search( + array( + 'hook' => 'woocommerce_run_product_attribute_lookup_update_callback', + 'args' => $args, + 'status' => \ActionScheduler_Store::STATUS_PENDING, + ), + 'ids' + ); + + if ( empty( $already_scheduled ) ) { + $queue->schedule_single( + WC()->call_function( 'time' ) + 1, + 'woocommerce_run_product_attribute_lookup_update_callback', + $args, + 'woocommerce-db-updates' + ); + } + } + + /** + * Perform an update of the lookup table for a specific product. + * + * @param int $product_id The product id to perform the update for. + * @param int $action The action to perform, one of the ACTION_ constants. + */ + private function run_update_callback( int $product_id, int $action ) { + if ( ! $this->check_lookup_table_exists() ) { + return; + } + + $product = WC()->call_function( 'wc_get_product', $product_id ); + if ( ! $product ) { + $action = self::ACTION_DELETE; + } + + switch ( $action ) { + case self::ACTION_INSERT: + $this->delete_data_for( $product_id ); + $this->create_data_for( $product ); + break; + case self::ACTION_UPDATE_STOCK: + $this->update_stock_status_for( $product ); + break; + case self::ACTION_DELETE: + $this->delete_data_for( $product_id ); + break; + } + } + + /** + * Determine the type of action to perform depending on the received changeset. + * + * @param array|null $changeset The changeset received by on_product_changed. + * @return int One of the ACTION_ constants. + */ + private function get_update_action( $changeset ) { + if ( is_null( $changeset ) ) { + // No changeset at all means that the product is new. + return self::ACTION_INSERT; + } + + $keys = array_keys( $changeset ); + + // Order matters: + // - The change with the most precedence is a change in catalog visibility + // (which will result in all data being regenerated or deleted). + // - Then a change in attributes (all data will be regenerated). + // - And finally a change in stock status (existing data will be updated). + // Thus these conditions must be checked in that same order. + + if ( in_array( 'catalog_visibility', $keys, true ) ) { + $new_visibility = $changeset['catalog_visibility']; + if ( 'visible' === $new_visibility || 'catalog' === $new_visibility ) { + return self::ACTION_INSERT; + } else { + return self::ACTION_DELETE; + } + } + + if ( in_array( 'attributes', $keys, true ) ) { + return self::ACTION_INSERT; + } + + if ( array_intersect( $keys, array( 'stock_quantity', 'stock_status', 'manage_stock' ) ) ) { + return self::ACTION_UPDATE_STOCK; + } + + return self::ACTION_NONE; + } + + /** + * Update the stock status of the lookup table entries for a given product. + * + * @param \WC_Product $product The product to update the entries for. + */ + private function update_stock_status_for( \WC_Product $product ) { + global $wpdb; + + $in_stock = $product->is_in_stock(); + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( + $wpdb->prepare( + 'UPDATE ' . $this->lookup_table_name . ' SET in_stock = %d WHERE product_id = %d', + $in_stock ? 1 : 0, + $product->get_id() + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Delete the lookup table contents related to a given product or variation, + * if it's a variable product it deletes the information for variations too. + * This must be invoked after a product or a variation is trashed or deleted. + * + * @param int|\WC_Product $product Product object or product id. + */ + public function on_product_deleted( $product ) { + if ( ! $this->check_lookup_table_exists() ) { + return; + } + + if ( is_a( $product, \WC_Product::class ) ) { + $product_id = $product->get_id(); + } else { + $product_id = $product; + } + + $this->maybe_schedule_update( $product_id, self::ACTION_DELETE ); + } + + /** + * Create the lookup data for a given product, if a variable product is passed + * the information is created for all of its variations. + * This method is intended to be called from the data regenerator. + * + * @param int|WC_Product $product Product object or id. + * @throws \Exception A variation object is passed. + */ + public function create_data_for_product( $product ) { + if ( ! is_a( $product, \WC_Product::class ) ) { + $product = WC()->call_function( 'wc_get_product', $product ); + } + + if ( $this->is_variation( $product ) ) { + throw new \Exception( "LookupDataStore::create_data_for_product can't be called for variations." ); + } + + $this->delete_data_for( $product->get_id() ); + $this->create_data_for( $product ); + } + + /** + * Create lookup table data for a given product. + * + * @param \WC_Product $product The product to create the data for. + */ + private function create_data_for( \WC_Product $product ) { + if ( $this->is_variation( $product ) ) { + $this->create_data_for_variation( $product ); + } elseif ( $this->is_variable_product( $product ) ) { + $this->create_data_for_variable_product( $product ); + } else { + $this->create_data_for_simple_product( $product ); + } + } + + /** + * Delete all the lookup table entries for a given product, + * if it's a variable product information for variations is deleted too. + * + * @param int $product_id Simple product id, or main/parent product id for variable products. + */ + private function delete_data_for( int $product_id ) { + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( + $wpdb->prepare( + 'DELETE FROM ' . $this->lookup_table_name . ' WHERE product_id = %d OR product_or_parent_id = %d', + $product_id, + $product_id + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Create lookup table entries for a simple (non variable) product. + * Assumes that no entries exist yet. + * + * @param \WC_Product $product The product to create the entries for. + */ + private function create_data_for_simple_product( \WC_Product $product ) { + $product_attributes_data = $this->get_attribute_taxonomies( $product ); + $has_stock = $product->is_in_stock(); + $product_id = $product->get_id(); + foreach ( $product_attributes_data as $taxonomy => $data ) { + $term_ids = $data['term_ids']; + foreach ( $term_ids as $term_id ) { + $this->insert_lookup_table_data( $product_id, $product_id, $taxonomy, $term_id, false, $has_stock ); + } + } + } + + /** + * Create lookup table entries for a variable product. + * Assumes that no entries exist yet. + * + * @param \WC_Product_Variable $product The product to create the entries for. + */ + private function create_data_for_variable_product( \WC_Product_Variable $product ) { + $product_attributes_data = $this->get_attribute_taxonomies( $product ); + $variation_attributes_data = array_filter( + $product_attributes_data, + function( $item ) { + return $item['used_for_variations']; + } + ); + $non_variation_attributes_data = array_filter( + $product_attributes_data, + function( $item ) { + return ! $item['used_for_variations']; + } + ); + + $main_product_has_stock = $product->is_in_stock(); + $main_product_id = $product->get_id(); + + foreach ( $non_variation_attributes_data as $taxonomy => $data ) { + $term_ids = $data['term_ids']; + foreach ( $term_ids as $term_id ) { + $this->insert_lookup_table_data( $main_product_id, $main_product_id, $taxonomy, $term_id, false, $main_product_has_stock ); + } + } + + $term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) ); + $variations = $this->get_variations_of( $product ); + + foreach ( $variation_attributes_data as $taxonomy => $data ) { + foreach ( $variations as $variation ) { + $this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product_id, $data['term_ids'], $term_ids_by_slug_cache ); + } + } + } + + /** + * Create all the necessary lookup data for a given variation. + * + * @param \WC_Product_Variation $variation The variation to create entries for. + */ + private function create_data_for_variation( \WC_Product_Variation $variation ) { + $main_product = WC()->call_function( 'wc_get_product', $variation->get_parent_id() ); + + $product_attributes_data = $this->get_attribute_taxonomies( $main_product ); + $variation_attributes_data = array_filter( + $product_attributes_data, + function( $item ) { + return $item['used_for_variations']; + } + ); + + $term_ids_by_slug_cache = $this->get_term_ids_by_slug_cache( array_keys( $variation_attributes_data ) ); + + foreach ( $variation_attributes_data as $taxonomy => $data ) { + $this->insert_lookup_table_data_for_variation( $variation, $taxonomy, $main_product->get_id(), $data['term_ids'], $term_ids_by_slug_cache ); + } + } + + /** + * Create lookup table entries for a given variation, corresponding to a given taxonomy and a set of term ids. + * + * @param \WC_Product_Variation $variation The variation to create entries for. + * @param string $taxonomy The taxonomy to create the entries for. + * @param int $main_product_id The parent product id. + * @param array $term_ids The term ids to create entries for. + * @param array $term_ids_by_slug_cache A dictionary of term ids by term slug, as returned by 'get_term_ids_by_slug_cache'. + */ + private function insert_lookup_table_data_for_variation( \WC_Product_Variation $variation, string $taxonomy, int $main_product_id, array $term_ids, array $term_ids_by_slug_cache ) { + $variation_id = $variation->get_id(); + $variation_has_stock = $variation->is_in_stock(); + $variation_definition_term_id = $this->get_variation_definition_term_id( $variation, $taxonomy, $term_ids_by_slug_cache ); + if ( $variation_definition_term_id ) { + $this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $variation_definition_term_id, true, $variation_has_stock ); + } else { + $term_ids_for_taxonomy = $term_ids; + foreach ( $term_ids_for_taxonomy as $term_id ) { + $this->insert_lookup_table_data( $variation_id, $main_product_id, $taxonomy, $term_id, true, $variation_has_stock ); + } + } + } + + /** + * Get a cache of term ids by slug for a set of taxonomies, with this format: + * + * [ + * 'taxonomy' => [ + * 'slug_1' => id_1, + * 'slug_2' => id_2, + * ... + * ], ... + * ] + * + * @param array $taxonomies List of taxonomies to build the cache for. + * @return array A dictionary of taxonomies => dictionary of term slug => term id. + */ + private function get_term_ids_by_slug_cache( $taxonomies ) { + $result = array(); + foreach ( $taxonomies as $taxonomy ) { + $terms = WC()->call_function( + 'get_terms', + array( + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + 'fields' => 'id=>slug', + ) + ); + $result[ $taxonomy ] = array_flip( $terms ); + } + return $result; + } + + /** + * Get the id of the term that defines a variation for a given taxonomy, + * or null if there's no such defining id (for variations having "Any " as the definition) + * + * @param \WC_Product_Variation $variation The variation to get the defining term id for. + * @param string $taxonomy The taxonomy to get the defining term id for. + * @param array $term_ids_by_slug_cache A term ids by slug as generated by get_term_ids_by_slug_cache. + * @return int|null The term id, or null if there's no defining id for that taxonomy in that variation. + */ + private function get_variation_definition_term_id( \WC_Product_Variation $variation, string $taxonomy, array $term_ids_by_slug_cache ) { + $variation_attributes = $variation->get_attributes(); + $term_slug = ArrayUtil::get_value_or_default( $variation_attributes, $taxonomy ); + if ( $term_slug ) { + return $term_ids_by_slug_cache[ $taxonomy ][ $term_slug ]; + } else { + return null; + } + } + + /** + * Get the variations of a given variable product. + * + * @param \WC_Product_Variable $product The product to get the variations for. + * @return array An array of WC_Product_Variation objects. + */ + private function get_variations_of( \WC_Product_Variable $product ) { + $variation_ids = $product->get_children(); + return array_map( + function( $id ) { + return WC()->call_function( 'wc_get_product', $id ); + }, + $variation_ids + ); + } + + /** + * Check if a given product is a variable product. + * + * @param \WC_Product $product The product to check. + * @return bool True if it's a variable product, false otherwise. + */ + private function is_variable_product( \WC_Product $product ) { + return is_a( $product, \WC_Product_Variable::class ); + } + + /** + * Check if a given product is a variation. + * + * @param \WC_Product $product The product to check. + * @return bool True if it's a variation, false otherwise. + */ + private function is_variation( \WC_Product $product ) { + return is_a( $product, \WC_Product_Variation::class ); + } + + /** + * Return the list of taxonomies used for variations on a product together with + * the associated term ids, with the following format: + * + * [ + * 'taxonomy_name' => + * [ + * 'term_ids' => [id, id, ...], + * 'used_for_variations' => true|false + * ], ... + * ] + * + * @param \WC_Product $product The product to get the attribute taxonomies for. + * @return array Information about the attribute taxonomies of the product. + */ + private function get_attribute_taxonomies( \WC_Product $product ) { + $product_attributes = $product->get_attributes(); + $result = array(); + foreach ( $product_attributes as $taxonomy_name => $attribute_data ) { + if ( ! $attribute_data->get_id() ) { + // Custom product attribute, not suitable for attribute-based filtering. + continue; + } + + $result[ $taxonomy_name ] = array( + 'term_ids' => $attribute_data->get_options(), + 'used_for_variations' => $attribute_data->get_variation(), + ); + } + + return $result; + } + + /** + * Insert one entry in the lookup table. + * + * @param int $product_id The product id. + * @param int $product_or_parent_id The product id for non-variable products, the main/parent product id for variations. + * @param string $taxonomy Taxonomy name. + * @param int $term_id Term id. + * @param bool $is_variation_attribute True if the taxonomy corresponds to an attribute used to define variations. + * @param bool $has_stock True if the product is in stock. + */ + private function insert_lookup_table_data( int $product_id, int $product_or_parent_id, string $taxonomy, int $term_id, bool $is_variation_attribute, bool $has_stock ) { + global $wpdb; + + // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( + $wpdb->prepare( + 'INSERT INTO ' . $this->lookup_table_name . ' ( + product_id, + product_or_parent_id, + taxonomy, + term_id, + is_variation_attribute, + in_stock) + VALUES + ( %d, %d, %s, %d, %d, %d )', + $product_id, + $product_or_parent_id, + $taxonomy, + $term_id, + $is_variation_attribute ? 1 : 0, + $has_stock ? 1 : 0 + ) + ); + // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared + } + + /** + * Tells if a lookup table regeneration is currently in progress. + * + * @return bool True if a lookup table regeneration is already in progress. + */ + public function regeneration_is_in_progress() { + return 'yes' === get_option( 'woocommerce_attribute_lookup_regeneration_in_progress', null ); + } + + /** + * Set a permanent flag (via option) indicating that the lookup table regeneration is in process. + */ + public function set_regeneration_in_progress_flag() { + update_option( 'woocommerce_attribute_lookup_regeneration_in_progress', 'yes' ); + } + + /** + * Remove the flag indicating that the lookup table regeneration is in process. + */ + public function unset_regeneration_in_progress_flag() { + delete_option( 'woocommerce_attribute_lookup_regeneration_in_progress' ); + } +} diff --git a/src/Internal/RestApiUtil.php b/src/Internal/RestApiUtil.php new file mode 100644 index 0000000..bf4cbd8 --- /dev/null +++ b/src/Internal/RestApiUtil.php @@ -0,0 +1,202 @@ + "", + * "api_refund" => "x", + * "api_restock" => "x", + * "line_items" => [ + * "id" => "111", + * "quantity" => 222, + * "refund_total" => 333, + * "refund_tax" => [ + * [ + * "id": "444", + * "refund_total": 555 + * ],... + * ],... + * ] + * + * ...to the internally used format: + * + * [ + * "reason" => null, (if it's missing or any empty value, set as null) + * "api_refund" => true, (if it's missing or non-bool, set as "true") + * "api_restock" => true, (if it's missing or non-bool, set as "true") + * "line_items" => [ (convert sequential array to associative based on "id") + * "111" => [ + * "qty" => 222, (rename "quantity" to "qty") + * "refund_total" => 333, + * "refund_tax" => [ (convert sequential array to associative based on "id" and "refund_total) + * "444" => 555,... + * ],... + * ] + * ] + * + * It also calculates the amount if missing and whenever possible, see maybe_calculate_refund_amount_from_line_items. + * + * The conversion is done in a way that if the request is already in the internal format, + * then nothing is changed for compatibility. For example, if the line items array + * is already an associative array or any of its elements + * is missing the "id" key, then the entire array is left unchanged. + * Same for the "refund_tax" array inside each line item. + * + * @param \WP_REST_Request $request The request to adjust. + */ + public static function adjust_create_refund_request_parameters( \WP_REST_Request &$request ) { + if ( empty( $request['reason'] ) ) { + $request['reason'] = null; + } + + if ( ! is_bool( $request['api_refund'] ) ) { + $request['api_refund'] = true; + } + + if ( ! is_bool( $request['api_restock'] ) ) { + $request['api_restock'] = true; + } + + if ( empty( $request['line_items'] ) ) { + $request['line_items'] = array(); + } else { + $request['line_items'] = self::adjust_line_items_for_create_refund_request( $request['line_items'] ); + } + + if ( ! isset( $request['amount'] ) ) { + $amount = self::calculate_refund_amount_from_line_items( $request ); + if ( null !== $amount ) { + $request['amount'] = strval( $amount ); + } + } + } + + /** + * Calculate the "amount" parameter for the request based on the amounts found in line items. + * This will ONLY be possible if ALL of the following is true: + * + * - "line_items" in the request is a non-empty array. + * - All line items have a "refund_total" field with a numeric value. + * - All values inside "refund_tax" in all line items are a numeric value. + * + * The request is assumed to be in internal format already. + * + * @param \WP_REST_Request $request The request to maybe calculate the total amount for. + * @return number|null The calculated amount, or null if it can't be calculated. + */ + private static function calculate_refund_amount_from_line_items( $request ) { + $line_items = $request['line_items']; + + if ( ! is_array( $line_items ) || empty( $line_items ) ) { + return null; + } + + $amount = 0; + + foreach ( $line_items as $item ) { + if ( ! isset( $item['refund_total'] ) || ! is_numeric( $item['refund_total'] ) ) { + return null; + } + + $amount += $item['refund_total']; + + if ( ! isset( $item['refund_tax'] ) ) { + continue; + } + + foreach ( $item['refund_tax'] as $tax ) { + if ( ! is_numeric( $tax ) ) { + return null; + } + $amount += $tax; + } + } + + return $amount; + } + + /** + * Convert the line items of a refund request to internal format (see adjust_create_refund_request_parameters). + * + * @param array $line_items The line items to convert. + * @return array The converted line items. + */ + private static function adjust_line_items_for_create_refund_request( $line_items ) { + if ( ! is_array( $line_items ) || empty( $line_items ) || self::is_associative( $line_items ) ) { + return $line_items; + } + + $new_array = array(); + foreach ( $line_items as $item ) { + if ( ! isset( $item['id'] ) ) { + return $line_items; + } + + if ( isset( $item['quantity'] ) && ! isset( $item['qty'] ) ) { + $item['qty'] = $item['quantity']; + } + unset( $item['quantity'] ); + + if ( isset( $item['refund_tax'] ) ) { + $item['refund_tax'] = self::adjust_taxes_for_create_refund_request_line_item( $item['refund_tax'] ); + } + + $id = $item['id']; + $new_array[ $id ] = $item; + + unset( $new_array[ $id ]['id'] ); + } + + return $new_array; + } + + /** + * Adjust the taxes array from a line item in a refund request, see adjust_create_refund_parameters. + * + * @param array $taxes_array The array to adjust. + * @return array The adjusted array. + */ + private static function adjust_taxes_for_create_refund_request_line_item( $taxes_array ) { + if ( ! is_array( $taxes_array ) || empty( $taxes_array ) || self::is_associative( $taxes_array ) ) { + return $taxes_array; + } + + $new_array = array(); + foreach ( $taxes_array as $item ) { + if ( ! isset( $item['id'] ) || ! isset( $item['refund_total'] ) ) { + return $taxes_array; + } + + $id = $item['id']; + $refund_total = $item['refund_total']; + $new_array[ $id ] = $refund_total; + } + + return $new_array; + } + + /** + * Is an array sequential or associative? + * + * @param array $array The array to check. + * @return bool True if the array is associative, false if it's sequential. + */ + private static function is_associative( array $array ) { + return array_keys( $array ) !== range( 0, count( $array ) - 1 ); + } +} diff --git a/src/Internal/RestockRefundedItemsAdjuster.php b/src/Internal/RestockRefundedItemsAdjuster.php new file mode 100644 index 0000000..b4ccb4f --- /dev/null +++ b/src/Internal/RestockRefundedItemsAdjuster.php @@ -0,0 +1,77 @@ +order_factory = wc_get_container()->get( LegacyProxy::class )->get_instance_of( \WC_Order_Factory::class ); + add_action( 'woocommerce_before_save_order_items', array( $this, 'initialize_restock_refunded_items' ), 10, 2 ); + } + + /** + * Initializes the restock refunded items meta for order version less than 5.5. + * + * @see https://github.com/woocommerce/woocommerce/issues/29502 + * + * @param int $order_id Order ID. + * @param array $items Order items to save. + */ + public function initialize_restock_refunded_items( $order_id, $items ) { + $order = wc_get_order( $order_id ); + $order_version = $order->get_version(); + + if ( version_compare( $order_version, '5.5', '>=' ) ) { + return; + } + + // If there are no refund lines, then this migration isn't necessary because restock related meta's wouldn't be set. + if ( 0 === count( $order->get_refunds() ) ) { + return; + } + + if ( isset( $items['order_item_id'] ) ) { + foreach ( $items['order_item_id'] as $item_id ) { + $item = $this->order_factory::get_order_item( absint( $item_id ) ); + + if ( ! $item ) { + continue; + } + + if ( 'line_item' !== $item->get_type() ) { + continue; + } + + // There could be code paths in custom code which don't update version number but still update the items. + if ( '' !== $item->get_meta( '_restock_refunded_items', true ) ) { + continue; + } + + $refunded_item_quantity = abs( $order->get_qty_refunded_for_item( $item->get_id() ) ); + $item->add_meta_data( '_restock_refunded_items', $refunded_item_quantity, false ); + $item->save(); + } + } + } +} diff --git a/src/Internal/WCCom/ConnectionHelper.php b/src/Internal/WCCom/ConnectionHelper.php new file mode 100644 index 0000000..d3c771d --- /dev/null +++ b/src/Internal/WCCom/ConnectionHelper.php @@ -0,0 +1,29 @@ + '\\Automattic\\WooCommerce\\Blocks\\Package', + 'woocommerce-admin' => '\\Automattic\\WooCommerce\\Admin\\Composer\\Package', + ); + + /** + * Init the package loader. + * + * @since 3.7.0 + */ + public static function init() { + add_action( 'plugins_loaded', array( __CLASS__, 'on_init' ) ); + } + + /** + * Callback for WordPress init hook. + */ + public static function on_init() { + self::load_packages(); + } + + /** + * Checks a package exists by looking for it's directory. + * + * @param string $package Package name. + * @return boolean + */ + public static function package_exists( $package ) { + return file_exists( dirname( __DIR__ ) . '/packages/' . $package ); + } + + /** + * Loads packages after plugins_loaded hook. + * + * Each package should include an init file which loads the package so it can be used by core. + */ + protected static function load_packages() { + foreach ( self::$packages as $package_name => $package_class ) { + if ( ! self::package_exists( $package_name ) ) { + self::missing_package( $package_name ); + continue; + } + call_user_func( array( $package_class, 'init' ) ); + } + + // Proxies "activated_plugin" hook for embedded packages listen on WC plugin activation + // https://github.com/woocommerce/woocommerce/issues/28697. + if ( is_admin() ) { + $activated_plugin = get_transient( 'woocommerce_activated_plugin' ); + if ( $activated_plugin ) { + delete_transient( 'woocommerce_activated_plugin' ); + + /** + * WooCommerce is activated hook. + * + * @since 5.0.0 + * @param bool $activated_plugin Activated plugin path, + * generally woocommerce/woocommerce.php. + */ + do_action( 'woocommerce_activated_plugin', $activated_plugin ); + } + } + } + + /** + * If a package is missing, add an admin notice. + * + * @param string $package Package name. + */ + protected static function missing_package( $package ) { + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( // phpcs:ignore + sprintf( + /* Translators: %s package name. */ + esc_html__( 'Missing the WooCommerce %s package', 'woocommerce' ), + '' . esc_html( $package ) . '' + ) . ' - ' . esc_html__( 'Your installation of WooCommerce is incomplete. If you installed WooCommerce from GitHub, please refer to this document to set up your development environment: https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment', 'woocommerce' ) + ); + } + add_action( + 'admin_notices', + function() use ( $package ) { + ?> +
    +

    + + ' . esc_html( $package ) . '' + ); + ?> + +
    + ', + '' + ); + ?> +

    +
    + $method( ...$args ); + } + + // If the class is a singleton, use the "instance" method. + if ( method_exists( $class_name, 'instance' ) ) { + return $class_name::instance( ...$args ); + } + + // If the class has a "load" method, use it. + if ( method_exists( $class_name, 'load' ) ) { + return $class_name::load( ...$args ); + } + + // Fallback to simply creating a new instance of the class. + return new $class_name( ...$args ); + } + + /** + * Get an instance of a class implementing WC_Queue_Interface. + * + * @return \WC_Queue_Interface The instance. + */ + private function get_instance_of_wc_queue_interface() { + return \WC_Queue::instance(); + } + + /** + * Call a user function. This should be used to execute any non-idempotent function, especially + * those in the `includes` directory or provided by WordPress. + * + * @param string $function_name The function to execute. + * @param mixed ...$parameters The parameters to pass to the function. + * + * @return mixed The result from the function. + */ + public function call_function( $function_name, ...$parameters ) { + return call_user_func_array( $function_name, $parameters ); + } + + /** + * Call a static method in a class. This should be used to execute any non-idempotent method in classes + * from the `includes` directory. + * + * @param string $class_name The name of the class containing the method. + * @param string $method_name The name of the method. + * @param mixed ...$parameters The parameters to pass to the method. + * + * @return mixed The result from the method. + */ + public function call_static( $class_name, $method_name, ...$parameters ) { + return call_user_func_array( "$class_name::$method_name", $parameters ); + } +} diff --git a/src/Utilities/ArrayUtil.php b/src/Utilities/ArrayUtil.php new file mode 100644 index 0000000..8f63dbd --- /dev/null +++ b/src/Utilities/ArrayUtil.php @@ -0,0 +1,71 @@ + [ 'bar' => [ 'fizz' => 'buzz' ] ] ] the value for key 'foo::bar::fizz' would be 'buzz'. + * + * @param array $array The array to get the value from. + * @param string $key The complete key hierarchy, using '::' as separator. + * @param mixed $default The value to return if the key doesn't exist in the array. + * + * @return mixed The retrieved value, or the supplied default value. + * @throws \Exception $array is not an array. + */ + public static function get_nested_value( array $array, string $key, $default = null ) { + $key_stack = explode( '::', $key ); + $subkey = array_shift( $key_stack ); + + if ( isset( $array[ $subkey ] ) ) { + $value = $array[ $subkey ]; + + if ( count( $key_stack ) ) { + foreach ( $key_stack as $subkey ) { + if ( is_array( $value ) && isset( $value[ $subkey ] ) ) { + $value = $value[ $subkey ]; + } else { + $value = $default; + break; + } + } + } + } else { + $value = $default; + } + + return $value; + } + + /** + * Checks if a given key exists in an array and its value can be evaluated as 'true'. + * + * @param array $array The array to check. + * @param string $key The key for the value to check. + * @return bool True if the key exists in the array and the value can be evaluated as 'true'. + */ + public static function is_truthy( array $array, string $key ) { + return isset( $array[ $key ] ) && $array[ $key ]; + } + + /** + * Gets the value for a given key from an array, or a default value if the key doesn't exist in the array. + * + * @param array $array The array to get the value from. + * @param string $key The key to use to retrieve the value. + * @param null $default The default value to return if the key doesn't exist in the array. + * @return mixed|null The value for the key, or the default value passed. + */ + public static function get_value_or_default( array $array, string $key, $default = null ) { + return isset( $array[ $key ] ) ? $array[ $key ] : $default; + } +} + diff --git a/src/Utilities/NumberUtil.php b/src/Utilities/NumberUtil.php new file mode 100644 index 0000000..8e1a417 --- /dev/null +++ b/src/Utilities/NumberUtil.php @@ -0,0 +1,34 @@ + strlen( $string ) ) { + return false; + } + + $string = substr( $string, 0, $len ); + + if ( $case_sensitive ) { + return strcmp( $string, $starts_with ) === 0; + } + + return strcasecmp( $string, $starts_with ) === 0; + } + + /** + * Checks to see whether or not a string ends with another. + * + * @param string $string The string we want to check. + * @param string $ends_with The string we're looking for at the end of $string. + * @param bool $case_sensitive Indicates whether the comparison should be case-sensitive. + * + * @return bool True if the $string ends with $ends_with, false otherwise. + */ + public static function ends_with( string $string, string $ends_with, bool $case_sensitive = true ): bool { + $len = strlen( $ends_with ); + if ( $len > strlen( $string ) ) { + return false; + } + + $string = substr( $string, -$len ); + + if ( $case_sensitive ) { + return strcmp( $string, $ends_with ) === 0; + } + + return strcasecmp( $string, $ends_with ) === 0; + } +} diff --git a/templates/archive-product.php b/templates/archive-product.php new file mode 100644 index 0000000..998cd5c --- /dev/null +++ b/templates/archive-product.php @@ -0,0 +1,105 @@ + +
    + +

    + + + +
    + + + + diff --git a/templates/auth/form-grant-access.php b/templates/auth/form-grant-access.php new file mode 100644 index 0000000..0b67e7a --- /dev/null +++ b/templates/auth/form-grant-access.php @@ -0,0 +1,61 @@ + + + + +

    + +

    + + + +

    + ' . esc_html( $app_name ) . '', '' . esc_html( $scope ) . '' ); + ?> +

    + +
      + +
    • + +
    + +
    + ID, 70 ); ?> +

    + display_name ) ); + ?> + +

    +
    + +

    + + +

    + + diff --git a/templates/auth/form-login.php b/templates/auth/form-login.php new file mode 100644 index 0000000..3606ca0 --- /dev/null +++ b/templates/auth/form-login.php @@ -0,0 +1,54 @@ + + +

    + +

    + + + +

    + cancel and return to %1$s', 'woocommerce' ), esc_html( wc_clean( $app_name ) ), esc_url( $return_url ) ) ); + ?> +

    + + + + diff --git a/templates/auth/header.php b/templates/auth/header.php new file mode 100644 index 0000000..e05ccc9 --- /dev/null +++ b/templates/auth/header.php @@ -0,0 +1,33 @@ + +> + + + + + <?php esc_html_e( 'Application authentication request', 'woocommerce' ); ?> + + + + +

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

    +
    diff --git a/templates/cart/cart-empty.php b/templates/cart/cart-empty.php new file mode 100644 index 0000000..0a463e9 --- /dev/null +++ b/templates/cart/cart-empty.php @@ -0,0 +1,39 @@ + 0 ) : ?> +

    + + + +

    + diff --git a/templates/cart/cart-item-data.php b/templates/cart/cart-item-data.php new file mode 100644 index 0000000..d930009 --- /dev/null +++ b/templates/cart/cart-item-data.php @@ -0,0 +1,26 @@ + +
    + +
    :
    +
    + +
    diff --git a/templates/cart/cart-shipping.php b/templates/cart/cart-shipping.php new file mode 100644 index 0000000..134b05b --- /dev/null +++ b/templates/cart/cart-shipping.php @@ -0,0 +1,83 @@ +countries->get_formatted_address( $package['destination'], ', ' ); +$has_calculated_shipping = ! empty( $has_calculated_shipping ); +$show_shipping_calculator = ! empty( $show_shipping_calculator ); +$calculator_text = ''; +?> + + + + +
      + +
    • + ', $index, esc_attr( sanitize_title( $method->id ) ), esc_attr( $method->id ), checked( $method->id, $chosen_method, false ) ); // WPCS: XSS ok. + } else { + printf( '', $index, esc_attr( sanitize_title( $method->id ) ), esc_attr( $method->id ) ); // WPCS: XSS ok. + } + printf( '', $index, esc_attr( sanitize_title( $method->id ) ), wc_cart_totals_shipping_method_label( $method ) ); // WPCS: XSS ok. + do_action( 'woocommerce_after_shipping_rate', $method, $index ); + ?> +
    • + +
    + +

    + ' . esc_html( $formatted_destination ) . '' ); + $calculator_text = esc_html__( 'Change address', 'woocommerce' ); + } else { + echo wp_kses_post( apply_filters( 'woocommerce_shipping_estimate_html', __( 'Shipping options will be updated during checkout.', 'woocommerce' ) ) ); + } + ?> +

    + + ' . esc_html( $formatted_destination ) . '' ) ) ); + $calculator_text = esc_html__( 'Enter a different address', 'woocommerce' ); + endif; + ?> + + + ' . esc_html( $package_details ) . '

    '; ?> + + + + + + + diff --git a/templates/cart/cart-totals.php b/templates/cart/cart-totals.php new file mode 100644 index 0000000..f5af332 --- /dev/null +++ b/templates/cart/cart-totals.php @@ -0,0 +1,112 @@ + +
    + + + +

    + + + + + + + + + cart->get_coupons() as $code => $coupon ) : ?> + + + + + + + cart->needs_shipping() && WC()->cart->show_shipping() ) : ?> + + + + + + + + cart->needs_shipping() && 'yes' === get_option( 'woocommerce_enable_shipping_calc' ) ) : ?> + + + + + + + + + cart->get_fees() as $fee ) : ?> + + + + + + + cart->display_prices_including_tax() ) { + $taxable_address = WC()->customer->get_taxable_address(); + $estimated_text = ''; + + if ( WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ) { + /* translators: %s location. */ + $estimated_text = sprintf( ' ' . esc_html__( '(estimated for %s)', 'woocommerce' ) . '', WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ] ); + } + + if ( 'itemized' === get_option( 'woocommerce_tax_total_display' ) ) { + foreach ( WC()->cart->get_tax_totals() as $code => $tax ) { // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + ?> + + + + + + + + + + + + + + + + + + + + +
    name ); ?>
    label ) . $estimated_text; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>formatted_amount ); ?>
    countries->tax_or_vat() ) . $estimated_text; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
    + +
    + +
    + + + +
    diff --git a/templates/cart/cart.php b/templates/cart/cart.php new file mode 100644 index 0000000..adf15ed --- /dev/null +++ b/templates/cart/cart.php @@ -0,0 +1,176 @@ + + +
    + + + + + + + + + + + + + + + + + cart->get_cart() as $cart_item_key => $cart_item ) { + $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); + $product_id = apply_filters( 'woocommerce_cart_item_product_id', $cart_item['product_id'], $cart_item, $cart_item_key ); + + if ( $_product && $_product->exists() && $cart_item['quantity'] > 0 && apply_filters( 'woocommerce_cart_item_visible', true, $cart_item, $cart_item_key ) ) { + $product_permalink = apply_filters( 'woocommerce_cart_item_permalink', $_product->is_visible() ? $_product->get_permalink( $cart_item ) : '', $cart_item, $cart_item_key ); + ?> + + + + + + + + + + + + + + + + + + + + + + + + +
      
    + ×', + esc_url( wc_get_cart_remove_url( $cart_item_key ) ), + esc_html__( 'Remove this item', 'woocommerce' ), + esc_attr( $product_id ), + esc_attr( $_product->get_sku() ) + ), + $cart_item_key + ); + ?> + + get_image(), $cart_item, $cart_item_key ); + + if ( ! $product_permalink ) { + echo $thumbnail; // PHPCS: XSS ok. + } else { + printf( '%s', esc_url( $product_permalink ), $thumbnail ); // PHPCS: XSS ok. + } + ?> + + get_name(), $cart_item, $cart_item_key ) . ' ' ); + } else { + echo wp_kses_post( apply_filters( 'woocommerce_cart_item_name', sprintf( '%s', esc_url( $product_permalink ), $_product->get_name() ), $cart_item, $cart_item_key ) ); + } + + do_action( 'woocommerce_after_cart_item_name', $cart_item, $cart_item_key ); + + // Meta data. + echo wc_get_formatted_cart_item_data( $cart_item ); // PHPCS: XSS ok. + + // Backorder notification. + if ( $_product->backorders_require_notification() && $_product->is_on_backorder( $cart_item['quantity'] ) ) { + echo wp_kses_post( apply_filters( 'woocommerce_cart_item_backorder_notification', '

    ' . esc_html__( 'Available on backorder', 'woocommerce' ) . '

    ', $product_id ) ); + } + ?> +
    + cart->get_product_price( $_product ), $cart_item, $cart_item_key ); // PHPCS: XSS ok. + ?> + + is_sold_individually() ) { + $product_quantity = sprintf( '1 ', $cart_item_key ); + } else { + $product_quantity = woocommerce_quantity_input( + array( + 'input_name' => "cart[{$cart_item_key}][qty]", + 'input_value' => $cart_item['quantity'], + 'max_value' => $_product->get_max_purchase_quantity(), + 'min_value' => '0', + 'product_name' => $_product->get_name(), + ), + $_product, + false + ); + } + + echo apply_filters( 'woocommerce_cart_item_quantity', $product_quantity, $cart_item_key, $cart_item ); // PHPCS: XSS ok. + ?> + + cart->get_product_subtotal( $_product, $cart_item['quantity'] ), $cart_item, $cart_item_key ); // PHPCS: XSS ok. + ?> +
    + + +
    + + +
    + + + + + + + +
    + +
    + + + +
    + +
    + + diff --git a/templates/cart/cross-sells.php b/templates/cart/cross-sells.php new file mode 100644 index 0000000..2f9a478 --- /dev/null +++ b/templates/cart/cross-sells.php @@ -0,0 +1,51 @@ + + +
    + +

    + + + + + + + get_id() ); + + setup_postdata( $GLOBALS['post'] =& $post_object ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, Squiz.PHP.DisallowMultipleAssignments.Found + + wc_get_template_part( 'content', 'product' ); + ?> + + + + + +
    + + +cart->is_empty() ) : ?> + +
      + cart->get_cart() as $cart_item_key => $cart_item ) { + $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); + $product_id = apply_filters( 'woocommerce_cart_item_product_id', $cart_item['product_id'], $cart_item, $cart_item_key ); + + if ( $_product && $_product->exists() && $cart_item['quantity'] > 0 && apply_filters( 'woocommerce_widget_cart_item_visible', true, $cart_item, $cart_item_key ) ) { + $product_name = apply_filters( 'woocommerce_cart_item_name', $_product->get_name(), $cart_item, $cart_item_key ); + $thumbnail = apply_filters( 'woocommerce_cart_item_thumbnail', $_product->get_image(), $cart_item, $cart_item_key ); + $product_price = apply_filters( 'woocommerce_cart_item_price', WC()->cart->get_product_price( $_product ), $cart_item, $cart_item_key ); + $product_permalink = apply_filters( 'woocommerce_cart_item_permalink', $_product->is_visible() ? $_product->get_permalink( $cart_item ) : '', $cart_item, $cart_item_key ); + ?> +
    • + ×', + esc_url( wc_get_cart_remove_url( $cart_item_key ) ), + esc_attr__( 'Remove this item', 'woocommerce' ), + esc_attr( $product_id ), + esc_attr( $cart_item_key ), + esc_attr( $_product->get_sku() ) + ), + $cart_item_key + ); + ?> + + + + + + + + + ' . sprintf( '%s × %s', $cart_item['quantity'], $product_price ) . '', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
    • + +
    + +

    + +

    + + + +

    + + + + + +

    + + + + diff --git a/templates/cart/proceed-to-checkout-button.php b/templates/cart/proceed-to-checkout-button.php new file mode 100644 index 0000000..d678926 --- /dev/null +++ b/templates/cart/proceed-to-checkout-button.php @@ -0,0 +1,27 @@ + + + + + diff --git a/templates/cart/shipping-calculator.php b/templates/cart/shipping-calculator.php new file mode 100644 index 0000000..3cd4402 --- /dev/null +++ b/templates/cart/shipping-calculator.php @@ -0,0 +1,91 @@ + + +
    + + %s', esc_html( ! empty( $button_text ) ? $button_text : __( 'Calculate shipping', 'woocommerce' ) ) ); ?> + + +
    + + diff --git a/templates/checkout/cart-errors.php b/templates/checkout/cart-errors.php new file mode 100644 index 0000000..85a280e --- /dev/null +++ b/templates/checkout/cart-errors.php @@ -0,0 +1,25 @@ + + +

    + + + +

    diff --git a/templates/checkout/form-billing.php b/templates/checkout/form-billing.php new file mode 100644 index 0000000..9889929 --- /dev/null +++ b/templates/checkout/form-billing.php @@ -0,0 +1,74 @@ + +
    + cart->needs_shipping() ) : ?> + +

    + + + +

    + + + + + +
    + get_checkout_fields( 'billing' ); + + foreach ( $fields as $key => $field ) { + woocommerce_form_field( $key, $field, $checkout->get_value( $key ) ); + } + ?> +
    + + +
    + +is_registration_enabled() ) : ?> + + diff --git a/templates/checkout/form-checkout.php b/templates/checkout/form-checkout.php new file mode 100644 index 0000000..00d5459 --- /dev/null +++ b/templates/checkout/form-checkout.php @@ -0,0 +1,66 @@ +is_registration_enabled() && $checkout->is_registration_required() && ! is_user_logged_in() ) { + echo esc_html( apply_filters( 'woocommerce_checkout_must_be_logged_in_message', __( 'You must be logged in to checkout.', 'woocommerce' ) ) ); + return; +} + +?> + +
    + + get_checkout_fields() ) : ?> + + + +
    +
    + +
    + +
    + +
    +
    + + + + + + + +

    + + + +
    + +
    + + + +
    + + diff --git a/templates/checkout/form-coupon.php b/templates/checkout/form-coupon.php new file mode 100644 index 0000000..bca0336 --- /dev/null +++ b/templates/checkout/form-coupon.php @@ -0,0 +1,42 @@ + +
    + ' . esc_html__( 'Click here to enter your code', 'woocommerce' ) . '' ), 'notice' ); ?> +
    + + diff --git a/templates/checkout/form-login.php b/templates/checkout/form-login.php new file mode 100644 index 0000000..c9da081 --- /dev/null +++ b/templates/checkout/form-login.php @@ -0,0 +1,36 @@ + + + esc_html__( 'If you have shopped with us before, please enter your details below. If you are a new customer, please proceed to the Billing section.', 'woocommerce' ), + 'redirect' => wc_get_checkout_url(), + 'hidden' => true, + ) +); diff --git a/templates/checkout/form-pay.php b/templates/checkout/form-pay.php new file mode 100644 index 0000000..e7bdd50 --- /dev/null +++ b/templates/checkout/form-pay.php @@ -0,0 +1,98 @@ +get_order_item_totals(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited +?> +
    + + + + + + + + + + + get_items() ) > 0 ) : ?> + get_items() as $item_id => $item ) : ?> + + + + + + + + + + + + + + + + + + + +
    + get_name(), $item, false ) ); + + do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $order, false ); + + wc_display_item_meta( $item ); + + do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $order, false ); + ?> + ' . sprintf( '× %s', esc_html( $item->get_quantity() ) ) . '', $item ); ?>get_formatted_line_subtotal( $item ); ?>
    + +
    + needs_payment() ) : ?> +
      + $gateway ) ); + } + } else { + echo '
    • ' . apply_filters( 'woocommerce_no_available_payment_methods_message', esc_html__( 'Sorry, it seems that there are no available payment methods for your location. Please contact us if you require assistance or wish to make alternate arrangements.', 'woocommerce' ) ) . '
    • '; // @codingStandardsIgnoreLine + } + ?> +
    + +
    + + + + + + + ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> + + + + +
    +
    +
    diff --git a/templates/checkout/form-shipping.php b/templates/checkout/form-shipping.php new file mode 100644 index 0000000..f395ba2 --- /dev/null +++ b/templates/checkout/form-shipping.php @@ -0,0 +1,70 @@ + +
    + cart->needs_shipping_address() ) : ?> + +

    + +

    + +
    + + + +
    + get_checkout_fields( 'shipping' ); + + foreach ( $fields as $key => $field ) { + woocommerce_form_field( $key, $field, $checkout->get_value( $key ) ); + } + ?> +
    + + + +
    + + +
    +
    + + + + + cart->needs_shipping() || wc_ship_to_billing_address_only() ) : ?> + +

    + + + +
    + get_checkout_fields( 'order' ) as $key => $field ) : ?> + get_value( $key ) ); ?> + +
    + + + + +
    diff --git a/templates/checkout/order-receipt.php b/templates/checkout/order-receipt.php new file mode 100644 index 0000000..bd8ca6f --- /dev/null +++ b/templates/checkout/order-receipt.php @@ -0,0 +1,46 @@ + + +
      +
    • + + get_order_number() ); ?> +
    • +
    • + + get_date_created() ) ); ?> +
    • +
    • + + get_formatted_order_total() ); ?> +
    • + get_payment_method_title() ) : ?> +
    • + + get_payment_method_title() ); ?> +
    • + +
    + +get_payment_method(), $order->get_id() ); ?> + +
    diff --git a/templates/checkout/payment-method.php b/templates/checkout/payment-method.php new file mode 100644 index 0000000..a3d4dbd --- /dev/null +++ b/templates/checkout/payment-method.php @@ -0,0 +1,33 @@ + +
  • + chosen, true ); ?> data-order_button_text="order_button_text ); ?>" /> + + + has_fields() || $gateway->get_description() ) : ?> +
    chosen ) : /* phpcs:ignore Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace */ ?>style="display:none;"> + payment_fields(); ?> +
    + +
  • diff --git a/templates/checkout/payment.php b/templates/checkout/payment.php new file mode 100644 index 0000000..a5467a7 --- /dev/null +++ b/templates/checkout/payment.php @@ -0,0 +1,61 @@ + +
    + cart->needs_payment() ) : ?> +
      + $gateway ) ); + } + } else { + echo '
    • ' . apply_filters( 'woocommerce_no_available_payment_methods_message', WC()->customer->get_billing_country() ? esc_html__( 'Sorry, it seems that there are no available payment methods for your state. Please contact us if you require assistance or wish to make alternate arrangements.', 'woocommerce' ) : esc_html__( 'Please fill in your details above to see available payment methods.', 'woocommerce' ) ) . '
    • '; // @codingStandardsIgnoreLine + } + ?> +
    + +
    + + + + + + + ' . esc_html( $order_button_text ) . '' ); // @codingStandardsIgnoreLine ?> + + + + +
    +
    + + + + + + + + + + cart->get_cart() as $cart_item_key => $cart_item ) { + $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key ); + + if ( $_product && $_product->exists() && $cart_item['quantity'] > 0 && apply_filters( 'woocommerce_checkout_cart_item_visible', true, $cart_item, $cart_item_key ) ) { + ?> + + + + + + + + + + + + + + cart->get_coupons() as $code => $coupon ) : ?> + + + + + + + cart->needs_shipping() && WC()->cart->show_shipping() ) : ?> + + + + + + + + + + cart->get_fees() as $fee ) : ?> + + + + + + + cart->display_prices_including_tax() ) : ?> + + cart->get_tax_totals() as $code => $tax ) : // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited ?> + + + + + + + + + + + + + + + + + + + + + + + +
    + get_name(), $cart_item, $cart_item_key ) ) . ' '; ?> + ' . sprintf( '× %s', $cart_item['quantity'] ) . '', $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + + + cart->get_product_subtotal( $_product, $cart_item['quantity'] ), $cart_item, $cart_item_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
    name ); ?>
    label ); ?>formatted_amount ); ?>
    countries->tax_or_vat() ); ?>
    diff --git a/templates/checkout/terms.php b/templates/checkout/terms.php new file mode 100644 index 0000000..21ade90 --- /dev/null +++ b/templates/checkout/terms.php @@ -0,0 +1,40 @@ + +
    + + + +

    + + +

    + +
    + + +
    + + get_id() ); + ?> + + has_status( 'failed' ) ) : ?> + +

    + +

    + + + + +

    + + + +

    + +
      + +
    • + + get_order_number(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
    • + +
    • + + get_date_created() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
    • + + get_user_id() === get_current_user_id() && $order->get_billing_email() ) : ?> + + + +
    • + + get_formatted_order_total(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
    • + + get_payment_method_title() ) : ?> +
    • + + get_payment_method_title() ); ?> +
    • + + +
    + + + + get_payment_method(), $order->get_id() ); ?> + get_id() ); ?> + + + +

    + + + +
    diff --git a/templates/content-product-cat.php b/templates/content-product-cat.php new file mode 100644 index 0000000..e212f98 --- /dev/null +++ b/templates/content-product-cat.php @@ -0,0 +1,57 @@ + +
  • > + +
  • diff --git a/templates/content-product.php b/templates/content-product.php new file mode 100644 index 0000000..e354ae2 --- /dev/null +++ b/templates/content-product.php @@ -0,0 +1,67 @@ +is_visible() ) { + return; +} +?> +
  • > + +
  • diff --git a/templates/content-single-product.php b/templates/content-single-product.php new file mode 100644 index 0000000..37a2c06 --- /dev/null +++ b/templates/content-single-product.php @@ -0,0 +1,76 @@ + +
    > + + + +
    + +
    + + +
    + + diff --git a/templates/content-widget-price-filter.php b/templates/content-widget-price-filter.php new file mode 100644 index 0000000..a491d34 --- /dev/null +++ b/templates/content-widget-price-filter.php @@ -0,0 +1,40 @@ + + + +
    +
    + +
    + + + + + + +
    +
    +
    +
    + + diff --git a/templates/content-widget-product.php b/templates/content-widget-product.php new file mode 100644 index 0000000..a64ff1e --- /dev/null +++ b/templates/content-widget-product.php @@ -0,0 +1,43 @@ + +
  • + + + + get_image(); // PHPCS:Ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + get_name() ); ?> + + + + get_average_rating() ); // PHPCS:Ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + + + get_price_html(); // PHPCS:Ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> + + +
  • diff --git a/templates/content-widget-reviews.php b/templates/content-widget-reviews.php new file mode 100644 index 0000000..d163684 --- /dev/null +++ b/templates/content-widget-reviews.php @@ -0,0 +1,47 @@ + +
  • + + + + + + get_image(); ?> + get_name() ); ?> + + + comment_ID, 'rating', true ) ) ); ?> + + + comment_ID ) ); + ?> + + + + + +
  • diff --git a/templates/emails/admin-cancelled-order.php b/templates/emails/admin-cancelled-order.php new file mode 100644 index 0000000..af11740 --- /dev/null +++ b/templates/emails/admin-cancelled-order.php @@ -0,0 +1,60 @@ + + + +

    get_order_number() ), esc_html( $order->get_formatted_billing_full_name() ) ); ?>

    + + + + +

    get_order_number() ), esc_html( $order->get_formatted_billing_full_name() ) ); ?>

    + + + + +

    get_formatted_billing_full_name() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

    + + + +

    get_billing_first_name() ) ); ?>

    +

    + + + +

    get_billing_first_name() ) ); ?>

    + +needs_payment() ) { ?> +

    + array( + 'href' => array(), + ), + ) + ), + esc_html( get_bloginfo( 'name', 'display' ) ), + '' . esc_html__( 'Pay for this order', 'woocommerce' ) . '' + ); + ?> +

    + + +

    + get_date_created() ) ) ); + ?> +

    + + + +

    + +

    ' . esc_html( $user_login ) . '', make_clickable( esc_url( wc_get_page_permalink( 'myaccount' ) ) ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

    + + +

    ' . esc_html( $user_pass ) . '' ); ?>

    + + + + + +

    get_billing_first_name() ) ); ?>

    +

    + +
    + +

    + + + + +

    get_billing_first_name() ) ); ?>

    +

    + + + + +

    get_billing_first_name() ) ); ?>

    + +

    get_order_number() ) ); ?>

    + + + + +

    get_billing_first_name() ) ); ?>

    + +

    + +

    + + + + + +

    + +

    + +

    +

    +

    + + + +

    + +get_formatted_billing_address(); +$shipping = $order->get_formatted_shipping_address(); + +?> + + + needs_shipping_address() && $shipping ) : ?> + + + +
    +

    + +
    + + get_billing_phone() ) : ?> +
    get_billing_phone() ); ?> + + get_billing_email() ) : ?> +
    get_billing_email() ); ?> + +
    +
    +

    + +
    + + get_shipping_phone() ) : ?> +
    get_shipping_phone() ); ?> + +
    +
    diff --git a/templates/emails/email-customer-details.php b/templates/emails/email-customer-details.php new file mode 100644 index 0000000..70c42c2 --- /dev/null +++ b/templates/emails/email-customer-details.php @@ -0,0 +1,31 @@ + + +
    +

    +
      + +
    • :
    • + +
    +
    + diff --git a/templates/emails/email-downloads.php b/templates/emails/email-downloads.php new file mode 100644 index 0000000..6fce472 --- /dev/null +++ b/templates/emails/email-downloads.php @@ -0,0 +1,68 @@ +

    + + + + + $column_name ) : ?> + + + + + + + + $column_name ) : ?> + + + + +
    + + + + + + + +
    diff --git a/templates/emails/email-footer.php b/templates/emails/email-footer.php new file mode 100644 index 0000000..4579924 --- /dev/null +++ b/templates/emails/email-footer.php @@ -0,0 +1,56 @@ + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/emails/email-header.php b/templates/emails/email-header.php new file mode 100644 index 0000000..23793c0 --- /dev/null +++ b/templates/emails/email-header.php @@ -0,0 +1,65 @@ + + +> + + + <?php echo get_bloginfo( 'name', 'display' ); ?> + + ="0" marginwidth="0" topmargin="0" marginheight="0" offset="0"> +
    + + +
    +
    + ' . get_bloginfo( 'name', 'display' ) . '

    '; + } + ?> +
    + + + + + +
    + + + + + +
    +

    +
    + +
    + + + + + + + + + + + + + + + + + + + + diff --git a/templates/order/order-details.php b/templates/order/order-details.php new file mode 100644 index 0000000..d5b5966 --- /dev/null +++ b/templates/order/order-details.php @@ -0,0 +1,114 @@ +get_items( apply_filters( 'woocommerce_purchase_order_item_types', 'line_item' ) ); +$show_purchase_note = $order->has_status( apply_filters( 'woocommerce_purchase_note_order_statuses', array( 'completed', 'processing' ) ) ); +$show_customer_details = is_user_logged_in() && $order->get_user_id() === get_current_user_id(); +$downloads = $order->get_downloadable_items(); +$show_downloads = $order->has_downloadable_item() && $order->is_download_permitted(); + +if ( $show_downloads ) { + wc_get_template( + 'order/order-downloads.php', + array( + 'downloads' => $downloads, + 'show_title' => true, + ) + ); +} +?> +
    + + +

    + +
    + + + + + + + + + + + + + + + diff --git a/templates/emails/email-styles.php b/templates/emails/email-styles.php new file mode 100644 index 0000000..89c6aa4 --- /dev/null +++ b/templates/emails/email-styles.php @@ -0,0 +1,229 @@ + +body { + padding: 0; +} + +#wrapper { + background-color: ; + margin: 0; + padding: 70px 0; + -webkit-text-size-adjust: none !important; + width: 100%; +} + +#template_container { + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1) !important; + background-color: ; + border: 1px solid ; + border-radius: 3px !important; +} + +#template_header { + background-color: ; + border-radius: 3px 3px 0 0 !important; + color: ; + border-bottom: 0; + font-weight: bold; + line-height: 100%; + vertical-align: middle; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; +} + +#template_header h1, +#template_header h1 a { + color: ; + background-color: inherit; +} + +#template_header_image img { + margin-left: 0; + margin-right: 0; +} + +#template_footer td { + padding: 0; + border-radius: 6px; +} + +#template_footer #credit { + border: 0; + color: ; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-size: 12px; + line-height: 150%; + text-align: center; + padding: 24px 0; +} + +#template_footer #credit p { + margin: 0 0 16px; +} + +#body_content { + background-color: ; +} + +#body_content table td { + padding: 48px 48px 32px; +} + +#body_content table td td { + padding: 12px; +} + +#body_content table td th { + padding: 12px; +} + +#body_content td ul.wc-item-meta { + font-size: small; + margin: 1em 0 0; + padding: 0; + list-style: none; +} + +#body_content td ul.wc-item-meta li { + margin: 0.5em 0 0; + padding: 0; +} + +#body_content td ul.wc-item-meta li p { + margin: 0; +} + +#body_content p { + margin: 0 0 16px; +} + +#body_content_inner { + color: ; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-size: 14px; + line-height: 150%; + text-align: ; +} + +.td { + color: ; + border: 1px solid ; + vertical-align: middle; +} + +.address { + padding: 12px; + color: ; + border: 1px solid ; +} + +.text { + color: ; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; +} + +.link { + color: ; +} + +#header_wrapper { + padding: 36px 48px; + display: block; +} + +h1 { + color: ; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-size: 30px; + font-weight: 300; + line-height: 150%; + margin: 0; + text-align: ; + text-shadow: 0 1px 0 ; +} + +h2 { + color: ; + display: block; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-size: 18px; + font-weight: bold; + line-height: 130%; + margin: 0 0 18px; + text-align: ; +} + +h3 { + color: ; + display: block; + font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-size: 16px; + font-weight: bold; + line-height: 130%; + margin: 16px 0 8px; + text-align: ; +} + +a { + color: ; + font-weight: normal; + text-decoration: underline; +} + +img { + border: none; + display: inline-block; + font-size: 14px; + font-weight: bold; + height: auto; + outline: none; + text-decoration: none; + text-transform: capitalize; + vertical-align: middle; + margin-: 10px; + max-width: 100%; + height: auto; +} +get_order_number() ), esc_html( $order->get_formatted_billing_full_name() ) ) . "\n\n"; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/admin-failed-order.php b/templates/emails/plain/admin-failed-order.php new file mode 100644 index 0000000..90148e6 --- /dev/null +++ b/templates/emails/plain/admin-failed-order.php @@ -0,0 +1,58 @@ +get_order_number() ), esc_html( $order->get_formatted_billing_full_name() ) ) . "\n\n"; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/admin-new-order.php b/templates/emails/plain/admin-new-order.php new file mode 100644 index 0000000..97931f6 --- /dev/null +++ b/templates/emails/plain/admin-new-order.php @@ -0,0 +1,58 @@ +get_formatted_billing_full_name() ) ) . "\n\n"; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-completed-order.php b/templates/emails/plain/customer-completed-order.php new file mode 100644 index 0000000..56f0071 --- /dev/null +++ b/templates/emails/plain/customer-completed-order.php @@ -0,0 +1,60 @@ +get_billing_first_name() ) ) . "\n\n"; +/* translators: %s: Site title */ +echo esc_html__( 'We have finished processing your order.', 'woocommerce' ) . "\n\n"; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-invoice.php b/templates/emails/plain/customer-invoice.php new file mode 100644 index 0000000..a70e610 --- /dev/null +++ b/templates/emails/plain/customer-invoice.php @@ -0,0 +1,79 @@ +get_billing_first_name() ) ) . "\n\n"; + +if ( $order->has_status( 'pending' ) ) { + echo wp_kses_post( + sprintf( + /* translators: %1$s: Site title, %2$s: Order pay link */ + __( 'An order has been created for you on %1$s. Your invoice is below, with a link to make payment when you’re ready: %2$s', 'woocommerce' ), + esc_html( get_bloginfo( 'name', 'display' ) ), + esc_url( $order->get_checkout_payment_url() ) + ) + ) . "\n\n"; + +} else { + /* translators: %s: Order date */ + echo sprintf( esc_html__( 'Here are the details of your order placed on %s:', 'woocommerce' ), esc_html( wc_format_datetime( $order->get_date_created() ) ) ) . "\n\n"; +} + +/** + * Hook for the woocommerce_email_order_details. + * + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/** + * Hook for the woocommerce_email_order_meta. + * + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/** + * Hook for woocommerce_email_customer_details + * + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-new-account.php b/templates/emails/plain/customer-new-account.php new file mode 100644 index 0000000..56204d2 --- /dev/null +++ b/templates/emails/plain/customer-new-account.php @@ -0,0 +1,44 @@ +get_billing_first_name() ) ) . "\n\n"; +echo esc_html__( 'The following note has been added to your order:', 'woocommerce' ) . "\n\n"; + +echo "----------\n\n"; + +echo wptexturize( $customer_note ) . "\n\n"; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped + +echo "----------\n\n"; + +echo esc_html__( 'As a reminder, here are your order details:', 'woocommerce' ) . "\n\n"; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-on-hold-order.php b/templates/emails/plain/customer-on-hold-order.php new file mode 100644 index 0000000..b5dec0a --- /dev/null +++ b/templates/emails/plain/customer-on-hold-order.php @@ -0,0 +1,59 @@ +get_billing_first_name() ) ) . "\n\n"; +echo esc_html__( 'Thanks for your order. It’s on-hold until we confirm that payment has been received. In the meantime, here’s a reminder of what you ordered:', 'woocommerce' ) . "\n\n"; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-processing-order.php b/templates/emails/plain/customer-processing-order.php new file mode 100644 index 0000000..fead3d7 --- /dev/null +++ b/templates/emails/plain/customer-processing-order.php @@ -0,0 +1,60 @@ +get_billing_first_name() ) ) . "\n\n"; +/* translators: %s: Order number */ +echo sprintf( esc_html__( 'Just to let you know — we\'ve received your order #%s, and it is now being processed:', 'woocommerce' ), esc_html( $order->get_order_number() ) ) . "\n\n"; + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-refunded-order.php b/templates/emails/plain/customer-refunded-order.php new file mode 100644 index 0000000..a6626d3 --- /dev/null +++ b/templates/emails/plain/customer-refunded-order.php @@ -0,0 +1,65 @@ +get_billing_first_name() ) . "\n\n"; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped +if ( $partial_refund ) { + /* translators: %s: Site title */ + echo sprintf( esc_html__( 'Your order on %s has been partially refunded. There are more details below for your reference:', 'woocommerce' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) . "\n\n"; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped +} else { + /* translators: %s: Site title */ + echo sprintf( esc_html__( 'Your order on %s has been refunded. There are more details below for your reference:', 'woocommerce' ), wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES ) ) . "\n\n"; // phpcs:ignore WordPress.XSS.EscapeOutput.OutputNotEscaped +} + +/* + * @hooked WC_Emails::order_details() Shows the order details table. + * @hooked WC_Structured_Data::generate_order_data() Generates structured data. + * @hooked WC_Structured_Data::output_structured_data() Outputs structured data. + * @since 2.5.0 + */ +do_action( 'woocommerce_email_order_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n----------------------------------------\n\n"; + +/* + * @hooked WC_Emails::order_meta() Shows order meta data. + */ +do_action( 'woocommerce_email_order_meta', $order, $sent_to_admin, $plain_text, $email ); + +/* + * @hooked WC_Emails::customer_details() Shows customer details + * @hooked WC_Emails::email_address() Shows email address + */ +do_action( 'woocommerce_email_customer_details', $order, $sent_to_admin, $plain_text, $email ); + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/customer-reset-password.php b/templates/emails/plain/customer-reset-password.php new file mode 100644 index 0000000..efc4e5b --- /dev/null +++ b/templates/emails/plain/customer-reset-password.php @@ -0,0 +1,43 @@ + $reset_key, 'id' => $user_id ), wc_get_endpoint_url( 'lost-password', '', wc_get_page_permalink( 'myaccount' ) ) ) ) . "\n\n"; // phpcs:ignore + +echo "\n\n----------------------------------------\n\n"; + +/** + * Show user-defined additional content - this is set in each email's settings. + */ +if ( $additional_content ) { + echo esc_html( wp_strip_all_tags( wptexturize( $additional_content ) ) ); + echo "\n\n----------------------------------------\n\n"; +} + +echo wp_kses_post( apply_filters( 'woocommerce_email_footer_text', get_option( 'woocommerce_email_footer_text' ) ) ); diff --git a/templates/emails/plain/email-addresses.php b/templates/emails/plain/email-addresses.php new file mode 100644 index 0000000..c9eef44 --- /dev/null +++ b/templates/emails/plain/email-addresses.php @@ -0,0 +1,42 @@ +#i', "\n", $order->get_formatted_billing_address() ) . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + +if ( $order->get_billing_phone() ) { + echo $order->get_billing_phone() . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +if ( $order->get_billing_email() ) { + echo $order->get_billing_email() . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +} + +if ( ! wc_ship_to_billing_address_only() && $order->needs_shipping_address() ) { + $shipping = $order->get_formatted_shipping_address(); + + if ( $shipping ) { + echo "\n" . esc_html( wc_strtoupper( esc_html__( 'Shipping address', 'woocommerce' ) ) ) . "\n\n"; + echo preg_replace( '##i', "\n", $shipping ) . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + if ( $order->get_shipping_phone() ) { + echo $order->get_shipping_phone() . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } +} diff --git a/templates/emails/plain/email-customer-details.php b/templates/emails/plain/email-customer-details.php new file mode 100644 index 0000000..e86c24a --- /dev/null +++ b/templates/emails/plain/email-customer-details.php @@ -0,0 +1,26 @@ + $column_name ) { + echo wp_kses_post( $column_name ) . ': '; + + if ( has_action( 'woocommerce_email_downloads_column_' . $column_id ) ) { + do_action( 'woocommerce_email_downloads_column_' . $column_id, $download, $plain_text ); + } else { + switch ( $column_id ) { + case 'download-product': + echo esc_html( $download['product_name'] ); + break; + case 'download-file': + echo esc_html( $download['download_name'] ) . ' - ' . esc_url( $download['download_url'] ); + break; + case 'download-expires': + if ( ! empty( $download['access_expires'] ) ) { + echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $download['access_expires'] ) ) ); + } else { + esc_html_e( 'Never', 'woocommerce' ); + } + break; + } + } + echo "\n"; + } + echo "\n"; +} +echo '=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-='; +echo "\n\n"; diff --git a/templates/emails/plain/email-order-details.php b/templates/emails/plain/email-order-details.php new file mode 100644 index 0000000..3a292aa --- /dev/null +++ b/templates/emails/plain/email-order-details.php @@ -0,0 +1,54 @@ +get_order_number(), wc_format_datetime( $order->get_date_created() ) ) ) ) . "\n"; +echo "\n" . wc_get_email_order_items( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + $order, + array( + 'show_sku' => $sent_to_admin, + 'show_image' => false, + 'image_size' => array( 32, 32 ), + 'plain_text' => true, + 'sent_to_admin' => $sent_to_admin, + ) +); + +echo "==========\n\n"; + +$item_totals = $order->get_order_item_totals(); + +if ( $item_totals ) { + foreach ( $item_totals as $total ) { + echo wp_kses_post( $total['label'] . "\t " . $total['value'] ) . "\n"; + } +} + +if ( $order->get_customer_note() ) { + echo esc_html__( 'Note:', 'woocommerce' ) . "\t " . wp_kses_post( wptexturize( $order->get_customer_note() ) ) . "\n"; +} + +if ( $sent_to_admin ) { + /* translators: %s: Order link. */ + echo "\n" . sprintf( esc_html__( 'View order: %s', 'woocommerce' ), esc_url( $order->get_edit_order_url() ) ) . "\n"; +} + +do_action( 'woocommerce_email_after_order_table', $order, $sent_to_admin, $plain_text, $email ); diff --git a/templates/emails/plain/email-order-items.php b/templates/emails/plain/email-order-items.php new file mode 100644 index 0000000..61cf3fd --- /dev/null +++ b/templates/emails/plain/email-order-items.php @@ -0,0 +1,66 @@ + $item ) : + if ( apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { + $product = $item->get_product(); + $sku = ''; + $purchase_note = ''; + + if ( is_object( $product ) ) { + $sku = $product->get_sku(); + $purchase_note = $product->get_purchase_note(); + } + + // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped + echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, false ) ); + if ( $show_sku && $sku ) { + echo ' (#' . $sku . ')'; + } + echo ' X ' . apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ); + echo ' = ' . $order->get_formatted_line_subtotal( $item ) . "\n"; + // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped + + // allow other plugins to add additional product information here. + do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $order, $plain_text ); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo strip_tags( + wc_display_item_meta( + $item, + array( + 'before' => "\n- ", + 'separator' => "\n- ", + 'after' => '', + 'echo' => false, + 'autop' => false, + ) + ) + ); + + // allow other plugins to add additional product information here. + do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $order, $plain_text ); + } + // Note. + if ( $show_purchase_note && $purchase_note ) { + echo "\n" . do_shortcode( wp_kses_post( $purchase_note ) ); + } + echo "\n\n"; +endforeach; diff --git a/templates/global/breadcrumb.php b/templates/global/breadcrumb.php new file mode 100644 index 0000000..3d6bc21 --- /dev/null +++ b/templates/global/breadcrumb.php @@ -0,0 +1,46 @@ + $crumb ) { + + echo $before; + + if ( ! empty( $crumb[1] ) && sizeof( $breadcrumb ) !== $key + 1 ) { + echo '' . esc_html( $crumb[0] ) . ''; + } else { + echo esc_html( $crumb[0] ); + } + + echo $after; + + if ( sizeof( $breadcrumb ) !== $key + 1 ) { + echo $delimiter; + } + } + + echo $wrap_after; + +} diff --git a/templates/global/form-login.php b/templates/global/form-login.php new file mode 100644 index 0000000..1c64a44 --- /dev/null +++ b/templates/global/form-login.php @@ -0,0 +1,61 @@ + +> + + + + + +

    + + +

    +

    + + +

    +
    + + + +

    + + + + +

    +

    + +

    + +
    + + + + diff --git a/templates/global/quantity-input.php b/templates/global/quantity-input.php new file mode 100644 index 0000000..172fc85 --- /dev/null +++ b/templates/global/quantity-input.php @@ -0,0 +1,49 @@ + + + +
    + + + + +
    + '; + break; + case 'twentyeleven': + echo ''; + get_sidebar( 'shop' ); + echo ''; + break; + case 'twentytwelve': + echo ''; + break; + case 'twentythirteen': + echo ''; + break; + case 'twentyfourteen': + echo ''; + get_sidebar( 'content' ); + break; + case 'twentyfifteen': + echo ''; + break; + case 'twentysixteen': + echo ''; + break; + default: + echo ''; + break; +} diff --git a/templates/global/wrapper-start.php b/templates/global/wrapper-start.php new file mode 100644 index 0000000..aceb891 --- /dev/null +++ b/templates/global/wrapper-start.php @@ -0,0 +1,49 @@ +
    '; + break; + case 'twentyeleven': + echo '
    '; + break; + case 'twentytwelve': + echo '
    '; + break; + case 'twentythirteen': + echo '
    '; + break; + case 'twentyfourteen': + echo '
    '; + break; + case 'twentyfifteen': + echo '
    '; + break; + case 'twentysixteen': + echo '
    '; + break; + default: + echo '
    '; + break; +} diff --git a/templates/loop/add-to-cart.php b/templates/loop/add-to-cart.php new file mode 100644 index 0000000..9e4dda0 --- /dev/null +++ b/templates/loop/add-to-cart.php @@ -0,0 +1,36 @@ +%s', + esc_url( $product->add_to_cart_url() ), + esc_attr( isset( $args['quantity'] ) ? $args['quantity'] : 1 ), + esc_attr( isset( $args['class'] ) ? $args['class'] : 'button' ), + isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '', + esc_html( $product->add_to_cart_text() ) + ), + $product, + $args +); diff --git a/templates/loop/loop-end.php b/templates/loop/loop-end.php new file mode 100644 index 0000000..802acb9 --- /dev/null +++ b/templates/loop/loop-end.php @@ -0,0 +1,22 @@ + + diff --git a/templates/loop/loop-start.php b/templates/loop/loop-start.php new file mode 100644 index 0000000..b2d4960 --- /dev/null +++ b/templates/loop/loop-start.php @@ -0,0 +1,22 @@ + +
      diff --git a/templates/loop/no-products-found.php b/templates/loop/no-products-found.php new file mode 100644 index 0000000..906edb3 --- /dev/null +++ b/templates/loop/no-products-found.php @@ -0,0 +1,21 @@ + +

      diff --git a/templates/loop/orderby.php b/templates/loop/orderby.php new file mode 100644 index 0000000..5b2c1f8 --- /dev/null +++ b/templates/loop/orderby.php @@ -0,0 +1,31 @@ + +
      + + + + diff --git a/templates/loop/pagination.php b/templates/loop/pagination.php new file mode 100644 index 0000000..b524d19 --- /dev/null +++ b/templates/loop/pagination.php @@ -0,0 +1,51 @@ + + diff --git a/templates/loop/price.php b/templates/loop/price.php new file mode 100644 index 0000000..951ca17 --- /dev/null +++ b/templates/loop/price.php @@ -0,0 +1,27 @@ + + +get_price_html() ) : ?> + + diff --git a/templates/loop/rating.php b/templates/loop/rating.php new file mode 100644 index 0000000..9b29aca --- /dev/null +++ b/templates/loop/rating.php @@ -0,0 +1,28 @@ +get_average_rating() ); // WordPress.XSS.EscapeOutput.OutputNotEscaped. diff --git a/templates/loop/result-count.php b/templates/loop/result-count.php new file mode 100644 index 0000000..faec643 --- /dev/null +++ b/templates/loop/result-count.php @@ -0,0 +1,40 @@ + +

      + +

      diff --git a/templates/loop/sale-flash.php b/templates/loop/sale-flash.php new file mode 100644 index 0000000..2b35f76 --- /dev/null +++ b/templates/loop/sale-flash.php @@ -0,0 +1,32 @@ + +is_on_sale() ) : ?> + + ' . esc_html__( 'Sale!', 'woocommerce' ) . '', $post, $product ); ?> + + array( + 'href' => array(), + ), +); +?> + +

      + Log out)', 'woocommerce' ), $allowed_html ), + '' . esc_html( $current_user->display_name ) . '', + esc_url( wc_logout_url() ) + ); + ?> +

      + +

      + recent orders, manage your billing address, and edit your password and account details.', 'woocommerce' ); + if ( wc_shipping_enabled() ) { + /* translators: 1: Orders URL 2: Addresses URL 3: Account URL. */ + $dashboard_desc = __( 'From your account dashboard you can view your recent orders, manage your shipping and billing addresses, and edit your password and account details.', 'woocommerce' ); + } + printf( + wp_kses( $dashboard_desc, $allowed_html ), + esc_url( wc_get_endpoint_url( 'orders' ) ), + esc_url( wc_get_endpoint_url( 'edit-address' ) ), + esc_url( wc_get_endpoint_url( 'edit-account' ) ) + ); + ?> +

      + +customer->get_downloadable_products(); +$has_downloads = (bool) $downloads; + +do_action( 'woocommerce_before_account_downloads', $has_downloads ); ?> + + + + + + + + + + +
      + + + + +
      + + + diff --git a/templates/myaccount/form-add-payment-method.php b/templates/myaccount/form-add-payment-method.php new file mode 100644 index 0000000..71b859a --- /dev/null +++ b/templates/myaccount/form-add-payment-method.php @@ -0,0 +1,61 @@ +payment_gateways->get_available_payment_gateways(); + +if ( $available_gateways ) : ?> +
      +
      +
        + set_current(); + } + + foreach ( $available_gateways as $gateway ) { + ?> +
      • + chosen, true ); ?> /> + + has_fields() || $gateway->get_description() ) { + echo ''; + } + ?> +
      • + +
      + + + +
      + + + +
      +
      + + +

      + diff --git a/templates/myaccount/form-edit-account.php b/templates/myaccount/form-edit-account.php new file mode 100644 index 0000000..eb5dfd1 --- /dev/null +++ b/templates/myaccount/form-edit-account.php @@ -0,0 +1,76 @@ + + + > + + + +

      + + +

      +

      + + +

      +
      + +

      + + +

      +
      + +

      + + +

      + +
      + + +

      + + +

      +

      + + +

      +

      + + +

      +
      +
      + + + +

      + + + +

      + + + + + diff --git a/templates/myaccount/form-edit-address.php b/templates/myaccount/form-edit-address.php new file mode 100644 index 0000000..6916bef --- /dev/null +++ b/templates/myaccount/form-edit-address.php @@ -0,0 +1,56 @@ + + + + + + +
      + +

      + +
      + + +
      + $field ) { + woocommerce_form_field( $key, $field, wc_get_post_data_by_key( $key, $field['value'] ) ); + } + ?> +
      + + + +

      + + + +

      +
      + + + + + + diff --git a/templates/myaccount/form-login.php b/templates/myaccount/form-login.php new file mode 100644 index 0000000..6680179 --- /dev/null +++ b/templates/myaccount/form-login.php @@ -0,0 +1,119 @@ + + + + +
      + +
      + + + +

      + + + + + +

      + + +

      +

      + + +

      + + + +

      + + + +

      +

      + +

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

      + +
      > + + + + + +

      + + +

      + + + +

      + + +

      + + + +

      + + +

      + + + +

      + + + + + +

      + + +

      + + + + + +
      + +
      + + + diff --git a/templates/myaccount/form-lost-password.php b/templates/myaccount/form-lost-password.php new file mode 100644 index 0000000..15fc984 --- /dev/null +++ b/templates/myaccount/form-lost-password.php @@ -0,0 +1,45 @@ + + +
      + +

      + +

      + + +

      + +
      + + + +

      + + +

      + + + + + + +
      + +

      + +

      + + +

      +

      + + +

      + + + + +
      + + + +

      + + +

      + + + + + + + + +

      + + diff --git a/templates/myaccount/my-account.php b/templates/myaccount/my-account.php new file mode 100644 index 0000000..9a4f85f --- /dev/null +++ b/templates/myaccount/my-account.php @@ -0,0 +1,36 @@ + + +
      + +
      diff --git a/templates/myaccount/my-address.php b/templates/myaccount/my-address.php new file mode 100644 index 0000000..5cd3161 --- /dev/null +++ b/templates/myaccount/my-address.php @@ -0,0 +1,77 @@ + __( 'Billing address', 'woocommerce' ), + 'shipping' => __( 'Shipping address', 'woocommerce' ), + ), + $customer_id + ); +} else { + $get_addresses = apply_filters( + 'woocommerce_my_account_get_addresses', + array( + 'billing' => __( 'Billing address', 'woocommerce' ), + ), + $customer_id + ); +} + +$oldcol = 1; +$col = 1; +?> + +

      + +

      + + +
      + + + $address_title ) : ?> + + +
      +
      +

      + +
      +
      + +
      +
      + + + + +
      + customer->get_downloadable_products(); + +if ( $downloads ) : ?> + + + +

      + +
        + +
      • + ' . sprintf( _n( '%s download remaining', '%s downloads remaining', $download['downloads_remaining'], 'woocommerce' ), $download['downloads_remaining'] ) . ' ', $download ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + echo apply_filters( 'woocommerce_available_download_link', '' . $download['download_name'] . '', $download ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + do_action( 'woocommerce_available_download_end', $download ); + ?> +
      • + +
      + + + + diff --git a/templates/myaccount/my-orders.php b/templates/myaccount/my-orders.php new file mode 100644 index 0000000..77761e0 --- /dev/null +++ b/templates/myaccount/my-orders.php @@ -0,0 +1,95 @@ + esc_html__( 'Order', 'woocommerce' ), + 'order-date' => esc_html__( 'Date', 'woocommerce' ), + 'order-status' => esc_html__( 'Status', 'woocommerce' ), + 'order-total' => esc_html__( 'Total', 'woocommerce' ), + 'order-actions' => ' ', + ) +); + +$customer_orders = get_posts( + apply_filters( + 'woocommerce_my_account_my_orders_query', + array( + 'numberposts' => $order_count, + 'meta_key' => '_customer_user', + 'meta_value' => get_current_user_id(), + 'post_type' => wc_get_order_types( 'view-orders' ), + 'post_status' => array_keys( wc_get_order_statuses() ), + ) + ) +); + +if ( $customer_orders ) : ?> + +

      + +
    +
    diff --git a/templates/emails/email-order-details.php b/templates/emails/email-order-details.php new file mode 100644 index 0000000..2184d69 --- /dev/null +++ b/templates/emails/email-order-details.php @@ -0,0 +1,90 @@ + + +

    + get_edit_order_url() ) . '">'; + $after = ''; + } else { + $before = ''; + $after = ''; + } + /* translators: %s: Order ID. */ + echo wp_kses_post( $before . sprintf( __( '[Order #%s]', 'woocommerce' ) . $after . ' ()', $order->get_order_number(), $order->get_date_created()->format( 'c' ), wc_format_datetime( $order->get_date_created() ) ) ); + ?> +

    + +
    + + + + + + + + + + $sent_to_admin, + 'show_image' => false, + 'image_size' => array( 32, 32 ), + 'plain_text' => $plain_text, + 'sent_to_admin' => $sent_to_admin, + ) + ); + ?> + + + get_order_item_totals(); + + if ( $item_totals ) { + $i = 0; + foreach ( $item_totals as $total ) { + $i++; + ?> + + + + + get_customer_note() ) { + ?> + + + + + + +
    get_customer_note() ) ) ); ?>
    +
    + + diff --git a/templates/emails/email-order-items.php b/templates/emails/email-order-items.php new file mode 100644 index 0000000..74bc9c1 --- /dev/null +++ b/templates/emails/email-order-items.php @@ -0,0 +1,104 @@ + $item ) : + $product = $item->get_product(); + $sku = ''; + $purchase_note = ''; + $image = ''; + + if ( ! apply_filters( 'woocommerce_order_item_visible', true, $item ) ) { + continue; + } + + if ( is_object( $product ) ) { + $sku = $product->get_sku(); + $purchase_note = $product->get_purchase_note(); + $image = $product->get_image( $image_size ); + } + + ?> +
    + get_name(), $item, false ) ); + + // SKU. + if ( $show_sku && $sku ) { + echo wp_kses_post( ' (#' . $sku . ')' ); + } + + // allow other plugins to add additional product information here. + do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $order, $plain_text ); + + wc_display_item_meta( + $item, + array( + 'label_before' => '', + ) + ); + + // allow other plugins to add additional product information here. + do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $order, $plain_text ); + + ?> + + get_quantity(); + $refunded_qty = $order->get_qty_refunded_for_item( $item_id ); + + if ( $refunded_qty ) { + $qty_display = '' . esc_html( $qty ) . ' ' . esc_html( $qty - ( $refunded_qty * -1 ) ) . ''; + } else { + $qty_display = esc_html( $qty ); + } + echo wp_kses_post( apply_filters( 'woocommerce_email_order_item_quantity', $qty_display, $item ) ); + ?> + + get_formatted_line_subtotal( $item ) ); ?> +
    + +
    + + + + $column_name ) : ?> + + + + + + + get_item_count(); + ?> + + $column_name ) : ?> + + + + + + + diff --git a/templates/myaccount/navigation.php b/templates/myaccount/navigation.php new file mode 100644 index 0000000..8fc0db0 --- /dev/null +++ b/templates/myaccount/navigation.php @@ -0,0 +1,35 @@ + + + + + diff --git a/templates/myaccount/orders.php b/templates/myaccount/orders.php new file mode 100644 index 0000000..10fcadc --- /dev/null +++ b/templates/myaccount/orders.php @@ -0,0 +1,105 @@ + + + + + + + + $column_name ) : ?> + + + + + + + orders as $customer_order ) { + $order = wc_get_order( $customer_order ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $item_count = $order->get_item_count() - $order->get_item_count_refunded(); + ?> + + $column_name ) : ?> + + + + + + + + + + max_num_pages ) : ?> +
    + + + + + max_num_pages ) !== $current_page ) : ?> + + +
    + + + +
    + + +
    + + + diff --git a/templates/myaccount/payment-methods.php b/templates/myaccount/payment-methods.php new file mode 100644 index 0000000..8c6ad65 --- /dev/null +++ b/templates/myaccount/payment-methods.php @@ -0,0 +1,78 @@ + + + + + + + + $column_name ) : ?> + + + + + $methods ) : // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited ?> + + + $column_name ) : ?> + + + + + + + + + +

    + + + + + +payment_gateways->get_available_payment_gateways() ) : ?> + + diff --git a/templates/myaccount/view-order.php b/templates/myaccount/view-order.php new file mode 100644 index 0000000..2691e3d --- /dev/null +++ b/templates/myaccount/view-order.php @@ -0,0 +1,56 @@ +get_customer_order_notes(); +?> +

    +' . $order->get_order_number() . '', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + '' . wc_format_datetime( $order->get_date_created() ) . '', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + '' . wc_get_order_status_name( $order->get_status() ) . '' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +); +?> +

    + + +

    +
      + +
    1. +
      +
      +

      comment_date ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

      +
      + comment_content ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
      +
      +
      +
      +
      +
    2. + +
    + + + diff --git a/templates/notices/error.php b/templates/notices/error.php new file mode 100644 index 0000000..fe4af62 --- /dev/null +++ b/templates/notices/error.php @@ -0,0 +1,33 @@ + + diff --git a/templates/notices/notice.php b/templates/notices/notice.php new file mode 100644 index 0000000..4e611df --- /dev/null +++ b/templates/notices/notice.php @@ -0,0 +1,32 @@ + + + +
    > + +
    + diff --git a/templates/notices/success.php b/templates/notices/success.php new file mode 100644 index 0000000..e8643e5 --- /dev/null +++ b/templates/notices/success.php @@ -0,0 +1,32 @@ + + + +
    role="alert"> + +
    + diff --git a/templates/order/form-tracking.php b/templates/order/form-tracking.php new file mode 100644 index 0000000..d26f1a0 --- /dev/null +++ b/templates/order/form-tracking.php @@ -0,0 +1,34 @@ + + +
    + +

    + +

    +

    +
    + +

    + + +
    diff --git a/templates/order/order-again.php b/templates/order/order-again.php new file mode 100644 index 0000000..f7bc224 --- /dev/null +++ b/templates/order/order-again.php @@ -0,0 +1,23 @@ + + +

    + +

    diff --git a/templates/order/order-details-customer.php b/templates/order/order-details-customer.php new file mode 100644 index 0000000..6992020 --- /dev/null +++ b/templates/order/order-details-customer.php @@ -0,0 +1,66 @@ +needs_shipping_address(); +?> +
    + + + +
    +
    + + + +

    + +
    + get_formatted_billing_address( esc_html__( 'N/A', 'woocommerce' ) ) ); ?> + + get_billing_phone() ) : ?> +

    get_billing_phone() ); ?>

    + + + get_billing_email() ) : ?> + + +
    + + + +
    + +
    +

    +
    + get_formatted_shipping_address( esc_html__( 'N/A', 'woocommerce' ) ) ); ?> + + get_shipping_phone() ) : ?> +

    get_shipping_phone() ); ?>

    + +
    +
    + +
    + + + + + +
    diff --git a/templates/order/order-details-item.php b/templates/order/order-details-item.php new file mode 100644 index 0000000..271b94c --- /dev/null +++ b/templates/order/order-details-item.php @@ -0,0 +1,68 @@ + +
    + is_visible(); + $product_permalink = apply_filters( 'woocommerce_order_item_permalink', $is_visible ? $product->get_permalink( $item ) : '', $item, $order ); + + echo wp_kses_post( apply_filters( 'woocommerce_order_item_name', $product_permalink ? sprintf( '%s', $product_permalink, $item->get_name() ) : $item->get_name(), $item, $is_visible ) ); + + $qty = $item->get_quantity(); + $refunded_qty = $order->get_qty_refunded_for_item( $item_id ); + + if ( $refunded_qty ) { + $qty_display = '' . esc_html( $qty ) . ' ' . esc_html( $qty - ( $refunded_qty * -1 ) ) . ''; + } else { + $qty_display = esc_html( $qty ); + } + + echo apply_filters( 'woocommerce_order_item_quantity_html', ' ' . sprintf( '× %s', $qty_display ) . '', $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + do_action( 'woocommerce_order_item_meta_start', $item_id, $item, $order, false ); + + wc_display_item_meta( $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + do_action( 'woocommerce_order_item_meta_end', $item_id, $item, $order, false ); + ?> + + get_formatted_line_subtotal( $item ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
    + + + + + + + + + + $item ) { + $product = $item->get_product(); + + wc_get_template( + 'order/order-details-item.php', + array( + 'order' => $order, + 'item_id' => $item_id, + 'item' => $item, + 'show_purchase_note' => $show_purchase_note, + 'purchase_note' => $product ? $product->get_purchase_note() : '', + 'product' => $product, + ) + ); + } + + do_action( 'woocommerce_order_details_after_order_table_items', $order ); + ?> + + + + get_order_item_totals() as $key => $total ) { + ?> + + + + + + get_customer_note() ) : ?> + + + + + + +
    get_customer_note() ) ) ); ?>
    + + + + + $order ) ); +} diff --git a/templates/order/order-downloads.php b/templates/order/order-downloads.php new file mode 100644 index 0000000..9566cd3 --- /dev/null +++ b/templates/order/order-downloads.php @@ -0,0 +1,73 @@ + +
    + +

    + + + + + + $column_name ) : ?> + + + + + + + + $column_name ) : ?> + + + + +
    + ' . esc_html( $download['product_name'] ) . ''; + } else { + echo esc_html( $download['product_name'] ); + } + break; + case 'download-file': + echo '' . esc_html( $download['download_name'] ) . ''; + break; + case 'download-remaining': + echo is_numeric( $download['downloads_remaining'] ) ? esc_html( $download['downloads_remaining'] ) : esc_html__( '∞', 'woocommerce' ); + break; + case 'download-expires': + if ( ! empty( $download['access_expires'] ) ) { + echo ''; + } else { + esc_html_e( 'Never', 'woocommerce' ); + } + break; + } + } + ?> +
    +
    diff --git a/templates/order/tracking.php b/templates/order/tracking.php new file mode 100644 index 0000000..7e9d554 --- /dev/null +++ b/templates/order/tracking.php @@ -0,0 +1,60 @@ +get_customer_order_notes(); +?> + +

    + ' . $order->get_order_number() . '', + '' . wc_format_datetime( $order->get_date_created() ) . '', + '' . wc_get_order_status_name( $order->get_status() ) . '' + ) + ) + ); + ?> +

    + + +

    +
      + +
    1. +
      +
      +

      comment_date ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>

      +
      + comment_content ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
      +
      +
      +
      +
      +
    2. + +
    + + +get_id() ); ?> diff --git a/templates/product-searchform.php b/templates/product-searchform.php new file mode 100644 index 0000000..c6c92bd --- /dev/null +++ b/templates/product-searchform.php @@ -0,0 +1,28 @@ + + diff --git a/templates/single-product-reviews.php b/templates/single-product-reviews.php new file mode 100644 index 0000000..4d6f12e --- /dev/null +++ b/templates/single-product-reviews.php @@ -0,0 +1,145 @@ + +
    +
    +

    + get_review_count(); + if ( $count && wc_review_ratings_enabled() ) { + /* translators: 1: reviews count 2: product name */ + $reviews_title = sprintf( esc_html( _n( '%1$s review for %2$s', '%1$s reviews for %2$s', $count, 'woocommerce' ) ), esc_html( $count ), '' . get_the_title() . '' ); + echo apply_filters( 'woocommerce_reviews_title', $reviews_title, $count, $product ); // WPCS: XSS ok. + } else { + esc_html_e( 'Reviews', 'woocommerce' ); + } + ?> +

    + + +
      + 'woocommerce_comments' ) ) ); ?> +
    + + 1 && get_option( 'page_comments' ) ) : + echo ''; + endif; + ?> + +

    + +
    + + get_id() ) ) : ?> +
    +
    + have_comments() ? esc_html__( 'Add a review', 'woocommerce' ) : sprintf( esc_html__( 'Be the first to review “%s”', 'woocommerce' ), get_the_title() ), + /* translators: %s is product title */ + 'title_reply_to' => esc_html__( 'Leave a Reply to %s', 'woocommerce' ), + 'title_reply_before' => '', + 'title_reply_after' => '', + 'comment_notes_after' => '', + 'label_submit' => esc_html__( 'Submit', 'woocommerce' ), + 'logged_in_as' => '', + 'comment_field' => '', + ); + + $name_email_required = (bool) get_option( 'require_name_email', 1 ); + $fields = array( + 'author' => array( + 'label' => __( 'Name', 'woocommerce' ), + 'type' => 'text', + 'value' => $commenter['comment_author'], + 'required' => $name_email_required, + ), + 'email' => array( + 'label' => __( 'Email', 'woocommerce' ), + 'type' => 'email', + 'value' => $commenter['comment_author_email'], + 'required' => $name_email_required, + ), + ); + + $comment_form['fields'] = array(); + + foreach ( $fields as $key => $field ) { + $field_html = '

    '; + $field_html .= '

    '; + + $comment_form['fields'][ $key ] = $field_html; + } + + $account_page_url = wc_get_page_permalink( 'myaccount' ); + if ( $account_page_url ) { + /* translators: %s opening and closing link tags respectively */ + $comment_form['must_log_in'] = ''; + } + + if ( wc_review_ratings_enabled() ) { + $comment_form['comment_field'] = '
    '; + } + + $comment_form['comment_field'] .= '

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

    + + +
    +
    diff --git a/templates/single-product.php b/templates/single-product.php new file mode 100644 index 0000000..39eeb1d --- /dev/null +++ b/templates/single-product.php @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + +
    + + + + + + + +
    + + diff --git a/templates/single-product/add-to-cart/grouped.php b/templates/single-product/add-to-cart/grouped.php new file mode 100644 index 0000000..fb6fff7 --- /dev/null +++ b/templates/single-product/add-to-cart/grouped.php @@ -0,0 +1,126 @@ + + +
    + + + get_id() ); + $quantites_required = $quantites_required || ( $grouped_product_child->is_purchasable() && ! $grouped_product_child->has_options() ); + $post = $post_object; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + setup_postdata( $post ); + + if ( $grouped_product_child->is_in_stock() ) { + $show_add_to_cart_button = true; + } + + echo ''; + + // Output columns for each product. + foreach ( $grouped_product_columns as $column_id ) { + do_action( 'woocommerce_grouped_product_list_before_' . $column_id, $grouped_product_child ); + + switch ( $column_id ) { + case 'quantity': + ob_start(); + + if ( ! $grouped_product_child->is_purchasable() || $grouped_product_child->has_options() || ! $grouped_product_child->is_in_stock() ) { + woocommerce_template_loop_add_to_cart(); + } elseif ( $grouped_product_child->is_sold_individually() ) { + echo ''; + } else { + do_action( 'woocommerce_before_add_to_cart_quantity' ); + + woocommerce_quantity_input( + array( + 'input_name' => 'quantity[' . $grouped_product_child->get_id() . ']', + 'input_value' => isset( $_POST['quantity'][ $grouped_product_child->get_id() ] ) ? wc_stock_amount( wc_clean( wp_unslash( $_POST['quantity'][ $grouped_product_child->get_id() ] ) ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Missing + 'min_value' => apply_filters( 'woocommerce_quantity_input_min', 0, $grouped_product_child ), + 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $grouped_product_child->get_max_purchase_quantity(), $grouped_product_child ), + 'placeholder' => '0', + ) + ); + + do_action( 'woocommerce_after_add_to_cart_quantity' ); + } + + $value = ob_get_clean(); + break; + case 'label': + $value = ''; + break; + case 'price': + $value = $grouped_product_child->get_price_html() . wc_get_stock_html( $grouped_product_child ); + break; + default: + $value = ''; + break; + } + + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + + do_action( 'woocommerce_grouped_product_list_after_' . $column_id, $grouped_product_child ); + } + + echo ''; + } + $post = $previous_post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + setup_postdata( $post ); + + do_action( 'woocommerce_grouped_product_list_after', $grouped_product_columns, $quantites_required, $product ); + ?> + +
    ' . apply_filters( 'woocommerce_grouped_product_list_column_' . $column_id, $value, $grouped_product_child ) . '
    + + + + + + + + + + + + +
    + + diff --git a/templates/single-product/add-to-cart/simple.php b/templates/single-product/add-to-cart/simple.php new file mode 100644 index 0000000..494ece1 --- /dev/null +++ b/templates/single-product/add-to-cart/simple.php @@ -0,0 +1,56 @@ +is_purchasable() ) { + return; +} + +echo wc_get_stock_html( $product ); // WPCS: XSS ok. + +if ( $product->is_in_stock() ) : ?> + + + +
    + + + apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ), + 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ), + 'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // WPCS: CSRF ok, input var ok. + ) + ); + + do_action( 'woocommerce_after_add_to_cart_quantity' ); + ?> + + + + +
    + + + + diff --git a/templates/single-product/add-to-cart/variable.php b/templates/single-product/add-to-cart/variable.php new file mode 100644 index 0000000..4d14248 --- /dev/null +++ b/templates/single-product/add-to-cart/variable.php @@ -0,0 +1,84 @@ + + +
    + + + +

    + + + + $options ) : ?> + + + + + + +
    + $options, + 'attribute' => $attribute_name, + 'product' => $product, + ) + ); + echo end( $attribute_keys ) === $attribute_name ? wp_kses_post( apply_filters( 'woocommerce_reset_variations_link', '' . esc_html__( 'Clear', 'woocommerce' ) . '' ) ) : ''; + ?> +
    + +
    + +
    + + + +
    + + +
    + + + apply_filters( 'woocommerce_quantity_input_min', $product->get_min_purchase_quantity(), $product ), + 'max_value' => apply_filters( 'woocommerce_quantity_input_max', $product->get_max_purchase_quantity(), $product ), + 'input_value' => isset( $_POST['quantity'] ) ? wc_stock_amount( wp_unslash( $_POST['quantity'] ) ) : $product->get_min_purchase_quantity(), // WPCS: CSRF ok, input var ok. + ) + ); + + do_action( 'woocommerce_after_add_to_cart_quantity' ); + ?> + + + + + + + + +
    diff --git a/templates/single-product/add-to-cart/variation.php b/templates/single-product/add-to-cart/variation.php new file mode 100644 index 0000000..af3dde3 --- /dev/null +++ b/templates/single-product/add-to-cart/variation.php @@ -0,0 +1,23 @@ + + + diff --git a/templates/single-product/meta.php b/templates/single-product/meta.php new file mode 100644 index 0000000..68a7e0f --- /dev/null +++ b/templates/single-product/meta.php @@ -0,0 +1,40 @@ + +
    + + + + get_sku() || $product->is_type( 'variable' ) ) ) : ?> + + get_sku() ) ? $sku : esc_html__( 'N/A', 'woocommerce' ); ?> + + + + get_id(), ', ', '' . _n( 'Category:', 'Categories:', count( $product->get_category_ids() ), 'woocommerce' ) . ' ', '' ); ?> + + get_id(), ', ', '' . _n( 'Tag:', 'Tags:', count( $product->get_tag_ids() ), 'woocommerce' ) . ' ', '' ); ?> + + + +
    diff --git a/templates/single-product/photoswipe.php b/templates/single-product/photoswipe.php new file mode 100644 index 0000000..761b0b1 --- /dev/null +++ b/templates/single-product/photoswipe.php @@ -0,0 +1,56 @@ + + + diff --git a/templates/single-product/price.php b/templates/single-product/price.php new file mode 100644 index 0000000..38e6d4d --- /dev/null +++ b/templates/single-product/price.php @@ -0,0 +1,25 @@ + +

    get_price_html(); ?>

    diff --git a/templates/single-product/product-attributes.php b/templates/single-product/product-attributes.php new file mode 100644 index 0000000..86faf52 --- /dev/null +++ b/templates/single-product/product-attributes.php @@ -0,0 +1,33 @@ + + + $product_attribute ) : ?> + + + + + +
    diff --git a/templates/single-product/product-image.php b/templates/single-product/product-image.php new file mode 100644 index 0000000..d892b4b --- /dev/null +++ b/templates/single-product/product-image.php @@ -0,0 +1,55 @@ +get_image_id(); +$wrapper_classes = apply_filters( + 'woocommerce_single_product_image_gallery_classes', + array( + 'woocommerce-product-gallery', + 'woocommerce-product-gallery--' . ( $post_thumbnail_id ? 'with-images' : 'without-images' ), + 'woocommerce-product-gallery--columns-' . absint( $columns ), + 'images', + ) +); +?> +
    +
    '; + } + + echo apply_filters( 'woocommerce_single_product_image_thumbnail_html', $html, $post_thumbnail_id ); // phpcs:disable WordPress.XSS.EscapeOutput.OutputNotEscaped + + do_action( 'woocommerce_product_thumbnails' ); + ?> + + diff --git a/templates/single-product/product-thumbnails.php b/templates/single-product/product-thumbnails.php new file mode 100644 index 0000000..cec3f34 --- /dev/null +++ b/templates/single-product/product-thumbnails.php @@ -0,0 +1,33 @@ +get_gallery_image_ids(); + +if ( $attachment_ids && $product->get_image_id() ) { + foreach ( $attachment_ids as $attachment_id ) { + echo apply_filters( 'woocommerce_single_product_image_thumbnail_html', wc_get_gallery_image_html( $attachment_id ), $attachment_id ); // phpcs:disable WordPress.XSS.EscapeOutput.OutputNotEscaped + } +} diff --git a/templates/single-product/rating.php b/templates/single-product/rating.php new file mode 100644 index 0000000..a758f8b --- /dev/null +++ b/templates/single-product/rating.php @@ -0,0 +1,43 @@ +get_rating_count(); +$review_count = $product->get_review_count(); +$average = $product->get_average_rating(); + +if ( $rating_count > 0 ) : ?> + + + + diff --git a/templates/single-product/related.php b/templates/single-product/related.php new file mode 100644 index 0000000..a7d3a3e --- /dev/null +++ b/templates/single-product/related.php @@ -0,0 +1,54 @@ + + + + comment_ID ); + +if ( '0' === $comment->comment_approved ) { ?> + +

    + + + +

    + + + +

    + + (' . esc_attr__( 'verified owner', 'woocommerce' ) . ') '; + } + + ?> + +

    + + comment_ID, 'rating', true ) ); + +if ( $rating && wc_review_ratings_enabled() ) { + echo wc_get_rating_html( $rating ); // WPCS: XSS ok. +} diff --git a/templates/single-product/review.php b/templates/single-product/review.php new file mode 100644 index 0000000..cc22770 --- /dev/null +++ b/templates/single-product/review.php @@ -0,0 +1,67 @@ + +
  • id="li-comment-"> + +
    + + + +
    + + + +
    +
    diff --git a/templates/single-product/sale-flash.php b/templates/single-product/sale-flash.php new file mode 100644 index 0000000..d3ce965 --- /dev/null +++ b/templates/single-product/sale-flash.php @@ -0,0 +1,32 @@ + +is_on_sale() ) : ?> + + ' . esc_html__( 'Sale!', 'woocommerce' ) . '', $post, $product ); ?> + + post_excerpt ); + +if ( ! $short_description ) { + return; +} + +?> +
    + +
    diff --git a/templates/single-product/stock.php b/templates/single-product/stock.php new file mode 100644 index 0000000..d8b1477 --- /dev/null +++ b/templates/single-product/stock.php @@ -0,0 +1,23 @@ + +

    diff --git a/templates/single-product/tabs/additional-information.php b/templates/single-product/tabs/additional-information.php new file mode 100644 index 0000000..f857b0c --- /dev/null +++ b/templates/single-product/tabs/additional-information.php @@ -0,0 +1,30 @@ + + + +

    + + + diff --git a/templates/single-product/tabs/description.php b/templates/single-product/tabs/description.php new file mode 100644 index 0000000..e7018a7 --- /dev/null +++ b/templates/single-product/tabs/description.php @@ -0,0 +1,30 @@ + + + +

    + + + diff --git a/templates/single-product/tabs/tabs.php b/templates/single-product/tabs/tabs.php new file mode 100644 index 0000000..819943c --- /dev/null +++ b/templates/single-product/tabs/tabs.php @@ -0,0 +1,56 @@ + + +
    +
      + $product_tab ) : ?> + + +
    + $product_tab ) : ?> +
    + +
    + + + +
    + + diff --git a/templates/single-product/title.php b/templates/single-product/title.php new file mode 100644 index 0000000..6fd646f --- /dev/null +++ b/templates/single-product/title.php @@ -0,0 +1,22 @@ +', '' ); diff --git a/templates/single-product/up-sells.php b/templates/single-product/up-sells.php new file mode 100644 index 0000000..36e5ed4 --- /dev/null +++ b/templates/single-product/up-sells.php @@ -0,0 +1,54 @@ + + +
    + +

    + + + + + + + get_id() ); + + setup_postdata( $GLOBALS['post'] =& $post_object ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited, Squiz.PHP.DisallowMultipleAssignments.Found + + wc_get_template_part( 'content', 'product' ); + ?> + + + + + +
    + + get_var( "SHOW TABLES LIKE '{$wpdb->prefix}woocommerce_attribute_taxonomies';" ) ) { + $wc_attributes = array_filter( (array) $wpdb->get_col( "SELECT attribute_name FROM {$wpdb->prefix}woocommerce_attribute_taxonomies;" ) ); + } else { + $wc_attributes = array(); + } + + // Tables. + WC_Install::drop_tables(); + + // Delete options. + $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'woocommerce\_%';" ); + $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'widget\_woocommerce\_%';" ); + + // Delete usermeta. + $wpdb->query( "DELETE FROM $wpdb->usermeta WHERE meta_key LIKE 'woocommerce\_%';" ); + + // Delete posts + data. + $wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type IN ( 'product', 'product_variation', 'shop_coupon', 'shop_order', 'shop_order_refund' );" ); + $wpdb->query( "DELETE meta FROM {$wpdb->postmeta} meta LEFT JOIN {$wpdb->posts} posts ON posts.ID = meta.post_id WHERE posts.ID IS NULL;" ); + + $wpdb->query( "DELETE FROM {$wpdb->comments} WHERE comment_type IN ( 'order_note' );" ); + $wpdb->query( "DELETE meta FROM {$wpdb->commentmeta} meta LEFT JOIN {$wpdb->comments} comments ON comments.comment_ID = meta.comment_id WHERE comments.comment_ID IS NULL;" ); + + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_order_items" ); + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}woocommerce_order_itemmeta" ); + + // Delete terms if > WP 4.2 (term splitting was added in 4.2). + if ( version_compare( $wp_version, '4.2', '>=' ) ) { + // Delete term taxonomies. + foreach ( array( 'product_cat', 'product_tag', 'product_shipping_class', 'product_type' ) as $_taxonomy ) { + $wpdb->delete( + $wpdb->term_taxonomy, + array( + 'taxonomy' => $_taxonomy, + ) + ); + } + + // Delete term attributes. + foreach ( $wc_attributes as $_taxonomy ) { + $wpdb->delete( + $wpdb->term_taxonomy, + array( + 'taxonomy' => 'pa_' . $_taxonomy, + ) + ); + } + + // Delete orphan relationships. + $wpdb->query( "DELETE tr FROM {$wpdb->term_relationships} tr LEFT JOIN {$wpdb->posts} posts ON posts.ID = tr.object_id WHERE posts.ID IS NULL;" ); + + // Delete orphan terms. + $wpdb->query( "DELETE t FROM {$wpdb->terms} t LEFT JOIN {$wpdb->term_taxonomy} tt ON t.term_id = tt.term_id WHERE tt.term_id IS NULL;" ); + + // Delete orphan term meta. + if ( ! empty( $wpdb->termmeta ) ) { + $wpdb->query( "DELETE tm FROM {$wpdb->termmeta} tm LEFT JOIN {$wpdb->term_taxonomy} tt ON tm.term_id = tt.term_id WHERE tt.term_id IS NULL;" ); + } + } + + // Clear any cached data that has been removed. + wp_cache_flush(); +} diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..7a57fd5 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + '../autoload_packages.php', + ); + $ignoreList = array( + 'AutoloadGenerator.php', + 'AutoloadProcessor.php', + 'CustomAutoloaderPlugin.php', + 'ManifestGenerator.php', + 'AutoloadFileWriter.php', + ); + + // Copy all of the autoloader files. + $files = scandir( __DIR__ ); + foreach ( $files as $file ) { + // Only PHP files will be copied. + if ( substr( $file, -4 ) !== '.php' ) { + continue; + } + + if ( in_array( $file, $ignoreList, true ) ) { + continue; + } + + $newFile = isset( $renameList[ $file ] ) ? $renameList[ $file ] : $file; + $content = self::prepareAutoloaderFile( $file, $suffix ); + + $written = file_put_contents( $outDir . '/' . $newFile, $content ); + if ( $io ) { + if ( $written ) { + $io->writeError( " Generated: $newFile" ); + } else { + $io->writeError( " Error: $newFile" ); + } + } + } + } + + /** + * Prepares an autoloader file to be written to the destination. + * + * @param String $filename a file to prepare. + * @param String $suffix Unique suffix used in the namespace. + * + * @return string + */ + private static function prepareAutoloaderFile( $filename, $suffix ) { + $header = self::COMMENT; + $header .= PHP_EOL; + $header .= 'namespace Automattic\Jetpack\Autoloader\jp' . $suffix . ';'; + $header .= PHP_EOL . PHP_EOL; + + $sourceLoader = fopen( __DIR__ . '/' . $filename, 'r' ); + $file_contents = stream_get_contents( $sourceLoader ); + return str_replace( + '/* HEADER */', + $header, + $file_contents + ); + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/AutoloadGenerator.php b/vendor/automattic/jetpack-autoloader/src/AutoloadGenerator.php new file mode 100644 index 0000000..7ebdc4e --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/AutoloadGenerator.php @@ -0,0 +1,393 @@ +io = $io; + $this->filesystem = new Filesystem(); + } + + /** + * Dump the Jetpack autoloader files. + * + * @param Composer $composer The Composer object. + * @param Config $config Config object. + * @param InstalledRepositoryInterface $localRepo Installed Repository object. + * @param PackageInterface $mainPackage Main Package object. + * @param InstallationManager $installationManager Manager for installing packages. + * @param string $targetDir Path to the current target directory. + * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. + * @param string $suffix The autoloader suffix. + */ + public function dump( + Composer $composer, + Config $config, + InstalledRepositoryInterface $localRepo, + PackageInterface $mainPackage, + InstallationManager $installationManager, + $targetDir, + $scanPsrPackages = false, + $suffix = null + ) { + $this->filesystem->ensureDirectoryExists( $config->get( 'vendor-dir' ) ); + + $packageMap = $composer->getAutoloadGenerator()->buildPackageMap( $installationManager, $mainPackage, $localRepo->getCanonicalPackages() ); + $autoloads = $this->parseAutoloads( $packageMap, $mainPackage ); + + // Convert the autoloads into a format that the manifest generator can consume more easily. + $basePath = $this->filesystem->normalizePath( realpath( getcwd() ) ); + $vendorPath = $this->filesystem->normalizePath( realpath( $config->get( 'vendor-dir' ) ) ); + $processedAutoloads = $this->processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ); + unset( $packageMap, $autoloads ); + + // Make sure none of the legacy files remain that can lead to problems with the autoloader. + $this->removeLegacyFiles( $vendorPath ); + + // Write all of the files now that we're done. + $this->writeAutoloaderFiles( $vendorPath . '/jetpack-autoloader/', $suffix ); + $this->writeManifests( $vendorPath . '/' . $targetDir, $processedAutoloads ); + + if ( ! $scanPsrPackages ) { + $this->io->writeError( 'You are generating an unoptimized autoloader. If this is a production build, consider using the -o option.' ); + } + } + + /** + * Compiles an ordered list of namespace => path mappings + * + * @param array $packageMap Array of array(package, installDir-relative-to-composer.json). + * @param PackageInterface $mainPackage Main package instance. + * + * @return array The list of path mappings. + */ + public function parseAutoloads( array $packageMap, PackageInterface $mainPackage ) { + $rootPackageMap = array_shift( $packageMap ); + + $sortedPackageMap = $this->sortPackageMap( $packageMap ); + $sortedPackageMap[] = $rootPackageMap; + array_unshift( $packageMap, $rootPackageMap ); + + $psr0 = $this->parseAutoloadsType( $packageMap, 'psr-0', $mainPackage ); + $psr4 = $this->parseAutoloadsType( $packageMap, 'psr-4', $mainPackage ); + $classmap = $this->parseAutoloadsType( array_reverse( $sortedPackageMap ), 'classmap', $mainPackage ); + $files = $this->parseAutoloadsType( $sortedPackageMap, 'files', $mainPackage ); + + krsort( $psr0 ); + krsort( $psr4 ); + + return array( + 'psr-0' => $psr0, + 'psr-4' => $psr4, + 'classmap' => $classmap, + 'files' => $files, + ); + } + + /** + * Sorts packages by dependency weight + * + * Packages of equal weight retain the original order + * + * @param array $packageMap The package map. + * + * @return array + */ + protected function sortPackageMap( array $packageMap ) { + $packages = array(); + $paths = array(); + + foreach ( $packageMap as $item ) { + list( $package, $path ) = $item; + $name = $package->getName(); + $packages[ $name ] = $package; + $paths[ $name ] = $path; + } + + $sortedPackages = PackageSorter::sortPackages( $packages ); + + $sortedPackageMap = array(); + + foreach ( $sortedPackages as $package ) { + $name = $package->getName(); + $sortedPackageMap[] = array( $packages[ $name ], $paths[ $name ] ); + } + + return $sortedPackageMap; + } + + /** + * Returns the file identifier. + * + * @param PackageInterface $package The package instance. + * @param string $path The path. + */ + protected function getFileIdentifier( PackageInterface $package, $path ) { + return md5( $package->getName() . ':' . $path ); + } + + /** + * Returns the path code for the given path. + * + * @param Filesystem $filesystem The filesystem instance. + * @param string $basePath The base path. + * @param string $vendorPath The vendor path. + * @param string $path The path. + * + * @return string The path code. + */ + protected function getPathCode( Filesystem $filesystem, $basePath, $vendorPath, $path ) { + if ( ! $filesystem->isAbsolutePath( $path ) ) { + $path = $basePath . '/' . $path; + } + $path = $filesystem->normalizePath( $path ); + + $baseDir = ''; + if ( 0 === strpos( $path . '/', $vendorPath . '/' ) ) { + $path = substr( $path, strlen( $vendorPath ) ); + $baseDir = '$vendorDir'; + + if ( false !== $path ) { + $baseDir .= ' . '; + } + } else { + $path = $filesystem->normalizePath( $filesystem->findShortestPath( $basePath, $path, true ) ); + if ( ! $filesystem->isAbsolutePath( $path ) ) { + $baseDir = '$baseDir . '; + $path = '/' . $path; + } + } + + if ( strpos( $path, '.phar' ) !== false ) { + $baseDir = "'phar://' . " . $baseDir; + } + + return $baseDir . ( ( false !== $path ) ? var_export( $path, true ) : '' ); + } + + /** + * This function differs from the composer parseAutoloadsType in that beside returning the path. + * It also return the path and the version of a package. + * + * Supports PSR-4, PSR-0, and classmap parsing. + * + * @param array $packageMap Map of all the packages. + * @param string $type Type of autoloader to use. + * @param PackageInterface $mainPackage Instance of the Package Object. + * + * @return array + */ + protected function parseAutoloadsType( array $packageMap, $type, PackageInterface $mainPackage ) { + $autoloads = array(); + + foreach ( $packageMap as $item ) { + list($package, $installPath) = $item; + $autoload = $package->getAutoload(); + + if ( $package === $mainPackage ) { + $autoload = array_merge_recursive( $autoload, $package->getDevAutoload() ); + } + + if ( null !== $package->getTargetDir() && $package !== $mainPackage ) { + $installPath = substr( $installPath, 0, -strlen( '/' . $package->getTargetDir() ) ); + } + + if ( in_array( $type, array( 'psr-4', 'psr-0' ), true ) && isset( $autoload[ $type ] ) && is_array( $autoload[ $type ] ) ) { + foreach ( $autoload[ $type ] as $namespace => $paths ) { + $paths = is_array( $paths ) ? $paths : array( $paths ); + foreach ( $paths as $path ) { + $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; + $autoloads[ $namespace ][] = array( + 'path' => $relativePath, + 'version' => $package->getVersion(), // Version of the class comes from the package - should we try to parse it? + ); + } + } + } + + if ( 'classmap' === $type && isset( $autoload['classmap'] ) && is_array( $autoload['classmap'] ) ) { + foreach ( $autoload['classmap'] as $paths ) { + $paths = is_array( $paths ) ? $paths : array( $paths ); + foreach ( $paths as $path ) { + $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; + $autoloads[] = array( + 'path' => $relativePath, + 'version' => $package->getVersion(), // Version of the class comes from the package - should we try to parse it? + ); + } + } + } + if ( 'files' === $type && isset( $autoload['files'] ) && is_array( $autoload['files'] ) ) { + foreach ( $autoload['files'] as $paths ) { + $paths = is_array( $paths ) ? $paths : array( $paths ); + foreach ( $paths as $path ) { + $relativePath = empty( $installPath ) ? ( empty( $path ) ? '.' : $path ) : $installPath . '/' . $path; + $autoloads[ $this->getFileIdentifier( $package, $path ) ] = array( + 'path' => $relativePath, + 'version' => $package->getVersion(), // Version of the file comes from the package - should we try to parse it? + ); + } + } + } + } + + return $autoloads; + } + + /** + * Given Composer's autoloads this will convert them to a version that we can use to generate the manifests. + * + * When the $scanPsrPackages argument is true, PSR-4 namespaces are converted to classmaps. When $scanPsrPackages + * is false, PSR-4 namespaces are not converted to classmaps. + * + * PSR-0 namespaces are always converted to classmaps. + * + * @param array $autoloads The autoloads we want to process. + * @param bool $scanPsrPackages Whether or not PSR-4 packages should be converted to a classmap. + * @param string $vendorPath The path to the vendor directory. + * @param string $basePath The path to the current directory. + * + * @return array $processedAutoloads + */ + private function processAutoloads( $autoloads, $scanPsrPackages, $vendorPath, $basePath ) { + $processor = new AutoloadProcessor( + function ( $path, $excludedClasses, $namespace ) use ( $basePath ) { + $dir = $this->filesystem->normalizePath( + $this->filesystem->isAbsolutePath( $path ) ? $path : $basePath . '/' . $path + ); + return ClassMapGenerator::createMap( + $dir, + $excludedClasses, + null, // Don't pass the IOInterface since the normal autoload generation will have reported already. + empty( $namespace ) ? null : $namespace + ); + }, + function ( $path ) use ( $basePath, $vendorPath ) { + return $this->getPathCode( $this->filesystem, $basePath, $vendorPath, $path ); + } + ); + + return array( + 'psr-4' => $processor->processPsr4Packages( $autoloads, $scanPsrPackages ), + 'classmap' => $processor->processClassmap( $autoloads, $scanPsrPackages ), + 'files' => $processor->processFiles( $autoloads ), + ); + } + + /** + * Removes all of the legacy autoloader files so they don't cause any problems. + * + * @param string $outDir The directory legacy files are written to. + */ + private function removeLegacyFiles( $outDir ) { + $files = array( + 'autoload_functions.php', + 'class-autoloader-handler.php', + 'class-classes-handler.php', + 'class-files-handler.php', + 'class-plugins-handler.php', + 'class-version-selector.php', + ); + foreach ( $files as $file ) { + $this->filesystem->remove( $outDir . '/' . $file ); + } + } + + /** + * Writes all of the autoloader files to disk. + * + * @param string $outDir The directory to write to. + * @param string $suffix The unique autoloader suffix. + */ + private function writeAutoloaderFiles( $outDir, $suffix ) { + $this->io->writeError( "Generating jetpack autoloader ($outDir)" ); + + // We will remove all autoloader files to generate this again. + $this->filesystem->emptyDirectory( $outDir ); + + // Write the autoloader files. + AutoloadFileWriter::copyAutoloaderFiles( $this->io, $outDir, $suffix ); + } + + /** + * Writes all of the manifest files to disk. + * + * @param string $outDir The directory to write to. + * @param array $processedAutoloads The processed autoloads. + */ + private function writeManifests( $outDir, $processedAutoloads ) { + $this->io->writeError( "Generating jetpack autoloader manifests ($outDir)" ); + + $manifestFiles = array( + 'classmap' => 'jetpack_autoload_classmap.php', + 'psr-4' => 'jetpack_autoload_psr4.php', + 'files' => 'jetpack_autoload_filemap.php', + ); + + foreach ( $manifestFiles as $key => $file ) { + // Make sure the file doesn't exist so it isn't there if we don't write it. + $this->filesystem->remove( $outDir . '/' . $file ); + if ( empty( $processedAutoloads[ $key ] ) ) { + continue; + } + + $content = ManifestGenerator::buildManifest( $key, $file, $processedAutoloads[ $key ] ); + if ( empty( $content ) ) { + continue; + } + + if ( file_put_contents( $outDir . '/' . $file, $content ) ) { + $this->io->writeError( " Generated: $file" ); + } else { + $this->io->writeError( " Error: $file" ); + } + } + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/AutoloadProcessor.php b/vendor/automattic/jetpack-autoloader/src/AutoloadProcessor.php new file mode 100644 index 0000000..70501f2 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/AutoloadProcessor.php @@ -0,0 +1,180 @@ +classmapScanner = $classmapScanner; + $this->pathCodeTransformer = $pathCodeTransformer; + } + + /** + * Processes the classmap autoloads into a relative path format including the version for each file. + * + * @param array $autoloads The autoloads we are processing. + * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. + * + * @return array $processed + */ + public function processClassmap( $autoloads, $scanPsrPackages ) { + // We can't scan PSR packages if we don't actually have any. + if ( empty( $autoloads['psr-4'] ) ) { + $scanPsrPackages = false; + } + + if ( empty( $autoloads['classmap'] ) && ! $scanPsrPackages ) { + return null; + } + + $excludedClasses = null; + if ( ! empty( $autoloads['exclude-from-classmap'] ) ) { + $excludedClasses = '{(' . implode( '|', $autoloads['exclude-from-classmap'] ) . ')}'; + } + + $processed = array(); + + if ( $scanPsrPackages ) { + foreach ( $autoloads['psr-4'] as $namespace => $sources ) { + $namespace = empty( $namespace ) ? null : $namespace; + + foreach ( $sources as $source ) { + $classmap = call_user_func( $this->classmapScanner, $source['path'], $excludedClasses, $namespace ); + + foreach ( $classmap as $class => $path ) { + $processed[ $class ] = array( + 'version' => $source['version'], + 'path' => call_user_func( $this->pathCodeTransformer, $path ), + ); + } + } + } + } + + /* + * PSR-0 namespaces are converted to classmaps for both optimized and unoptimized autoloaders because any new + * development should use classmap or PSR-4 autoloading. + */ + if ( ! empty( $autoloads['psr-0'] ) ) { + foreach ( $autoloads['psr-0'] as $namespace => $sources ) { + $namespace = empty( $namespace ) ? null : $namespace; + + foreach ( $sources as $source ) { + $classmap = call_user_func( $this->classmapScanner, $source['path'], $excludedClasses, $namespace ); + foreach ( $classmap as $class => $path ) { + $processed[ $class ] = array( + 'version' => $source['version'], + 'path' => call_user_func( $this->pathCodeTransformer, $path ), + ); + } + } + } + } + + if ( ! empty( $autoloads['classmap'] ) ) { + foreach ( $autoloads['classmap'] as $package ) { + $classmap = call_user_func( $this->classmapScanner, $package['path'], $excludedClasses, null ); + + foreach ( $classmap as $class => $path ) { + $processed[ $class ] = array( + 'version' => $package['version'], + 'path' => call_user_func( $this->pathCodeTransformer, $path ), + ); + } + } + } + + return $processed; + } + + /** + * Processes the PSR-4 autoloads into a relative path format including the version for each file. + * + * @param array $autoloads The autoloads we are processing. + * @param bool $scanPsrPackages Whether or not PSR packages should be converted to a classmap. + * + * @return array $processed + */ + public function processPsr4Packages( $autoloads, $scanPsrPackages ) { + if ( $scanPsrPackages || empty( $autoloads['psr-4'] ) ) { + return null; + } + + $processed = array(); + + foreach ( $autoloads['psr-4'] as $namespace => $packages ) { + $namespace = empty( $namespace ) ? null : $namespace; + $paths = array(); + + foreach ( $packages as $package ) { + $paths[] = call_user_func( $this->pathCodeTransformer, $package['path'] ); + } + + $processed[ $namespace ] = array( + 'version' => $package['version'], + 'path' => $paths, + ); + } + + return $processed; + } + + /** + * Processes the file autoloads into a relative format including the version for each file. + * + * @param array $autoloads The autoloads we are processing. + * + * @return array|null $processed + */ + public function processFiles( $autoloads ) { + if ( empty( $autoloads['files'] ) ) { + return null; + } + + $processed = array(); + + foreach ( $autoloads['files'] as $file_id => $package ) { + $processed[ $file_id ] = array( + 'version' => $package['version'], + 'path' => call_user_func( $this->pathCodeTransformer, $package['path'] ), + ); + } + + return $processed; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php b/vendor/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php new file mode 100644 index 0000000..78c908c --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php @@ -0,0 +1,198 @@ +composer = $composer; + $this->io = $io; + } + + /** + * Do nothing. + * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + * + * @param Composer $composer Composer object. + * @param IOInterface $io IO object. + */ + public function deactivate( Composer $composer, IOInterface $io ) { + /* + * Intentionally left empty. This is a PluginInterface method. + * phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + */ + } + + /** + * Do nothing. + * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + * + * @param Composer $composer Composer object. + * @param IOInterface $io IO object. + */ + public function uninstall( Composer $composer, IOInterface $io ) { + /* + * Intentionally left empty. This is a PluginInterface method. + * phpcs:enable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + */ + } + + /** + * Tell composer to listen for events and do something with them. + * + * @return array List of subscribed events. + */ + public static function getSubscribedEvents() { + return array( + ScriptEvents::POST_AUTOLOAD_DUMP => 'postAutoloadDump', + ); + } + + /** + * Generate the custom autolaoder. + * + * @param Event $event Script event object. + */ + public function postAutoloadDump( Event $event ) { + // When the autoloader is not required by the root package we don't want to execute it. + // This prevents unwanted transitive execution that generates unused autoloaders or + // at worst throws fatal executions. + if ( ! $this->isRequiredByRoot() ) { + return; + } + + $config = $this->composer->getConfig(); + + if ( 'vendor' !== $config->raw()['config']['vendor-dir'] ) { + $this->io->writeError( "\nAn error occurred while generating the autoloader files:", true ); + $this->io->writeError( 'The project\'s composer.json or composer environment set a non-default vendor directory.', true ); + $this->io->writeError( 'The default composer vendor directory must be used.', true ); + exit(); + } + + $installationManager = $this->composer->getInstallationManager(); + $repoManager = $this->composer->getRepositoryManager(); + $localRepo = $repoManager->getLocalRepository(); + $package = $this->composer->getPackage(); + $optimize = $event->getFlags()['optimize']; + $suffix = $this->determineSuffix(); + + $generator = new AutoloadGenerator( $this->io ); + $generator->dump( $this->composer, $config, $localRepo, $package, $installationManager, 'composer', $optimize, $suffix ); + $this->generated = true; + } + + /** + * Determine the suffix for the autoloader class. + * + * Reuses an existing suffix from vendor/autoload_packages.php or vendor/autoload.php if possible. + * + * @return string Suffix. + */ + private function determineSuffix() { + $config = $this->composer->getConfig(); + $vendorPath = $config->get( 'vendor-dir' ); + + // Command line. + $suffix = $config->get( 'autoloader-suffix' ); + if ( $suffix ) { + return $suffix; + } + + // Reuse our own suffix, if any. + if ( is_readable( $vendorPath . '/autoload_packages.php' ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $content = file_get_contents( $vendorPath . '/autoload_packages.php' ); + if ( preg_match( '/^namespace Automattic\\\\Jetpack\\\\Autoloader\\\\jp([^;\s]+);/m', $content, $match ) ) { + return $match[1]; + } + } + + // Reuse Composer's suffix, if any. + if ( is_readable( $vendorPath . '/autoload.php' ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $content = file_get_contents( $vendorPath . '/autoload.php' ); + if ( preg_match( '{ComposerAutoloaderInit([^:\s]+)::}', $content, $match ) ) { + return $match[1]; + } + } + + // Generate a random suffix. + return md5( uniqid( '', true ) ); + } + + /** + * Checks to see whether or not the root package is the one that required the autoloader. + * + * @return bool + */ + private function isRequiredByRoot() { + $package = $this->composer->getPackage(); + $requires = $package->getRequires(); + if ( ! is_array( $requires ) ) { + $requires = array(); + } + $devRequires = $package->getDevRequires(); + if ( ! is_array( $devRequires ) ) { + $devRequires = array(); + } + $requires = array_merge( $requires, $devRequires ); + + if ( empty( $requires ) ) { + $this->io->writeError( "\nThe package is not required and this should never happen?", true ); + exit(); + } + + foreach ( $requires as $require ) { + if ( 'automattic/jetpack-autoloader' === $require->getTarget() ) { + return true; + } + } + + return false; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/ManifestGenerator.php b/vendor/automattic/jetpack-autoloader/src/ManifestGenerator.php new file mode 100644 index 0000000..595a97f --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/ManifestGenerator.php @@ -0,0 +1,121 @@ + $data ) { + $key = var_export( $key, true ); + $versionCode = var_export( $data['version'], true ); + $fileContent .= << array( + 'version' => $versionCode, + 'path' => {$data['path']} + ), +MANIFEST_CODE; + $fileContent .= PHP_EOL; + } + + return self::buildFile( $fileName, $fileContent ); + } + + /** + * Builds the contents for the PSR-4 manifest file. + * + * @param string $fileName The filename we are building. + * @param array $namespaces The formatted PSR-4 data for the manifest. + * + * @return string|null $manifestFile + */ + private static function buildPsr4Manifest( $fileName, $namespaces ) { + $fileContent = PHP_EOL; + foreach ( $namespaces as $namespace => $data ) { + $namespaceCode = var_export( $namespace, true ); + $versionCode = var_export( $data['version'], true ); + $pathCode = 'array( ' . implode( ', ', $data['path'] ) . ' )'; + $fileContent .= << array( + 'version' => $versionCode, + 'path' => $pathCode + ), +MANIFEST_CODE; + $fileContent .= PHP_EOL; + } + + return self::buildFile( $fileName, $fileContent ); + } + + /** + * Generate the PHP that will be used in the file. + * + * @param string $fileName The filename we are building. + * @param string $content The content to be written into the file. + * + * @return string $fileContent + */ + private static function buildFile( $fileName, $content ) { + return <<php_autoloader = $php_autoloader; + $this->hook_manager = $hook_manager; + $this->manifest_reader = $manifest_reader; + $this->version_selector = $version_selector; + } + + /** + * Checks to see whether or not an autoloader is currently in the process of initializing. + * + * @return bool + */ + public function is_initializing() { + // If no version has been set it means that no autoloader has started initializing yet. + global $jetpack_autoloader_latest_version; + if ( ! isset( $jetpack_autoloader_latest_version ) ) { + return false; + } + + // When the version is set but the classmap is not it ALWAYS means that this is the + // latest autoloader and is being included by an older one. + global $jetpack_packages_classmap; + if ( empty( $jetpack_packages_classmap ) ) { + return true; + } + + // Version 2.4.0 added a new global and altered the reset semantics. We need to check + // the other global as well since it may also point at initialization. + // Note: We don't need to check for the class first because every autoloader that + // will set the latest version global requires this class in the classmap. + $replacing_version = $jetpack_packages_classmap[ AutoloadGenerator::class ]['version']; + if ( $this->version_selector->is_dev_version( $replacing_version ) || version_compare( $replacing_version, '2.4.0.0', '>=' ) ) { + global $jetpack_autoloader_loader; + if ( ! isset( $jetpack_autoloader_loader ) ) { + return true; + } + } + + return false; + } + + /** + * Activates an autoloader using the given plugins and activates it. + * + * @param string[] $plugins The plugins to initialize the autoloader for. + */ + public function activate_autoloader( $plugins ) { + global $jetpack_packages_psr4; + $jetpack_packages_psr4 = array(); + $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_psr4.php', $jetpack_packages_psr4 ); + + global $jetpack_packages_classmap; + $jetpack_packages_classmap = array(); + $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_classmap.php', $jetpack_packages_classmap ); + + global $jetpack_packages_filemap; + $jetpack_packages_filemap = array(); + $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_filemap.php', $jetpack_packages_filemap ); + + $loader = new Version_Loader( + $this->version_selector, + $jetpack_packages_classmap, + $jetpack_packages_psr4, + $jetpack_packages_filemap + ); + + $this->php_autoloader->register_autoloader( $loader ); + + // Now that the autoloader is active we can load the filemap. + $loader->load_filemap(); + } + + /** + * Resets the active autoloader and all related global state. + */ + public function reset_autoloader() { + $this->php_autoloader->unregister_autoloader(); + $this->hook_manager->reset(); + + // Clear all of the autoloader globals so that older autoloaders don't do anything strange. + global $jetpack_autoloader_latest_version; + $jetpack_autoloader_latest_version = null; + + global $jetpack_packages_classmap; + $jetpack_packages_classmap = array(); // Must be array to avoid exceptions in old autoloaders! + + global $jetpack_packages_psr4; + $jetpack_packages_psr4 = array(); // Must be array to avoid exceptions in old autoloaders! + + global $jetpack_packages_filemap; + $jetpack_packages_filemap = array(); // Must be array to avoid exceptions in old autoloaders! + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-autoloader-locator.php b/vendor/automattic/jetpack-autoloader/src/class-autoloader-locator.php new file mode 100644 index 0000000..828fe25 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-autoloader-locator.php @@ -0,0 +1,82 @@ +version_selector = $version_selector; + } + + /** + * Finds the path to the plugin with the latest autoloader. + * + * @param array $plugin_paths An array of plugin paths. + * @param string $latest_version The latest version reference. + * + * @return string|null + */ + public function find_latest_autoloader( $plugin_paths, &$latest_version ) { + $latest_plugin = null; + + foreach ( $plugin_paths as $plugin_path ) { + $version = $this->get_autoloader_version( $plugin_path ); + if ( ! $this->version_selector->is_version_update_required( $latest_version, $version ) ) { + continue; + } + + $latest_version = $version; + $latest_plugin = $plugin_path; + } + + return $latest_plugin; + } + + /** + * Gets the path to the autoloader. + * + * @param string $plugin_path The path to the plugin. + * + * @return string + */ + public function get_autoloader_path( $plugin_path ) { + return trailingslashit( $plugin_path ) . 'vendor/autoload_packages.php'; + } + + /** + * Gets the version for the autoloader. + * + * @param string $plugin_path The path to the plugin. + * + * @return string|null + */ + public function get_autoloader_version( $plugin_path ) { + $classmap = trailingslashit( $plugin_path ) . 'vendor/composer/jetpack_autoload_classmap.php'; + if ( ! file_exists( $classmap ) ) { + return null; + } + + $classmap = require $classmap; + if ( isset( $classmap[ AutoloadGenerator::class ] ) ) { + return $classmap[ AutoloadGenerator::class ]['version']; + } + + return null; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-autoloader.php b/vendor/automattic/jetpack-autoloader/src/class-autoloader.php new file mode 100644 index 0000000..1668dfa --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-autoloader.php @@ -0,0 +1,82 @@ +get( Autoloader_Handler::class ); + + // If the autoloader is already initializing it means that it has included us as the latest. + $was_included_by_autoloader = $autoloader_handler->is_initializing(); + + /** @var Plugin_Locator $plugin_locator */ + $plugin_locator = $container->get( Plugin_Locator::class ); + + /** @var Plugins_Handler $plugins_handler */ + $plugins_handler = $container->get( Plugins_Handler::class ); + + // The current plugin is the one that we are attempting to initialize here. + $current_plugin = $plugin_locator->find_current_plugin(); + + // The active plugins are those that we were able to discover on the site. This list will not + // include mu-plugins, those activated by code, or those who are hidden by filtering. We also + // want to take care to not consider the current plugin unknown if it was included by an + // autoloader. This avoids the case where a plugin will be marked "active" while deactivated + // due to it having the latest autoloader. + $active_plugins = $plugins_handler->get_active_plugins( true, ! $was_included_by_autoloader ); + + // The cached plugins are all of those that were active or discovered by the autoloader during a previous request. + // Note that it's possible this list will include plugins that have since been deactivated, but after a request + // the cache should be updated and the deactivated plugins will be removed. + $cached_plugins = $plugins_handler->get_cached_plugins(); + + // We combine the active list and cached list to preemptively load classes for plugins that are + // presently unknown but will be loaded during the request. While this may result in us considering packages in + // deactivated plugins there shouldn't be any problems as a result and the eventual consistency is sufficient. + $all_plugins = array_merge( $active_plugins, $cached_plugins ); + + // In particular we also include the current plugin to address the case where it is the latest autoloader + // but also unknown (and not cached). We don't want it in the active list because we don't know that it + // is active but we need it in the all plugins list so that it is considered by the autoloader. + $all_plugins[] = $current_plugin; + + // We require uniqueness in the array to avoid processing the same plugin more than once. + $all_plugins = array_values( array_unique( $all_plugins ) ); + + /** @var Latest_Autoloader_Guard $guard */ + $guard = $container->get( Latest_Autoloader_Guard::class ); + if ( $guard->should_stop_init( $current_plugin, $all_plugins, $was_included_by_autoloader ) ) { + return; + } + + // Initialize the autoloader using the handler now that we're ready. + $autoloader_handler->activate_autoloader( $all_plugins ); + + /** @var Hook_Manager $hook_manager */ + $hook_manager = $container->get( Hook_Manager::class ); + + // Register a shutdown handler to clean up the autoloader. + $hook_manager->add_action( 'shutdown', new Shutdown_Handler( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) ); + + // phpcs:enable Generic.Commenting.DocComment.MissingShort + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-container.php b/vendor/automattic/jetpack-autoloader/src/class-container.php new file mode 100644 index 0000000..19ea95c --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-container.php @@ -0,0 +1,142 @@ + 'Hook_Manager', + ); + + /** + * A map of all the dependencies we've registered with the container and created. + * + * @var array + */ + protected $dependencies; + + /** + * The constructor. + */ + public function __construct() { + $this->dependencies = array(); + + $this->register_shared_dependencies(); + $this->register_dependencies(); + $this->initialize_globals(); + } + + /** + * Gets a dependency out of the container. + * + * @param string $class The class to fetch. + * + * @return mixed + * @throws \InvalidArgumentException When a class that isn't registered with the container is fetched. + */ + public function get( $class ) { + if ( ! isset( $this->dependencies[ $class ] ) ) { + throw new \InvalidArgumentException( "Class '$class' is not registered with the container." ); + } + + return $this->dependencies[ $class ]; + } + + /** + * Registers all of the dependencies that are shared between all instances of the autoloader. + */ + private function register_shared_dependencies() { + global $jetpack_autoloader_container_shared; + if ( ! isset( $jetpack_autoloader_container_shared ) ) { + $jetpack_autoloader_container_shared = array(); + } + + $key = self::SHARED_DEPENDENCY_KEYS[ Hook_Manager::class ]; + if ( ! isset( $jetpack_autoloader_container_shared[ $key ] ) ) { + require_once __DIR__ . '/class-hook-manager.php'; + $jetpack_autoloader_container_shared[ $key ] = new Hook_Manager(); + } + $this->dependencies[ Hook_Manager::class ] = &$jetpack_autoloader_container_shared[ $key ]; + } + + /** + * Registers all of the dependencies with the container. + */ + private function register_dependencies() { + require_once __DIR__ . '/class-path-processor.php'; + $this->dependencies[ Path_Processor::class ] = new Path_Processor(); + + require_once __DIR__ . '/class-plugin-locator.php'; + $this->dependencies[ Plugin_Locator::class ] = new Plugin_Locator( + $this->get( Path_Processor::class ) + ); + + require_once __DIR__ . '/class-version-selector.php'; + $this->dependencies[ Version_Selector::class ] = new Version_Selector(); + + require_once __DIR__ . '/class-autoloader-locator.php'; + $this->dependencies[ Autoloader_Locator::class ] = new Autoloader_Locator( + $this->get( Version_Selector::class ) + ); + + require_once __DIR__ . '/class-php-autoloader.php'; + $this->dependencies[ PHP_Autoloader::class ] = new PHP_Autoloader(); + + require_once __DIR__ . '/class-manifest-reader.php'; + $this->dependencies[ Manifest_Reader::class ] = new Manifest_Reader( + $this->get( Version_Selector::class ) + ); + + require_once __DIR__ . '/class-plugins-handler.php'; + $this->dependencies[ Plugins_Handler::class ] = new Plugins_Handler( + $this->get( Plugin_Locator::class ), + $this->get( Path_Processor::class ) + ); + + require_once __DIR__ . '/class-autoloader-handler.php'; + $this->dependencies[ Autoloader_Handler::class ] = new Autoloader_Handler( + $this->get( PHP_Autoloader::class ), + $this->get( Hook_Manager::class ), + $this->get( Manifest_Reader::class ), + $this->get( Version_Selector::class ) + ); + + require_once __DIR__ . '/class-latest-autoloader-guard.php'; + $this->dependencies[ Latest_Autoloader_Guard::class ] = new Latest_Autoloader_Guard( + $this->get( Plugins_Handler::class ), + $this->get( Autoloader_Handler::class ), + $this->get( Autoloader_Locator::class ) + ); + + // Register any classes that we will use elsewhere. + require_once __DIR__ . '/class-version-loader.php'; + require_once __DIR__ . '/class-shutdown-handler.php'; + } + + /** + * Initializes any of the globals needed by the autoloader. + */ + private function initialize_globals() { + /* + * This global was retired in version 2.9. The value is set to 'false' to maintain + * compatibility with older versions of the autoloader. + */ + global $jetpack_autoloader_including_latest; + $jetpack_autoloader_including_latest = false; + + // Not all plugins can be found using the locator. In cases where a plugin loads the autoloader + // but was not discoverable, we will record them in this array to track them as "active". + global $jetpack_autoloader_activating_plugins_paths; + if ( ! isset( $jetpack_autoloader_activating_plugins_paths ) ) { + $jetpack_autoloader_activating_plugins_paths = array(); + } + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-hook-manager.php b/vendor/automattic/jetpack-autoloader/src/class-hook-manager.php new file mode 100644 index 0000000..1d64c75 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-hook-manager.php @@ -0,0 +1,68 @@ +registered_hooks = array(); + } + + /** + * Adds an action to WordPress and registers it internally. + * + * @param string $tag The name of the action which is hooked. + * @param callable $callable The function to call. + * @param int $priority Used to specify the priority of the action. + * @param int $accepted_args Used to specify the number of arguments the callable accepts. + */ + public function add_action( $tag, $callable, $priority = 10, $accepted_args = 1 ) { + $this->registered_hooks[ $tag ][] = array( + 'priority' => $priority, + 'callable' => $callable, + ); + + add_action( $tag, $callable, $priority, $accepted_args ); + } + + /** + * Adds a filter to WordPress and registers it internally. + * + * @param string $tag The name of the filter which is hooked. + * @param callable $callable The function to call. + * @param int $priority Used to specify the priority of the filter. + * @param int $accepted_args Used to specify the number of arguments the callable accepts. + */ + public function add_filter( $tag, $callable, $priority = 10, $accepted_args = 1 ) { + $this->registered_hooks[ $tag ][] = array( + 'priority' => $priority, + 'callable' => $callable, + ); + + add_filter( $tag, $callable, $priority, $accepted_args ); + } + + /** + * Removes all of the registered hooks. + */ + public function reset() { + foreach ( $this->registered_hooks as $tag => $hooks ) { + foreach ( $hooks as $hook ) { + remove_filter( $tag, $hook['callable'], $hook['priority'] ); + } + } + $this->registered_hooks = array(); + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-latest-autoloader-guard.php b/vendor/automattic/jetpack-autoloader/src/class-latest-autoloader-guard.php new file mode 100644 index 0000000..54edfc8 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-latest-autoloader-guard.php @@ -0,0 +1,78 @@ +plugins_handler = $plugins_handler; + $this->autoloader_handler = $autoloader_handler; + $this->autoloader_locator = $autoloader_locator; + } + + /** + * Indicates whether or not the autoloader should be initialized. Note that this function + * has the side-effect of actually loading the latest autoloader in the event that this + * is not it. + * + * @param string $current_plugin The current plugin we're checking. + * @param string[] $plugins The active plugins to check for autoloaders in. + * @param bool $was_included_by_autoloader Indicates whether or not this autoloader was included by another. + * + * @return bool True if we should stop initialization, otherwise false. + */ + public function should_stop_init( $current_plugin, $plugins, $was_included_by_autoloader ) { + global $jetpack_autoloader_latest_version; + + // We need to reset the autoloader when the plugins change because + // that means the autoloader was generated with a different list. + if ( $this->plugins_handler->have_plugins_changed( $plugins ) ) { + $this->autoloader_handler->reset_autoloader(); + } + + // When the latest autoloader has already been found we don't need to search for it again. + // We should take care however because this will also trigger if the autoloader has been + // included by an older one. + if ( isset( $jetpack_autoloader_latest_version ) && ! $was_included_by_autoloader ) { + return true; + } + + $latest_plugin = $this->autoloader_locator->find_latest_autoloader( $plugins, $jetpack_autoloader_latest_version ); + if ( isset( $latest_plugin ) && $latest_plugin !== $current_plugin ) { + require $this->autoloader_locator->get_autoloader_path( $latest_plugin ); + return true; + } + + return false; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-manifest-reader.php b/vendor/automattic/jetpack-autoloader/src/class-manifest-reader.php new file mode 100644 index 0000000..8eb4825 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-manifest-reader.php @@ -0,0 +1,91 @@ +version_selector = $version_selector; + } + + /** + * Reads all of the manifests in the given plugin paths. + * + * @param array $plugin_paths The paths to the plugins we're loading the manifest in. + * @param string $manifest_path The path that we're loading the manifest from in each plugin. + * @param array $path_map The path map to add the contents of the manifests to. + * + * @return array $path_map The path map we've built using the manifests in each plugin. + */ + public function read_manifests( $plugin_paths, $manifest_path, &$path_map ) { + $file_paths = array_map( + function ( $path ) use ( $manifest_path ) { + return trailingslashit( $path ) . $manifest_path; + }, + $plugin_paths + ); + + foreach ( $file_paths as $path ) { + $this->register_manifest( $path, $path_map ); + } + + return $path_map; + } + + /** + * Registers a plugin's manifest file with the path map. + * + * @param string $manifest_path The absolute path to the manifest that we're loading. + * @param array $path_map The path map to add the contents of the manifest to. + */ + protected function register_manifest( $manifest_path, &$path_map ) { + if ( ! is_readable( $manifest_path ) ) { + return; + } + + $manifest = require $manifest_path; + if ( ! is_array( $manifest ) ) { + return; + } + + foreach ( $manifest as $key => $data ) { + $this->register_record( $key, $data, $path_map ); + } + } + + /** + * Registers an entry from the manifest in the path map. + * + * @param string $key The identifier for the entry we're registering. + * @param array $data The data for the entry we're registering. + * @param array $path_map The path map to add the contents of the manifest to. + */ + protected function register_record( $key, $data, &$path_map ) { + if ( isset( $path_map[ $key ]['version'] ) ) { + $selected_version = $path_map[ $key ]['version']; + } else { + $selected_version = null; + } + + if ( $this->version_selector->is_version_update_required( $selected_version, $data['version'] ) ) { + $path_map[ $key ] = array( + 'version' => $data['version'], + 'path' => $data['path'], + ); + } + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-path-processor.php b/vendor/automattic/jetpack-autoloader/src/class-path-processor.php new file mode 100644 index 0000000..bc480c6 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-path-processor.php @@ -0,0 +1,186 @@ +get_normalized_constants(); + foreach ( $constants as $constant => $constant_path ) { + $len = strlen( $constant_path ); + if ( substr( $path, 0, $len ) !== $constant_path ) { + continue; + } + + return substr_replace( $path, '{{' . $constant . '}}', 0, $len ); + } + + return $path; + } + + /** + * Given a path this will replace any of the path constant tokens with the expanded path. + * + * @param string $tokenized_path The path we want to process. + * + * @return string The expanded path. + */ + public function untokenize_path_constants( $tokenized_path ) { + $tokenized_path = wp_normalize_path( $tokenized_path ); + + $constants = $this->get_normalized_constants(); + foreach ( $constants as $constant => $constant_path ) { + $constant = '{{' . $constant . '}}'; + + $len = strlen( $constant ); + if ( substr( $tokenized_path, 0, $len ) !== $constant ) { + continue; + } + + return $this->get_real_path( substr_replace( $tokenized_path, $constant_path, 0, $len ) ); + } + + return $tokenized_path; + } + + /** + * Given a file and an array of places it might be, this will find the absolute path and return it. + * + * @param string $file The plugin or theme file to resolve. + * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. + * + * @return string|false Returns the absolute path to the directory, otherwise false. + */ + public function find_directory_with_autoloader( $file, $directories_to_check ) { + $file = wp_normalize_path( $file ); + + if ( ! $this->is_absolute_path( $file ) ) { + $file = $this->find_absolute_plugin_path( $file, $directories_to_check ); + if ( ! isset( $file ) ) { + return false; + } + } + + // We need the real path for consistency with __DIR__ paths. + $file = $this->get_real_path( $file ); + + // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged + $directory = @is_file( $file ) ? dirname( $file ) : $file; + if ( ! @is_file( $directory . '/vendor/composer/jetpack_autoload_classmap.php' ) ) { + return false; + } + // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged + + return $directory; + } + + /** + * Fetches an array of normalized paths keyed by the constant they came from. + * + * @return string[] The normalized paths keyed by the constant. + */ + private function get_normalized_constants() { + $raw_constants = array( + // Order the constants from most-specific to least-specific. + 'WP_PLUGIN_DIR', + 'WPMU_PLUGIN_DIR', + 'WP_CONTENT_DIR', + 'ABSPATH', + ); + + $constants = array(); + foreach ( $raw_constants as $raw ) { + if ( ! defined( $raw ) ) { + continue; + } + + $path = wp_normalize_path( constant( $raw ) ); + if ( isset( $path ) ) { + $constants[ $raw ] = $path; + } + } + + return $constants; + } + + /** + * Indicates whether or not a path is absolute. + * + * @param string $path The path to check. + * + * @return bool True if the path is absolute, otherwise false. + */ + private function is_absolute_path( $path ) { + if ( 0 === strlen( $path ) || '.' === $path[0] ) { + return false; + } + + // Absolute paths on Windows may begin with a drive letter. + if ( preg_match( '/^[a-zA-Z]:[\/\\\\]/', $path ) ) { + return true; + } + + // A path starting with / or \ is absolute; anything else is relative. + return ( '/' === $path[0] || '\\' === $path[0] ); + } + + /** + * Given a file and a list of directories to check, this method will try to figure out + * the absolute path to the file in question. + * + * @param string $normalized_path The normalized path to the plugin or theme file to resolve. + * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. + * + * @return string|null The absolute path to the plugin file, otherwise null. + */ + private function find_absolute_plugin_path( $normalized_path, $directories_to_check ) { + // We're only able to find the absolute path for plugin/theme PHP files. + if ( ! is_string( $normalized_path ) || '.php' !== substr( $normalized_path, -4 ) ) { + return null; + } + + foreach ( $directories_to_check as $directory ) { + $normalized_check = wp_normalize_path( trailingslashit( $directory ) ) . $normalized_path; + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + if ( @is_file( $normalized_check ) ) { + return $normalized_check; + } + } + + return null; + } + + /** + * Given a path this will figure out the real path that we should be using. + * + * @param string $path The path to resolve. + * + * @return string The resolved path. + */ + private function get_real_path( $path ) { + // We want to resolve symbolic links for consistency with __DIR__ paths. + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + $real_path = @realpath( $path ); + if ( false === $real_path ) { + // Let the autoloader deal with paths that don't exist. + $real_path = $path; + } + + // Using realpath will make it platform-specific so we must normalize it after. + if ( $path !== $real_path ) { + $real_path = wp_normalize_path( $real_path ); + } + + return $real_path; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-php-autoloader.php b/vendor/automattic/jetpack-autoloader/src/class-php-autoloader.php new file mode 100644 index 0000000..98d0724 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-php-autoloader.php @@ -0,0 +1,82 @@ +unregister_autoloader(); + + // Set the global so that it can be used to load classes. + global $jetpack_autoloader_loader; + $jetpack_autoloader_loader = $version_loader; + + // Ensure that the autoloader is first to avoid contention with others. + spl_autoload_register( array( self::class, 'load_class' ), true, true ); + } + + /** + * Unregisters the active autoloader so that it will no longer autoload classes. + */ + public function unregister_autoloader() { + // Remove any v2 autoloader that we've already registered. + $autoload_chain = spl_autoload_functions(); + foreach ( $autoload_chain as $autoloader ) { + // We can identify a v2 autoloader using the namespace. + $namespace_check = null; + + // Functions are recorded as strings. + if ( is_string( $autoloader ) ) { + $namespace_check = $autoloader; + } elseif ( is_array( $autoloader ) && is_string( $autoloader[0] ) ) { + // Static method calls have the class as the first array element. + $namespace_check = $autoloader[0]; + } else { + // Since the autoloader has only ever been a function or a static method we don't currently need to check anything else. + continue; + } + + // Check for the namespace without the generated suffix. + if ( 'Automattic\\Jetpack\\Autoloader\\jp' === substr( $namespace_check, 0, 32 ) ) { + spl_autoload_unregister( $autoloader ); + } + } + + // Clear the global now that the autoloader has been unregistered. + global $jetpack_autoloader_loader; + $jetpack_autoloader_loader = null; + } + + /** + * Loads a class file if one could be found. + * + * Note: This function is static so that the autoloader can be easily unregistered. If + * it was a class method we would have to unwrap the object to check the namespace. + * + * @param string $class_name The name of the class to autoload. + * + * @return bool Indicates whether or not a class file was loaded. + */ + public static function load_class( $class_name ) { + global $jetpack_autoloader_loader; + if ( ! isset( $jetpack_autoloader_loader ) ) { + return; + } + + $file = $jetpack_autoloader_loader->find_class_file( $class_name ); + if ( ! isset( $file ) ) { + return false; + } + + require $file; + return true; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-plugin-locator.php b/vendor/automattic/jetpack-autoloader/src/class-plugin-locator.php new file mode 100644 index 0000000..dea37ce --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-plugin-locator.php @@ -0,0 +1,145 @@ +path_processor = $path_processor; + } + + /** + * Finds the path to the current plugin. + * + * @return string $path The path to the current plugin. + * + * @throws \RuntimeException If the current plugin does not have an autoloader. + */ + public function find_current_plugin() { + // Escape from `vendor/__DIR__` to root plugin directory. + $plugin_directory = dirname( dirname( __DIR__ ) ); + + // Use the path processor to ensure that this is an autoloader we're referencing. + $path = $this->path_processor->find_directory_with_autoloader( $plugin_directory, array() ); + if ( false === $path ) { + throw new \RuntimeException( 'Failed to locate plugin ' . $plugin_directory ); + } + + return $path; + } + + /** + * Checks a given option for plugin paths. + * + * @param string $option_name The option that we want to check for plugin information. + * @param bool $site_option Indicates whether or not we want to check the site option. + * + * @return array $plugin_paths The list of absolute paths we've found. + */ + public function find_using_option( $option_name, $site_option = false ) { + $raw = $site_option ? get_site_option( $option_name ) : get_option( $option_name ); + if ( false === $raw ) { + return array(); + } + + return $this->convert_plugins_to_paths( $raw ); + } + + /** + * Checks for plugins in the `action` request parameter. + * + * @param string[] $allowed_actions The actions that we're allowed to return plugins for. + * + * @return array $plugin_paths The list of absolute paths we've found. + */ + public function find_using_request_action( $allowed_actions ) { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + + /** + * Note: we're not actually checking the nonce here because it's too early + * in the execution. The pluggable functions are not yet loaded to give + * plugins a chance to plug their versions. Therefore we're doing the bare + * minimum: checking whether the nonce exists and it's in the right place. + * The request will fail later if the nonce doesn't pass the check. + */ + if ( empty( $_REQUEST['_wpnonce'] ) ) { + return array(); + } + + $action = isset( $_REQUEST['action'] ) ? wp_unslash( $_REQUEST['action'] ) : false; + if ( ! in_array( $action, $allowed_actions, true ) ) { + return array(); + } + + $plugin_slugs = array(); + switch ( $action ) { + case 'activate': + case 'deactivate': + if ( empty( $_REQUEST['plugin'] ) ) { + break; + } + + $plugin_slugs[] = wp_unslash( $_REQUEST['plugin'] ); + break; + + case 'activate-selected': + case 'deactivate-selected': + if ( empty( $_REQUEST['checked'] ) ) { + break; + } + + $plugin_slugs = wp_unslash( $_REQUEST['checked'] ); + break; + } + + // phpcs:enable WordPress.Security.NonceVerification.Recommended + return $this->convert_plugins_to_paths( $plugin_slugs ); + } + + /** + * Given an array of plugin slugs or paths, this will convert them to absolute paths and filter + * out the plugins that are not directory plugins. Note that array keys will also be included + * if they are plugin paths! + * + * @param string[] $plugins Plugin paths or slugs to filter. + * + * @return string[] + */ + private function convert_plugins_to_paths( $plugins ) { + if ( ! is_array( $plugins ) || empty( $plugins ) ) { + return array(); + } + + // We're going to look for plugins in the standard directories. + $path_constants = array( WP_PLUGIN_DIR, WPMU_PLUGIN_DIR ); + + $plugin_paths = array(); + foreach ( $plugins as $key => $value ) { + $path = $this->path_processor->find_directory_with_autoloader( $key, $path_constants ); + if ( $path ) { + $plugin_paths[] = $path; + } + + $path = $this->path_processor->find_directory_with_autoloader( $value, $path_constants ); + if ( $path ) { + $plugin_paths[] = $path; + } + } + + return $plugin_paths; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-plugins-handler.php b/vendor/automattic/jetpack-autoloader/src/class-plugins-handler.php new file mode 100644 index 0000000..dd00ac1 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-plugins-handler.php @@ -0,0 +1,156 @@ +plugin_locator = $plugin_locator; + $this->path_processor = $path_processor; + } + + /** + * Gets all of the active plugins we can find. + * + * @param bool $include_deactivating When true, plugins deactivating this request will be considered active. + * @param bool $record_unknown When true, the current plugin will be marked as active and recorded when unknown. + * + * @return string[] + */ + public function get_active_plugins( $include_deactivating, $record_unknown ) { + global $jetpack_autoloader_activating_plugins_paths; + + // We're going to build a unique list of plugins from a few different sources + // to find all of our "active" plugins. While we need to return an integer + // array, we're going to use an associative array internally to reduce + // the amount of time that we're going to spend checking uniqueness + // and merging different arrays together to form the output. + $active_plugins = array(); + + // Make sure that plugins which have activated this request are considered as "active" even though + // they probably won't be present in any option. + if ( is_array( $jetpack_autoloader_activating_plugins_paths ) ) { + foreach ( $jetpack_autoloader_activating_plugins_paths as $path ) { + $active_plugins[ $path ] = $path; + } + } + + // This option contains all of the plugins that have been activated. + $plugins = $this->plugin_locator->find_using_option( 'active_plugins' ); + foreach ( $plugins as $path ) { + $active_plugins[ $path ] = $path; + } + + // This option contains all of the multisite plugins that have been activated. + if ( is_multisite() ) { + $plugins = $this->plugin_locator->find_using_option( 'active_sitewide_plugins', true ); + foreach ( $plugins as $path ) { + $active_plugins[ $path ] = $path; + } + } + + // These actions contain plugins that are being activated/deactivated during this request. + $plugins = $this->plugin_locator->find_using_request_action( array( 'activate', 'activate-selected', 'deactivate', 'deactivate-selected' ) ); + foreach ( $plugins as $path ) { + $active_plugins[ $path ] = $path; + } + + // When the current plugin isn't considered "active" there's a problem. + // Since we're here, the plugin is active and currently being loaded. + // We can support this case (mu-plugins and non-standard activation) + // by adding the current plugin to the active list and marking it + // as an unknown (activating) plugin. This also has the benefit + // of causing a reset because the active plugins list has + // been changed since it was saved in the global. + $current_plugin = $this->plugin_locator->find_current_plugin(); + if ( $record_unknown && ! in_array( $current_plugin, $active_plugins, true ) ) { + $active_plugins[ $current_plugin ] = $current_plugin; + $jetpack_autoloader_activating_plugins_paths[] = $current_plugin; + } + + // When deactivating plugins aren't desired we should entirely remove them from the active list. + if ( ! $include_deactivating ) { + // These actions contain plugins that are being deactivated during this request. + $plugins = $this->plugin_locator->find_using_request_action( array( 'deactivate', 'deactivate-selected' ) ); + foreach ( $plugins as $path ) { + unset( $active_plugins[ $path ] ); + } + } + + // Transform the array so that we don't have to worry about the keys interacting with other array types later. + return array_values( $active_plugins ); + } + + /** + * Gets all of the cached plugins if there are any. + * + * @return string[] + */ + public function get_cached_plugins() { + $cached = get_transient( self::TRANSIENT_KEY ); + if ( ! is_array( $cached ) || empty( $cached ) ) { + return array(); + } + + // We need to expand the tokens to an absolute path for this webserver. + return array_map( array( $this->path_processor, 'untokenize_path_constants' ), $cached ); + } + + /** + * Saves the plugin list to the cache. + * + * @param array $plugins The plugin list to save to the cache. + */ + public function cache_plugins( $plugins ) { + // We store the paths in a tokenized form so that that webservers with different absolute paths don't break. + $plugins = array_map( array( $this->path_processor, 'tokenize_path_constants' ), $plugins ); + + set_transient( self::TRANSIENT_KEY, $plugins ); + } + + /** + * Checks to see whether or not the plugin list given has changed when compared to the + * shared `$jetpack_autoloader_cached_plugin_paths` global. This allows us to deal + * with cases where the active list may change due to filtering.. + * + * @param string[] $plugins The plugins list to check against the global cache. + * + * @return bool True if the plugins have changed, otherwise false. + */ + public function have_plugins_changed( $plugins ) { + global $jetpack_autoloader_cached_plugin_paths; + + if ( $jetpack_autoloader_cached_plugin_paths !== $plugins ) { + $jetpack_autoloader_cached_plugin_paths = $plugins; + return true; + } + + return false; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-shutdown-handler.php b/vendor/automattic/jetpack-autoloader/src/class-shutdown-handler.php new file mode 100644 index 0000000..198b19c --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-shutdown-handler.php @@ -0,0 +1,84 @@ +plugins_handler = $plugins_handler; + $this->cached_plugins = $cached_plugins; + $this->was_included_by_autoloader = $was_included_by_autoloader; + } + + /** + * Handles the shutdown of the autoloader. + */ + public function __invoke() { + // Don't save a broken cache if an error happens during some plugin's initialization. + if ( ! did_action( 'plugins_loaded' ) ) { + // Ensure that the cache is emptied to prevent consecutive failures if the cache is to blame. + if ( ! empty( $this->cached_plugins ) ) { + $this->plugins_handler->cache_plugins( array() ); + } + + return; + } + + // Load the active plugins fresh since the list we pulled earlier might not contain + // plugins that were activated but did not reset the autoloader. This happens + // when a plugin is in the cache but not "active" when the autoloader loads. + // We also want to make sure that plugins which are deactivating are not + // considered "active" so that they will be removed from the cache now. + try { + $active_plugins = $this->plugins_handler->get_active_plugins( false, ! $this->was_included_by_autoloader ); + } catch ( \Exception $ex ) { + // When the package is deleted before shutdown it will throw an exception. + // In the event this happens we should erase the cache. + if ( ! empty( $this->cached_plugins ) ) { + $this->plugins_handler->cache_plugins( array() ); + } + return; + } + + // The paths should be sorted for easy comparisons with those loaded from the cache. + // Note we don't need to sort the cached entries because they're already sorted. + sort( $active_plugins ); + + // We don't want to waste time saving a cache that hasn't changed. + if ( $this->cached_plugins === $active_plugins ) { + return; + } + + $this->plugins_handler->cache_plugins( $active_plugins ); + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-version-loader.php b/vendor/automattic/jetpack-autoloader/src/class-version-loader.php new file mode 100644 index 0000000..a1169ce --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-version-loader.php @@ -0,0 +1,156 @@ +version_selector = $version_selector; + $this->classmap = $classmap; + $this->psr4_map = $psr4_map; + $this->filemap = $filemap; + } + + /** + * Finds the file path for the given class. + * + * @param string $class_name The class to find. + * + * @return string|null $file_path The path to the file if found, null if no class was found. + */ + public function find_class_file( $class_name ) { + $data = $this->select_newest_file( + isset( $this->classmap[ $class_name ] ) ? $this->classmap[ $class_name ] : null, + $this->find_psr4_file( $class_name ) + ); + if ( ! isset( $data ) ) { + return null; + } + + return $data['path']; + } + + /** + * Load all of the files in the filemap. + */ + public function load_filemap() { + if ( empty( $this->filemap ) ) { + return; + } + + foreach ( $this->filemap as $file_identifier => $file_data ) { + if ( empty( $GLOBALS['__composer_autoload_files'][ $file_identifier ] ) ) { + require_once $file_data['path']; + + $GLOBALS['__composer_autoload_files'][ $file_identifier ] = true; + } + } + } + + /** + * Compares different class sources and returns the newest. + * + * @param array|null $classmap_data The classmap class data. + * @param array|null $psr4_data The PSR-4 class data. + * + * @return array|null $data + */ + private function select_newest_file( $classmap_data, $psr4_data ) { + if ( ! isset( $classmap_data ) ) { + return $psr4_data; + } elseif ( ! isset( $psr4_data ) ) { + return $classmap_data; + } + + if ( $this->version_selector->is_version_update_required( $classmap_data['version'], $psr4_data['version'] ) ) { + return $psr4_data; + } + + return $classmap_data; + } + + /** + * Finds the file for a given class in a PSR-4 namespace. + * + * @param string $class_name The class to find. + * + * @return array|null $data The version and path path to the file if found, null otherwise. + */ + private function find_psr4_file( $class_name ) { + if ( ! isset( $this->psr4_map ) ) { + return null; + } + + // Don't bother with classes that have no namespace. + $class_index = strrpos( $class_name, '\\' ); + if ( ! $class_index ) { + return null; + } + $class_for_path = str_replace( '\\', '/', $class_name ); + + // Search for the namespace by iteratively cutting off the last segment until + // we find a match. This allows us to check the most-specific namespaces + // first as well as minimize the amount of time spent looking. + for ( + $class_namespace = substr( $class_name, 0, $class_index ); + ! empty( $class_namespace ); + $class_namespace = substr( $class_namespace, 0, strrpos( $class_namespace, '\\' ) ) + ) { + $namespace = $class_namespace . '\\'; + if ( ! isset( $this->psr4_map[ $namespace ] ) ) { + continue; + } + $data = $this->psr4_map[ $namespace ]; + + foreach ( $data['path'] as $path ) { + $path .= '/' . substr( $class_for_path, strlen( $namespace ) ) . '.php'; + if ( file_exists( $path ) ) { + return array( + 'version' => $data['version'], + 'path' => $path, + ); + } + } + } + + return null; + } +} diff --git a/vendor/automattic/jetpack-autoloader/src/class-version-selector.php b/vendor/automattic/jetpack-autoloader/src/class-version-selector.php new file mode 100644 index 0000000..dc84667 --- /dev/null +++ b/vendor/automattic/jetpack-autoloader/src/class-version-selector.php @@ -0,0 +1,61 @@ +is_dev_version( $selected_version ) ) { + return false; + } + + if ( $this->is_dev_version( $compare_version ) ) { + if ( $use_dev_versions ) { + return true; + } else { + return false; + } + } + + if ( version_compare( $selected_version, $compare_version, '<' ) ) { + return true; + } + + return false; + } + + /** + * Checks whether the given package version is a development version. + * + * @param String $version The package version. + * + * @return bool True if the version is a dev version, else false. + */ + public function is_dev_version( $version ) { + if ( 'dev-' === substr( $version, 0, 4 ) || '9999999-dev' === $version ) { + return true; + } + + return false; + } +} diff --git a/vendor/automattic/jetpack-constants/src/class-constants.php b/vendor/automattic/jetpack-constants/src/class-constants.php new file mode 100644 index 0000000..d7bdbcb --- /dev/null +++ b/vendor/automattic/jetpack-constants/src/class-constants.php @@ -0,0 +1,124 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..fbb9387 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,821 @@ + $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadFileWriter.php', + 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php', + 'Automattic\\Jetpack\\Autoloader\\AutoloadProcessor' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadProcessor.php', + 'Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin' => $vendorDir . '/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php', + 'Automattic\\Jetpack\\Autoloader\\ManifestGenerator' => $vendorDir . '/automattic/jetpack-autoloader/src/ManifestGenerator.php', + 'Automattic\\Jetpack\\Constants' => $vendorDir . '/automattic/jetpack-constants/src/class-constants.php', + 'Automattic\\WooCommerce\\Admin\\API\\Coupons' => $baseDir . '/packages/woocommerce-admin/src/API/Coupons.php', + 'Automattic\\WooCommerce\\Admin\\API\\CustomAttributeTraits' => $baseDir . '/packages/woocommerce-admin/src/API/CustomAttributeTraits.php', + 'Automattic\\WooCommerce\\Admin\\API\\Customers' => $baseDir . '/packages/woocommerce-admin/src/API/Customers.php', + 'Automattic\\WooCommerce\\Admin\\API\\Data' => $baseDir . '/packages/woocommerce-admin/src/API/Data.php', + 'Automattic\\WooCommerce\\Admin\\API\\DataCountries' => $baseDir . '/packages/woocommerce-admin/src/API/DataCountries.php', + 'Automattic\\WooCommerce\\Admin\\API\\DataDownloadIPs' => $baseDir . '/packages/woocommerce-admin/src/API/DataDownloadIPs.php', + 'Automattic\\WooCommerce\\Admin\\API\\Features' => $baseDir . '/packages/woocommerce-admin/src/API/Features.php', + 'Automattic\\WooCommerce\\Admin\\API\\Init' => $baseDir . '/packages/woocommerce-admin/src/API/Init.php', + 'Automattic\\WooCommerce\\Admin\\API\\Leaderboards' => $baseDir . '/packages/woocommerce-admin/src/API/Leaderboards.php', + 'Automattic\\WooCommerce\\Admin\\API\\Marketing' => $baseDir . '/packages/woocommerce-admin/src/API/Marketing.php', + 'Automattic\\WooCommerce\\Admin\\API\\MarketingOverview' => $baseDir . '/packages/woocommerce-admin/src/API/MarketingOverview.php', + 'Automattic\\WooCommerce\\Admin\\API\\NavigationFavorites' => $baseDir . '/packages/woocommerce-admin/src/API/NavigationFavorites.php', + 'Automattic\\WooCommerce\\Admin\\API\\NoteActions' => $baseDir . '/packages/woocommerce-admin/src/API/NoteActions.php', + 'Automattic\\WooCommerce\\Admin\\API\\Notes' => $baseDir . '/packages/woocommerce-admin/src/API/Notes.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingFreeExtensions' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingFreeExtensions.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingPayments' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingPayments.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProductTypes' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingProductTypes.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProfile' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingProfile.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingTasks' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingTasks.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingThemes' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingThemes.php', + 'Automattic\\WooCommerce\\Admin\\API\\Options' => $baseDir . '/packages/woocommerce-admin/src/API/Options.php', + 'Automattic\\WooCommerce\\Admin\\API\\Orders' => $baseDir . '/packages/woocommerce-admin/src/API/Orders.php', + 'Automattic\\WooCommerce\\Admin\\API\\Plugins' => $baseDir . '/packages/woocommerce-admin/src/API/Plugins.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributeTerms' => $baseDir . '/packages/woocommerce-admin/src/API/ProductAttributeTerms.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributes' => $baseDir . '/packages/woocommerce-admin/src/API/ProductAttributes.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductCategories' => $baseDir . '/packages/woocommerce-admin/src/API/ProductCategories.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductReviews' => $baseDir . '/packages/woocommerce-admin/src/API/ProductReviews.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductVariations' => $baseDir . '/packages/woocommerce-admin/src/API/ProductVariations.php', + 'Automattic\\WooCommerce\\Admin\\API\\Products' => $baseDir . '/packages/woocommerce-admin/src/API/Products.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductsLowInStock' => $baseDir . '/packages/woocommerce-admin/src/API/ProductsLowInStock.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Cache' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Cache.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStoreInterface' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/DataStoreInterface.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Files\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Files/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Export\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Export/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableInterface' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ExportableInterface.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableTraits' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ExportableTraits.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Import\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Import/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ParameterException' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ParameterException.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\PerformanceIndicators\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/PerformanceIndicators/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Revenue/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Revenue/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\SqlQuery' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/SqlQuery.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\TimeInterval' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/TimeInterval.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Controller' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Query' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Segmenter' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\SettingOptions' => $baseDir . '/packages/woocommerce-admin/src/API/SettingOptions.php', + 'Automattic\\WooCommerce\\Admin\\API\\Taxes' => $baseDir . '/packages/woocommerce-admin/src/API/Taxes.php', + 'Automattic\\WooCommerce\\Admin\\API\\Themes' => $baseDir . '/packages/woocommerce-admin/src/API/Themes.php', + 'Automattic\\WooCommerce\\Admin\\CategoryLookup' => $baseDir . '/packages/woocommerce-admin/src/CategoryLookup.php', + 'Automattic\\WooCommerce\\Admin\\Composer\\Package' => $baseDir . '/packages/woocommerce-admin/src/Composer/Package.php', + 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\CurrentDateTimeProvider' => $baseDir . '/packages/woocommerce-admin/src/DateTimeProvider/CurrentDateTimeProvider.php', + 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\DateTimeProviderInterface' => $baseDir . '/packages/woocommerce-admin/src/DateTimeProvider/DateTimeProviderInterface.php', + 'Automattic\\WooCommerce\\Admin\\DeprecatedClassFacade' => $baseDir . '/packages/woocommerce-admin/src/DeprecatedClassFacade.php', + 'Automattic\\WooCommerce\\Admin\\Events' => $baseDir . '/packages/woocommerce-admin/src/Events.php', + 'Automattic\\WooCommerce\\Admin\\FeaturePlugin' => $baseDir . '/packages/woocommerce-admin/src/FeaturePlugin.php', + 'Automattic\\WooCommerce\\Admin\\Features\\ActivityPanels' => $baseDir . '/packages/woocommerce-admin/src/Features/ActivityPanels.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Analytics' => $baseDir . '/packages/woocommerce-admin/src/Features/Analytics.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Coupons' => $baseDir . '/packages/woocommerce-admin/src/Features/Coupons.php', + 'Automattic\\WooCommerce\\Admin\\Features\\CouponsMovedTrait' => $baseDir . '/packages/woocommerce-admin/src/Features/CouponsMovedTrait.php', + 'Automattic\\WooCommerce\\Admin\\Features\\CustomerEffortScoreTracks' => $baseDir . '/packages/woocommerce-admin/src/Features/CustomerEffortScoreTracks.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Features' => $baseDir . '/packages/woocommerce-admin/src/Features/Features.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Homescreen' => $baseDir . '/packages/woocommerce-admin/src/Features/Homescreen.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Marketing' => $baseDir . '/packages/woocommerce-admin/src/Features/Marketing.php', + 'Automattic\\WooCommerce\\Admin\\Features\\MobileAppBanner' => $baseDir . '/packages/woocommerce-admin/src/Features/MobileAppBanner.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\CoreMenu' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/CoreMenu.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Favorites' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Favorites.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Menu' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Menu.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Screen' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Screen.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Onboarding' => $baseDir . '/packages/woocommerce-admin/src/Features/Onboarding.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Task' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Task.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskList' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskList.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskLists' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskLists.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Appearance' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Appearance.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Marketing' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Payments' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Payments.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Products' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Products.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Purchase' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Purchase.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Shipping' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Shipping.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\StoreDetails' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/StoreDetails.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Tax' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Tax.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\WooCommercePayments' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/WooCommercePayments.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DataSourcePoller' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DataSourcePoller.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DefaultPaymentGateways' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\EvaluateSuggestion' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\PaymentGatewaysController' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DataSourcePoller' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DefaultFreeExtensions' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DefaultFreeExtensions.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\EvaluateExtension' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/EvaluateExtension.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteInboxNotifications' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteInboxNotifications.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Settings' => $baseDir . '/packages/woocommerce-admin/src/Features/Settings.php', + 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBanner' => $baseDir . '/packages/woocommerce-admin/src/Features/ShippingLabelBanner.php', + 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBannerDisplayRules' => $baseDir . '/packages/woocommerce-admin/src/Features/ShippingLabelBannerDisplayRules.php', + 'Automattic\\WooCommerce\\Admin\\Features\\TransientNotices' => $baseDir . '/packages/woocommerce-admin/src/Features/TransientNotices.php', + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\DataSourcePoller' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/DataSourcePoller.php', + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\Init' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\WCPaymentGatewayPreInstallWCPayPromotion' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/WCPaymentGatewayPreInstallWCPayPromotion.php', + 'Automattic\\WooCommerce\\Admin\\Install' => $baseDir . '/packages/woocommerce-admin/src/Install.php', + 'Automattic\\WooCommerce\\Admin\\Loader' => $baseDir . '/packages/woocommerce-admin/src/Loader.php', + 'Automattic\\WooCommerce\\Admin\\Marketing\\InstalledExtensions' => $baseDir . '/packages/woocommerce-admin/src/Marketing/InstalledExtensions.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\AddFirstProduct' => $baseDir . '/packages/woocommerce-admin/src/Notes/AddFirstProduct.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\AddingAndManangingProducts' => $baseDir . '/packages/woocommerce-admin/src/Notes/AddingAndManangingProducts.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\ChooseNiche' => $baseDir . '/packages/woocommerce-admin/src/Notes/ChooseNiche.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\ChoosingTheme' => $baseDir . '/packages/woocommerce-admin/src/Notes/ChoosingTheme.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\CouponPageMoved' => $baseDir . '/packages/woocommerce-admin/src/Notes/CouponPageMoved.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizeStoreWithBlocks' => $baseDir . '/packages/woocommerce-admin/src/Notes/CustomizeStoreWithBlocks.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizingProductCatalog' => $baseDir . '/packages/woocommerce-admin/src/Notes/CustomizingProductCatalog.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\DataStore' => $baseDir . '/packages/woocommerce-admin/src/Notes/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\DeactivatePlugin' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeactivatePlugin.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\DrawAttention' => $baseDir . '/packages/woocommerce-admin/src/Notes/DrawAttention.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\EUVATNumber' => $baseDir . '/packages/woocommerce-admin/src/Notes/EUVATNumber.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\EditProductsOnTheMove' => $baseDir . '/packages/woocommerce-admin/src/Notes/EditProductsOnTheMove.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\FilterByProductVariationsInReports' => $baseDir . '/packages/woocommerce-admin/src/Notes/FilterByProductVariationsInReports.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\FirstDownlaodableProduct' => $baseDir . '/packages/woocommerce-admin/src/Notes/FirstDownlaodableProduct.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\FirstProduct' => $baseDir . '/packages/woocommerce-admin/src/Notes/FirstProduct.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\GettingStartedInEcommerceWebinar' => $baseDir . '/packages/woocommerce-admin/src/Notes/GettingStartedInEcommerceWebinar.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\GivingFeedbackNotes' => $baseDir . '/packages/woocommerce-admin/src/Notes/GivingFeedbackNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstProductAndPayment' => $baseDir . '/packages/woocommerce-admin/src/Notes/InsightFirstProductAndPayment.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstSale' => $baseDir . '/packages/woocommerce-admin/src/Notes/InsightFirstSale.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\InstallJPAndWCSPlugins' => $baseDir . '/packages/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\LaunchChecklist' => $baseDir . '/packages/woocommerce-admin/src/Notes/LaunchChecklist.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\LearnMoreAboutVariableProducts' => $baseDir . '/packages/woocommerce-admin/src/Notes/LearnMoreAboutVariableProducts.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\ManageOrdersOnTheGo' => $baseDir . '/packages/woocommerce-admin/src/Notes/ManageOrdersOnTheGo.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\ManageStoreActivityFromHomeScreen' => $baseDir . '/packages/woocommerce-admin/src/Notes/ManageStoreActivityFromHomeScreen.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\Marketing' => $baseDir . '/packages/woocommerce-admin/src/Notes/Marketing.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MarketingJetpack' => $baseDir . '/packages/woocommerce-admin/src/Notes/MarketingJetpack.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\MerchantEmailNotifications' => $baseDir . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/MerchantEmailNotifications.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\NotificationEmail' => $baseDir . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/NotificationEmail.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MigrateFromShopify' => $baseDir . '/packages/woocommerce-admin/src/Notes/MigrateFromShopify.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MobileApp' => $baseDir . '/packages/woocommerce-admin/src/Notes/MobileApp.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedback' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationFeedback.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedbackFollowUp' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationFeedbackFollowUp.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationNudge' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationNudge.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NeedSomeInspiration' => $baseDir . '/packages/woocommerce-admin/src/Notes/NeedSomeInspiration.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NewSalesRecord' => $baseDir . '/packages/woocommerce-admin/src/Notes/NewSalesRecord.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\Note' => $baseDir . '/packages/woocommerce-admin/src/Notes/Note.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NoteTraits' => $baseDir . '/packages/woocommerce-admin/src/Notes/NoteTraits.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\Notes' => $baseDir . '/packages/woocommerce-admin/src/Notes/Notes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NotesUnavailableException' => $baseDir . '/packages/woocommerce-admin/src/Notes/NotesUnavailableException.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingPayments' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnboardingPayments.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingTraits' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnboardingTraits.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\OnlineClothingStore' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnlineClothingStore.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\OrderMilestones' => $baseDir . '/packages/woocommerce-admin/src/Notes/OrderMilestones.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\PerformanceOnMobile' => $baseDir . '/packages/woocommerce-admin/src/Notes/PerformanceOnMobile.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\PersonalizeStore' => $baseDir . '/packages/woocommerce-admin/src/Notes/PersonalizeStore.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\RealTimeOrderAlerts' => $baseDir . '/packages/woocommerce-admin/src/Notes/RealTimeOrderAlerts.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\SellingOnlineCourses' => $baseDir . '/packages/woocommerce-admin/src/Notes/SellingOnlineCourses.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\SetUpAdditionalPaymentTypes' => $baseDir . '/packages/woocommerce-admin/src/Notes/SetUpAdditionalPaymentTypes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\StartDropshippingBusiness' => $baseDir . '/packages/woocommerce-admin/src/Notes/StartDropshippingBusiness.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\TestCheckout' => $baseDir . '/packages/woocommerce-admin/src/Notes/TestCheckout.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\TrackingOptIn' => $baseDir . '/packages/woocommerce-admin/src/Notes/TrackingOptIn.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\UnsecuredReportFiles' => $baseDir . '/packages/woocommerce-admin/src/Notes/UnsecuredReportFiles.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Note' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Choose_Niche' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Coupon_Page_Moved' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Customize_Store_With_Blocks' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Deactivate_Plugin' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Draw_Attention' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_EU_VAT_Number' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Edit_Products_On_The_Move' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Facebook_Marketing_Expert' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_First_Product' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Giving_Feedback_Notes' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Insight_First_Sale' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Install_JP_And_WCS_Plugins' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Launch_Checklist' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Marketing' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Migrate_From_Shopify' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Mobile_App' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Need_Some_Inspiration' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_New_Sales_Record' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Email_Marketing' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Payments' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Online_Clothing_Store' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Order_Milestones' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Performance_On_Mobile' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Personalize_Store' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Real_Time_Order_Alerts' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Selling_Online_Courses' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Set_Up_Additional_Payment_Types' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Start_Dropshipping_Business' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Test_Checkout' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Tracking_Opt_In' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Payments' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Subscriptions' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Woo_Subscriptions_Notes' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WelcomeToWooCommerceForStoreUsers' => $baseDir . '/packages/woocommerce-admin/src/Notes/WelcomeToWooCommerceForStoreUsers.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommercePayments' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooCommercePayments.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommerceSubscriptions' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooCommerceSubscriptions.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WooSubscriptionsNotes' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooSubscriptionsNotes.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\Order' => $baseDir . '/packages/woocommerce-admin/src/Overrides/Order.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund' => $baseDir . '/packages/woocommerce-admin/src/Overrides/OrderRefund.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderTraits' => $baseDir . '/packages/woocommerce-admin/src/Overrides/OrderTraits.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgrader' => $baseDir . '/packages/woocommerce-admin/src/Overrides/ThemeUpgrader.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgraderSkin' => $baseDir . '/packages/woocommerce-admin/src/Overrides/ThemeUpgraderSkin.php', + 'Automattic\\WooCommerce\\Admin\\PageController' => $baseDir . '/packages/woocommerce-admin/src/PageController.php', + 'Automattic\\WooCommerce\\Admin\\PaymentPlugins' => $baseDir . '/packages/woocommerce-admin/src/PaymentPlugins.php', + 'Automattic\\WooCommerce\\Admin\\PluginsHelper' => $baseDir . '/packages/woocommerce-admin/src/PluginsHelper.php', + 'Automattic\\WooCommerce\\Admin\\PluginsInstaller' => $baseDir . '/packages/woocommerce-admin/src/PluginsInstaller.php', + 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProvider' => $baseDir . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProvider.php', + 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProviderInterface' => $baseDir . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProviderInterface.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationCountryRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationStateRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationStateRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ComparisonOperation' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ComparisonOperation.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\DataSourcePoller' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/DataSourcePoller.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluateAndGetStatus' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluateAndGetStatus.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluationLogger' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluationLogger.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\FailRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/FailRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\GetRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/GetRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\IsEcommerceRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/IsEcommerceRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NotRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NotRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NoteStatusRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NoteStatusRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OnboardingProfileRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OnboardingProfileRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OptionRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OptionRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrderCountRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrderCountRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrdersProvider' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrdersProvider.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PassRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PassRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginVersionRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginVersionRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginsActivatedRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginsActivatedRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ProductCountRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ProductCountRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishAfterTimeRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishAfterTimeRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishBeforeTimeRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishBeforeTimeRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RemoteInboxNotificationsEngine' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleEvaluator' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleEvaluator.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleProcessorInterface' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleProcessorInterface.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\SpecRunner' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/SpecRunner.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateSetupForProducts' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateSetupForProducts.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerInterface' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerInterface.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerService' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerService.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayColumn' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayColumn.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayFlatten' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayFlatten.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayKeys' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayKeys.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArraySearch' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArraySearch.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayValues' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayValues.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\Count' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/Count.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\DotNotation' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/DotNotation.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForProvider' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForProvider.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WooCommerceAdminUpdatedRuleProcessor' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WooCommerceAdminUpdatedRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\ReportCSVEmail' => $baseDir . '/packages/woocommerce-admin/src/ReportCSVEmail.php', + 'Automattic\\WooCommerce\\Admin\\ReportCSVExporter' => $baseDir . '/packages/woocommerce-admin/src/ReportCSVExporter.php', + 'Automattic\\WooCommerce\\Admin\\ReportExporter' => $baseDir . '/packages/woocommerce-admin/src/ReportExporter.php', + 'Automattic\\WooCommerce\\Admin\\ReportsSync' => $baseDir . '/packages/woocommerce-admin/src/ReportsSync.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\CustomersScheduler' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/CustomersScheduler.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportInterface' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/ImportInterface.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportScheduler' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/ImportScheduler.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\MailchimpScheduler' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/MailchimpScheduler.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\OrdersScheduler' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/OrdersScheduler.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\SchedulerTraits' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/SchedulerTraits.php', + 'Automattic\\WooCommerce\\Admin\\Survey' => $baseDir . '/packages/woocommerce-admin/src/Survey.php', + 'Automattic\\WooCommerce\\Admin\\WCAdminHelper' => $baseDir . '/packages/woocommerce-admin/src/WCAdminHelper.php', + 'Automattic\\WooCommerce\\Admin\\WCAdminSharedSettings' => $baseDir . '/packages/woocommerce-admin/src/WCAdminSharedSettings.php', + 'Automattic\\WooCommerce\\Autoloader' => $baseDir . '/src/Autoloader.php', + 'Automattic\\WooCommerce\\Blocks\\Assets' => $baseDir . '/packages/woocommerce-blocks/src/Assets.php', + 'Automattic\\WooCommerce\\Blocks\\AssetsController' => $baseDir . '/packages/woocommerce-blocks/src/AssetsController.php', + 'Automattic\\WooCommerce\\Blocks\\Assets\\Api' => $baseDir . '/packages/woocommerce-blocks/src/Assets/Api.php', + 'Automattic\\WooCommerce\\Blocks\\Assets\\AssetDataRegistry' => $baseDir . '/packages/woocommerce-blocks/src/Assets/AssetDataRegistry.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypesController' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypesController.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractBlock' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractBlock.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractDynamicBlock' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractProductGrid' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ActiveFilters' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllProducts' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AllProducts.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllReviews' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AtomicBlock' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AttributeFilter' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Cart' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/Cart.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\CartI2' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/CartI2.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Checkout' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/Checkout.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedCategory' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedProduct' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\HandpickedProducts' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\MiniCart' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\PriceFilter' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductBestSellers' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductBestSellers.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategories' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategories.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategory' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductNew' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductOnSale' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductOnSale.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductSearch' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTag' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTopRated' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductsByAttribute' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductsByAttribute.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByCategory' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByProduct' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\SingleProduct' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\StockFilter' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Bootstrap' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Bootstrap.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Package' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Package.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\CreateAccount' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\DraftOrders' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\Email\\CustomerNewAccount' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\ExtendRestApi' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\FeatureGating' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\GoogleAnalytics' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php', + 'Automattic\\WooCommerce\\Blocks\\InboxNotifications' => $baseDir . '/packages/woocommerce-blocks/src/InboxNotifications.php', + 'Automattic\\WooCommerce\\Blocks\\Installer' => $baseDir . '/packages/woocommerce-blocks/src/Installer.php', + 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationInterface' => $baseDir . '/packages/woocommerce-blocks/src/Integrations/IntegrationInterface.php', + 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationRegistry' => $baseDir . '/packages/woocommerce-blocks/src/Integrations/IntegrationRegistry.php', + 'Automattic\\WooCommerce\\Blocks\\Library' => $baseDir . '/packages/woocommerce-blocks/src/Library.php', + 'Automattic\\WooCommerce\\Blocks\\Package' => $baseDir . '/packages/woocommerce-blocks/src/Package.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Api' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Api.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\AbstractPaymentMethodType' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/AbstractPaymentMethodType.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\BankTransfer' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/BankTransfer.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\CashOnDelivery' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/CashOnDelivery.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Cheque' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/Cheque.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\PayPal' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/PayPal.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Stripe' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/Stripe.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentContext' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentContext.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodRegistry' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentMethodRegistry.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodTypeInterface' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentMethodTypeInterface.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentResult' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentResult.php', + 'Automattic\\WooCommerce\\Blocks\\Registry\\AbstractDependencyType' => $baseDir . '/packages/woocommerce-blocks/src/Registry/AbstractDependencyType.php', + 'Automattic\\WooCommerce\\Blocks\\Registry\\Container' => $baseDir . '/packages/woocommerce-blocks/src/Registry/Container.php', + 'Automattic\\WooCommerce\\Blocks\\Registry\\FactoryType' => $baseDir . '/packages/woocommerce-blocks/src/Registry/FactoryType.php', + 'Automattic\\WooCommerce\\Blocks\\Registry\\SharedType' => $baseDir . '/packages/woocommerce-blocks/src/Registry/SharedType.php', + 'Automattic\\WooCommerce\\Blocks\\RestApi' => $baseDir . '/packages/woocommerce-blocks/src/RestApi.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\CurrencyFormatter' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/CurrencyFormatter.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\DefaultFormatter' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/DefaultFormatter.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\FormatterInterface' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/FormatterInterface.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\HtmlFormatter' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/HtmlFormatter.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\MoneyFormatter' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/MoneyFormatter.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\RoutesController' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/RoutesController.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractCartRoute' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractCartRoute.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractRoute' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractRoute.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractTermsRoute' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractTermsRoute.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Batch' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Batch.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Cart' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Cart.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartAddItem' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartAddItem.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartApplyCoupon' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartApplyCoupon.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCoupons' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCoupons.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCouponsByCode' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCouponsByCode.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartExtensions' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartExtensions.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItems' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItems.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItemsByKey' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItemsByKey.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveCoupon' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveCoupon.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveItem' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveItem.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartSelectShippingRate' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartSelectShippingRate.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateCustomer' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateCustomer.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateItem' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateItem.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Checkout' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Checkout.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributeTerms' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributeTerms.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributes' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributes.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributesById' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributesById.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategories' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategories.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategoriesById' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategoriesById.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCollectionData' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCollectionData.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductReviews' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductReviews.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductTags' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductTags.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Products' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Products.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductsById' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductsById.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteInterface' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteInterface.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\SchemaController' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/SchemaController.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractAddressSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractAddressSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\BillingAddressSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/BillingAddressSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartCouponSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartCouponSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartExtensionsSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartExtensionsSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartFeeSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartFeeSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartItemSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartItemSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartShippingRateSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartShippingRateSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CheckoutSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CheckoutSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ErrorSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ErrorSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ImageAttachmentSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ImageAttachmentSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\OrderCouponSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/OrderCouponSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductAttributeSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductAttributeSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCategorySchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCategorySchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCollectionDataSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCollectionDataSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductReviewSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductReviewSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ShippingAddressSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ShippingAddressSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\TermSchema' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/TermSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\CartController' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/CartController.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\InvalidStockLevelsInCartException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/InvalidStockLevelsInCartException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NotPurchasableException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NotPurchasableException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NoticeHandler' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NoticeHandler.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OrderController' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OutOfStockException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OutOfStockException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\Pagination' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/Pagination.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\PartialOutOfStockException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/PartialOutOfStockException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQuery' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQuery.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQueryFilters' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQueryFilters.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\StockAvailabilityException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/StockAvailabilityException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\TooManyInCartException' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/TooManyInCartException.php', + 'Automattic\\WooCommerce\\Blocks\\Utils\\ArrayUtils' => $baseDir . '/packages/woocommerce-blocks/src/Utils/ArrayUtils.php', + 'Automattic\\WooCommerce\\Blocks\\Utils\\BlocksWpQuery' => $baseDir . '/packages/woocommerce-blocks/src/Utils/BlocksWpQuery.php', + 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStock' => $baseDir . '/src/Checkout/Helpers/ReserveStock.php', + 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStockException' => $baseDir . '/src/Checkout/Helpers/ReserveStockException.php', + 'Automattic\\WooCommerce\\Container' => $baseDir . '/src/Container.php', + 'Automattic\\WooCommerce\\Internal\\AssignDefaultCategory' => $baseDir . '/src/Internal/AssignDefaultCategory.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\AbstractServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/AbstractServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ContainerException' => $baseDir . '/src/Internal/DependencyManagement/ContainerException.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\Definition' => $baseDir . '/src/Internal/DependencyManagement/Definition.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ExtendedContainer' => $baseDir . '/src/Internal/DependencyManagement/ExtendedContainer.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\AssignDefaultCategoryServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\DownloadPermissionsAdjusterServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProductAttributesLookupServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProxiesServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\RestockRefundedItemsAdjusterServiceProvider' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DownloadPermissionsAdjuster' => $baseDir . '/src/Internal/DownloadPermissionsAdjuster.php', + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\DataRegenerator' => $baseDir . '/src/Internal/ProductAttributesLookup/DataRegenerator.php', + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\Filterer' => $baseDir . '/src/Internal/ProductAttributesLookup/Filterer.php', + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\LookupDataStore' => $baseDir . '/src/Internal/ProductAttributesLookup/LookupDataStore.php', + 'Automattic\\WooCommerce\\Internal\\RestApiUtil' => $baseDir . '/src/Internal/RestApiUtil.php', + 'Automattic\\WooCommerce\\Internal\\RestockRefundedItemsAdjuster' => $baseDir . '/src/Internal/RestockRefundedItemsAdjuster.php', + 'Automattic\\WooCommerce\\Internal\\WCCom\\ConnectionHelper' => $baseDir . '/src/Internal/WCCom/ConnectionHelper.php', + 'Automattic\\WooCommerce\\Packages' => $baseDir . '/src/Packages.php', + 'Automattic\\WooCommerce\\Proxies\\ActionsProxy' => $baseDir . '/src/Proxies/ActionsProxy.php', + 'Automattic\\WooCommerce\\Proxies\\LegacyProxy' => $baseDir . '/src/Proxies/LegacyProxy.php', + 'Automattic\\WooCommerce\\RestApi\\Package' => $baseDir . '/includes/rest-api/Package.php', + 'Automattic\\WooCommerce\\RestApi\\Server' => $baseDir . '/includes/rest-api/Server.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\AdminNotesHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CouponHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CustomerHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\OrderHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ProductHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\QueueHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\SettingsHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ShippingHelper' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php', + 'Automattic\\WooCommerce\\RestApi\\Utilities\\ImageAttachment' => $baseDir . '/includes/rest-api/Utilities/ImageAttachment.php', + 'Automattic\\WooCommerce\\RestApi\\Utilities\\SingletonTrait' => $baseDir . '/includes/rest-api/Utilities/SingletonTrait.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\CodeHacker' => $baseDir . '/tests/Tools/CodeHacking/CodeHacker.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\BypassFinalsHack' => $baseDir . '/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\CodeHack' => $baseDir . '/tests/Tools/CodeHacking/Hacks/CodeHack.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\FunctionsMockerHack' => $baseDir . '/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\StaticMockerHack' => $baseDir . '/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\DependencyManagement\\MockableLegacyProxy' => $baseDir . '/tests/Tools/DependencyManagement/MockableLegacyProxy.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\FakeQueue' => $baseDir . '/tests/Tools/FakeQueue.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\AssignDefaultCategoryTest' => $baseDir . '/tests/php/src/Internal/AssignDefaultCategoryTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\AbstractServiceProviderTest' => $baseDir . '/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithDependencies' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithDependencies.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithInjectionMethodArgumentWithoutTypeHint' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithInjectionMethodArgumentWithoutTypeHint.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithNonFinalInjectionMethod' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithNonFinalInjectionMethod.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithPrivateInjectionMethod' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateInjectionMethod.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithScalarInjectionMethodArgument' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithScalarInjectionMethodArgument.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\DependencyClass' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/DependencyClass.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExtendedContainerTest' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExtendedContainerTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DownloadPermissionsAdjusterTest' => $baseDir . '/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\DataRegeneratorTest' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\FiltererTest' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\LookupDataStoreTest' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\RestApiUtilTest' => $baseDir . '/tests/php/src/Internal/RestApiUtilTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\WCCom\\ConnectionHelperTest' => $baseDir . '/tests/php/src/Internal/WCCom/ConnectionHelperTest.php', + 'Automattic\\WooCommerce\\Tests\\Proxies\\ClassThatDependsOnLegacyCodeTest' => $baseDir . '/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php', + 'Automattic\\WooCommerce\\Tests\\Proxies\\ExampleClasses\\ClassThatDependsOnLegacyCode' => $baseDir . '/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php', + 'Automattic\\WooCommerce\\Tests\\Proxies\\LegacyProxyTest' => $baseDir . '/tests/php/src/Proxies/LegacyProxyTest.php', + 'Automattic\\WooCommerce\\Tests\\Proxies\\MockableLegacyProxyTest' => $baseDir . '/tests/php/src/Proxies/MockableLegacyProxyTest.php', + 'Automattic\\WooCommerce\\Tests\\Utilities\\ArrayUtilTest' => $baseDir . '/tests/php/src/Utilities/ArrayUtilTest.php', + 'Automattic\\WooCommerce\\Tests\\Utilities\\NumberUtilTest' => $baseDir . '/tests/php/src/Utilities/NumberUtilTest.php', + 'Automattic\\WooCommerce\\Tests\\Utilities\\StringUtilTest' => $baseDir . '/tests/php/src/Utilities/StringUtilTest.php', + 'Automattic\\WooCommerce\\Utilities\\ArrayUtil' => $baseDir . '/src/Utilities/ArrayUtil.php', + 'Automattic\\WooCommerce\\Utilities\\NumberUtil' => $baseDir . '/src/Utilities/NumberUtil.php', + 'Automattic\\WooCommerce\\Utilities\\StringUtil' => $baseDir . '/src/Utilities/StringUtil.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverInterface' => $baseDir . '/lib/packages/League/Container/Argument/ArgumentResolverInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverTrait' => $baseDir . '/lib/packages/League/Container/Argument/ArgumentResolverTrait.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassName' => $baseDir . '/lib/packages/League/Container/Argument/ClassName.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameInterface' => $baseDir . '/lib/packages/League/Container/Argument/ClassNameInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameWithOptionalValue' => $baseDir . '/lib/packages/League/Container/Argument/ClassNameWithOptionalValue.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgument' => $baseDir . '/lib/packages/League/Container/Argument/RawArgument.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgumentInterface' => $baseDir . '/lib/packages/League/Container/Argument/RawArgumentInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Container' => $baseDir . '/lib/packages/League/Container/Container.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareInterface' => $baseDir . '/lib/packages/League/Container/ContainerAwareInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareTrait' => $baseDir . '/lib/packages/League/Container/ContainerAwareTrait.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\Definition' => $baseDir . '/lib/packages/League/Container/Definition/Definition.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregate' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionAggregate.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregateInterface' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionInterface' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\ContainerException' => $baseDir . '/lib/packages/League/Container/Exception/ContainerException.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\NotFoundException' => $baseDir . '/lib/packages/League/Container/Exception/NotFoundException.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\Inflector' => $baseDir . '/lib/packages/League/Container/Inflector/Inflector.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregate' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorAggregate.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregateInterface' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorInterface' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ReflectionContainer' => $baseDir . '/lib/packages/League/Container/ReflectionContainer.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\AbstractServiceProvider' => $baseDir . '/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\BootableServiceProviderInterface' => $baseDir . '/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregate' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregate.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregateInterface' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderInterface' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderInterface.php', + 'Composer\\Installers\\AglInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AglInstaller.php', + 'Composer\\Installers\\AimeosInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AimeosInstaller.php', + 'Composer\\Installers\\AnnotateCmsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php', + 'Composer\\Installers\\AsgardInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AsgardInstaller.php', + 'Composer\\Installers\\AttogramInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/AttogramInstaller.php', + 'Composer\\Installers\\BaseInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BaseInstaller.php', + 'Composer\\Installers\\BitrixInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BitrixInstaller.php', + 'Composer\\Installers\\BonefishInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/BonefishInstaller.php', + 'Composer\\Installers\\CakePHPInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CakePHPInstaller.php', + 'Composer\\Installers\\ChefInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ChefInstaller.php', + 'Composer\\Installers\\CiviCrmInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CiviCrmInstaller.php', + 'Composer\\Installers\\ClanCatsFrameworkInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php', + 'Composer\\Installers\\CockpitInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CockpitInstaller.php', + 'Composer\\Installers\\CodeIgniterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php', + 'Composer\\Installers\\Concrete5Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Concrete5Installer.php', + 'Composer\\Installers\\CraftInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CraftInstaller.php', + 'Composer\\Installers\\CroogoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/CroogoInstaller.php', + 'Composer\\Installers\\DecibelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DecibelInstaller.php', + 'Composer\\Installers\\DframeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DframeInstaller.php', + 'Composer\\Installers\\DokuWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DokuWikiInstaller.php', + 'Composer\\Installers\\DolibarrInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DolibarrInstaller.php', + 'Composer\\Installers\\DrupalInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/DrupalInstaller.php', + 'Composer\\Installers\\ElggInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ElggInstaller.php', + 'Composer\\Installers\\EliasisInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/EliasisInstaller.php', + 'Composer\\Installers\\ExpressionEngineInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php', + 'Composer\\Installers\\EzPlatformInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/EzPlatformInstaller.php', + 'Composer\\Installers\\FuelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelInstaller.php', + 'Composer\\Installers\\FuelphpInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelphpInstaller.php', + 'Composer\\Installers\\GravInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/GravInstaller.php', + 'Composer\\Installers\\HuradInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/HuradInstaller.php', + 'Composer\\Installers\\ImageCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ImageCMSInstaller.php', + 'Composer\\Installers\\Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Installer.php', + 'Composer\\Installers\\ItopInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ItopInstaller.php', + 'Composer\\Installers\\JoomlaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/JoomlaInstaller.php', + 'Composer\\Installers\\KanboardInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KanboardInstaller.php', + 'Composer\\Installers\\KirbyInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KirbyInstaller.php', + 'Composer\\Installers\\KnownInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KnownInstaller.php', + 'Composer\\Installers\\KodiCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KodiCMSInstaller.php', + 'Composer\\Installers\\KohanaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/KohanaInstaller.php', + 'Composer\\Installers\\LanManagementSystemInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php', + 'Composer\\Installers\\LaravelInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LaravelInstaller.php', + 'Composer\\Installers\\LavaLiteInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LavaLiteInstaller.php', + 'Composer\\Installers\\LithiumInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/LithiumInstaller.php', + 'Composer\\Installers\\MODULEWorkInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php', + 'Composer\\Installers\\MODXEvoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MODXEvoInstaller.php', + 'Composer\\Installers\\MagentoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MagentoInstaller.php', + 'Composer\\Installers\\MajimaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MajimaInstaller.php', + 'Composer\\Installers\\MakoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MakoInstaller.php', + 'Composer\\Installers\\MantisBTInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MantisBTInstaller.php', + 'Composer\\Installers\\MauticInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MauticInstaller.php', + 'Composer\\Installers\\MayaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MayaInstaller.php', + 'Composer\\Installers\\MediaWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MediaWikiInstaller.php', + 'Composer\\Installers\\MiaoxingInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MiaoxingInstaller.php', + 'Composer\\Installers\\MicroweberInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MicroweberInstaller.php', + 'Composer\\Installers\\ModxInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ModxInstaller.php', + 'Composer\\Installers\\MoodleInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/MoodleInstaller.php', + 'Composer\\Installers\\OctoberInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OctoberInstaller.php', + 'Composer\\Installers\\OntoWikiInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OntoWikiInstaller.php', + 'Composer\\Installers\\OsclassInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OsclassInstaller.php', + 'Composer\\Installers\\OxidInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/OxidInstaller.php', + 'Composer\\Installers\\PPIInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PPIInstaller.php', + 'Composer\\Installers\\PantheonInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PantheonInstaller.php', + 'Composer\\Installers\\PhiftyInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PhiftyInstaller.php', + 'Composer\\Installers\\PhpBBInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PhpBBInstaller.php', + 'Composer\\Installers\\PimcoreInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PimcoreInstaller.php', + 'Composer\\Installers\\PiwikInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PiwikInstaller.php', + 'Composer\\Installers\\PlentymarketsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php', + 'Composer\\Installers\\Plugin' => $vendorDir . '/composer/installers/src/Composer/Installers/Plugin.php', + 'Composer\\Installers\\PortoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PortoInstaller.php', + 'Composer\\Installers\\PrestashopInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PrestashopInstaller.php', + 'Composer\\Installers\\ProcessWireInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ProcessWireInstaller.php', + 'Composer\\Installers\\PuppetInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PuppetInstaller.php', + 'Composer\\Installers\\PxcmsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/PxcmsInstaller.php', + 'Composer\\Installers\\RadPHPInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RadPHPInstaller.php', + 'Composer\\Installers\\ReIndexInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ReIndexInstaller.php', + 'Composer\\Installers\\Redaxo5Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Redaxo5Installer.php', + 'Composer\\Installers\\RedaxoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RedaxoInstaller.php', + 'Composer\\Installers\\RoundcubeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/RoundcubeInstaller.php', + 'Composer\\Installers\\SMFInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SMFInstaller.php', + 'Composer\\Installers\\ShopwareInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ShopwareInstaller.php', + 'Composer\\Installers\\SilverStripeInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SilverStripeInstaller.php', + 'Composer\\Installers\\SiteDirectInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SiteDirectInstaller.php', + 'Composer\\Installers\\StarbugInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/StarbugInstaller.php', + 'Composer\\Installers\\SyDESInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SyDESInstaller.php', + 'Composer\\Installers\\SyliusInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/SyliusInstaller.php', + 'Composer\\Installers\\Symfony1Installer' => $vendorDir . '/composer/installers/src/Composer/Installers/Symfony1Installer.php', + 'Composer\\Installers\\TYPO3CmsInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php', + 'Composer\\Installers\\TYPO3FlowInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php', + 'Composer\\Installers\\TaoInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TaoInstaller.php', + 'Composer\\Installers\\TastyIgniterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php', + 'Composer\\Installers\\TheliaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TheliaInstaller.php', + 'Composer\\Installers\\TuskInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/TuskInstaller.php', + 'Composer\\Installers\\UserFrostingInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/UserFrostingInstaller.php', + 'Composer\\Installers\\VanillaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/VanillaInstaller.php', + 'Composer\\Installers\\VgmcpInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/VgmcpInstaller.php', + 'Composer\\Installers\\WHMCSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WHMCSInstaller.php', + 'Composer\\Installers\\WinterInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WinterInstaller.php', + 'Composer\\Installers\\WolfCMSInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WolfCMSInstaller.php', + 'Composer\\Installers\\WordPressInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/WordPressInstaller.php', + 'Composer\\Installers\\YawikInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/YawikInstaller.php', + 'Composer\\Installers\\ZendInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ZendInstaller.php', + 'Composer\\Installers\\ZikulaInstaller' => $vendorDir . '/composer/installers/src/Composer/Installers/ZikulaInstaller.php', + 'MaxMind\\Db\\Reader' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader.php', + 'MaxMind\\Db\\Reader\\Decoder' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php', + 'MaxMind\\Db\\Reader\\InvalidDatabaseException' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php', + 'MaxMind\\Db\\Reader\\Metadata' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php', + 'MaxMind\\Db\\Reader\\Util' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php', + 'Pelago\\Emogrifier' => $vendorDir . '/pelago/emogrifier/src/Emogrifier.php', + 'Pelago\\Emogrifier\\CssInliner' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/CssInliner.php', + 'Pelago\\Emogrifier\\HtmlProcessor\\AbstractHtmlProcessor' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php', + 'Pelago\\Emogrifier\\HtmlProcessor\\CssToAttributeConverter' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php', + 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlNormalizer' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php', + 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlPruner' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlPruner.php', + 'Pelago\\Emogrifier\\Utilities\\ArrayIntersector' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/Utilities/ArrayIntersector.php', + 'Pelago\\Emogrifier\\Utilities\\CssConcatenator' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/Utilities/CssConcatenator.php', + 'Psr\\Container\\ContainerExceptionInterface' => $vendorDir . '/psr/container/src/ContainerExceptionInterface.php', + 'Psr\\Container\\ContainerInterface' => $vendorDir . '/psr/container/src/ContainerInterface.php', + 'Psr\\Container\\NotFoundExceptionInterface' => $vendorDir . '/psr/container/src/NotFoundExceptionInterface.php', + 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => $vendorDir . '/symfony/css-selector/CssSelectorConverter.php', + 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/css-selector/Exception/ExceptionInterface.php', + 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => $vendorDir . '/symfony/css-selector/Exception/ExpressionErrorException.php', + 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => $vendorDir . '/symfony/css-selector/Exception/InternalErrorException.php', + 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => $vendorDir . '/symfony/css-selector/Exception/ParseException.php', + 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => $vendorDir . '/symfony/css-selector/Exception/SyntaxErrorException.php', + 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => $vendorDir . '/symfony/css-selector/Node/AbstractNode.php', + 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => $vendorDir . '/symfony/css-selector/Node/AttributeNode.php', + 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => $vendorDir . '/symfony/css-selector/Node/ClassNode.php', + 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => $vendorDir . '/symfony/css-selector/Node/CombinedSelectorNode.php', + 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => $vendorDir . '/symfony/css-selector/Node/ElementNode.php', + 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => $vendorDir . '/symfony/css-selector/Node/FunctionNode.php', + 'Symfony\\Component\\CssSelector\\Node\\HashNode' => $vendorDir . '/symfony/css-selector/Node/HashNode.php', + 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => $vendorDir . '/symfony/css-selector/Node/NegationNode.php', + 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => $vendorDir . '/symfony/css-selector/Node/NodeInterface.php', + 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => $vendorDir . '/symfony/css-selector/Node/PseudoNode.php', + 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => $vendorDir . '/symfony/css-selector/Node/SelectorNode.php', + 'Symfony\\Component\\CssSelector\\Node\\Specificity' => $vendorDir . '/symfony/css-selector/Node/Specificity.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/CommentHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => $vendorDir . '/symfony/css-selector/Parser/Handler/HandlerInterface.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/HashHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/NumberHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/StringHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Parser' => $vendorDir . '/symfony/css-selector/Parser/Parser.php', + 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => $vendorDir . '/symfony/css-selector/Parser/ParserInterface.php', + 'Symfony\\Component\\CssSelector\\Parser\\Reader' => $vendorDir . '/symfony/css-selector/Parser/Reader.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ClassParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ElementParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/HashParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Token' => $vendorDir . '/symfony/css-selector/Parser/Token.php', + 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => $vendorDir . '/symfony/css-selector/Parser/TokenStream.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AbstractExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/CombinationExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => $vendorDir . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/FunctionExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/HtmlExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/NodeExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Translator' => $vendorDir . '/symfony/css-selector/XPath/Translator.php', + 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => $vendorDir . '/symfony/css-selector/XPath/TranslatorInterface.php', + 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => $vendorDir . '/symfony/css-selector/XPath/XPathExpr.php', + 'WC_REST_CRUD_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php', + 'WC_REST_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php', + 'WC_REST_Coupons_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php', + 'WC_REST_Coupons_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php', + 'WC_REST_Coupons_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php', + 'WC_REST_Customer_Downloads_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php', + 'WC_REST_Customer_Downloads_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php', + 'WC_REST_Customer_Downloads_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php', + 'WC_REST_Customers_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php', + 'WC_REST_Customers_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php', + 'WC_REST_Customers_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php', + 'WC_REST_Data_Continents_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php', + 'WC_REST_Data_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php', + 'WC_REST_Data_Countries_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php', + 'WC_REST_Data_Currencies_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php', + 'WC_REST_Network_Orders_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php', + 'WC_REST_Network_Orders_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php', + 'WC_REST_Order_Notes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php', + 'WC_REST_Order_Notes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php', + 'WC_REST_Order_Notes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php', + 'WC_REST_Order_Refunds_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php', + 'WC_REST_Order_Refunds_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php', + 'WC_REST_Order_Refunds_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php', + 'WC_REST_Orders_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php', + 'WC_REST_Orders_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php', + 'WC_REST_Orders_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php', + 'WC_REST_Payment_Gateways_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php', + 'WC_REST_Payment_Gateways_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php', + 'WC_REST_Posts_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php', + 'WC_REST_Product_Attribute_Terms_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php', + 'WC_REST_Product_Attribute_Terms_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php', + 'WC_REST_Product_Attribute_Terms_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php', + 'WC_REST_Product_Attributes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php', + 'WC_REST_Product_Attributes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php', + 'WC_REST_Product_Attributes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php', + 'WC_REST_Product_Categories_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php', + 'WC_REST_Product_Categories_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php', + 'WC_REST_Product_Categories_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php', + 'WC_REST_Product_Reviews_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php', + 'WC_REST_Product_Reviews_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php', + 'WC_REST_Product_Reviews_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php', + 'WC_REST_Product_Shipping_Classes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php', + 'WC_REST_Product_Shipping_Classes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php', + 'WC_REST_Product_Shipping_Classes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php', + 'WC_REST_Product_Tags_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php', + 'WC_REST_Product_Tags_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php', + 'WC_REST_Product_Tags_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php', + 'WC_REST_Product_Variations_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php', + 'WC_REST_Product_Variations_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php', + 'WC_REST_Products_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php', + 'WC_REST_Products_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php', + 'WC_REST_Products_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php', + 'WC_REST_Report_Coupons_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php', + 'WC_REST_Report_Customers_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php', + 'WC_REST_Report_Orders_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php', + 'WC_REST_Report_Products_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php', + 'WC_REST_Report_Reviews_Totals_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php', + 'WC_REST_Report_Sales_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php', + 'WC_REST_Report_Sales_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php', + 'WC_REST_Report_Sales_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php', + 'WC_REST_Report_Top_Sellers_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php', + 'WC_REST_Report_Top_Sellers_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php', + 'WC_REST_Report_Top_Sellers_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php', + 'WC_REST_Reports_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php', + 'WC_REST_Reports_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php', + 'WC_REST_Reports_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php', + 'WC_REST_Setting_Options_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php', + 'WC_REST_Setting_Options_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php', + 'WC_REST_Settings_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php', + 'WC_REST_Settings_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php', + 'WC_REST_Shipping_Methods_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php', + 'WC_REST_Shipping_Methods_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php', + 'WC_REST_Shipping_Zone_Locations_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php', + 'WC_REST_Shipping_Zone_Locations_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php', + 'WC_REST_Shipping_Zone_Methods_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php', + 'WC_REST_Shipping_Zone_Methods_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php', + 'WC_REST_Shipping_Zones_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php', + 'WC_REST_Shipping_Zones_Controller_Base' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php', + 'WC_REST_Shipping_Zones_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php', + 'WC_REST_System_Status_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php', + 'WC_REST_System_Status_Tools_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php', + 'WC_REST_System_Status_Tools_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php', + 'WC_REST_System_Status_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php', + 'WC_REST_Tax_Classes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php', + 'WC_REST_Tax_Classes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php', + 'WC_REST_Tax_Classes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php', + 'WC_REST_Taxes_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php', + 'WC_REST_Taxes_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php', + 'WC_REST_Taxes_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php', + 'WC_REST_Telemetry_Controller' => $baseDir . '/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php', + 'WC_REST_Terms_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php', + 'WC_REST_Webhook_Deliveries_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php', + 'WC_REST_Webhook_Deliveries_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php', + 'WC_REST_Webhooks_Controller' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php', + 'WC_REST_Webhooks_V1_Controller' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php', + 'WC_REST_Webhooks_V2_Controller' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..8fa4e87 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,10 @@ + array($baseDir . '/lib/packages'), +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..f005c24 --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,21 @@ + array($vendorDir . '/symfony/css-selector'), + 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), + 'Pelago\\' => array($vendorDir . '/pelago/emogrifier/src'), + 'MaxMind\\Db\\' => array($vendorDir . '/maxmind-db/reader/src/MaxMind/Db'), + 'Composer\\Installers\\' => array($vendorDir . '/composer/installers/src/Composer/Installers'), + 'Automattic\\WooCommerce\\Vendor\\' => array($baseDir . '/lib/packages'), + 'Automattic\\WooCommerce\\Tests\\' => array($baseDir . '/tests/php/src'), + 'Automattic\\WooCommerce\\Testing\\Tools\\' => array($baseDir . '/tests/Tools'), + 'Automattic\\WooCommerce\\Blocks\\' => array($baseDir . '/packages/woocommerce-blocks/src'), + 'Automattic\\WooCommerce\\Admin\\' => array($baseDir . '/packages/woocommerce-admin/src'), + 'Automattic\\WooCommerce\\' => array($baseDir . '/src'), + 'Automattic\\Jetpack\\Autoloader\\' => array($vendorDir . '/automattic/jetpack-autoloader/src'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..418d610 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,55 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit7fa27687a59114a5aec1ac3080434897::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..240380d --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,925 @@ + + array ( + 'Symfony\\Component\\CssSelector\\' => 30, + ), + 'P' => + array ( + 'Psr\\Container\\' => 14, + 'Pelago\\' => 7, + ), + 'M' => + array ( + 'MaxMind\\Db\\' => 11, + ), + 'C' => + array ( + 'Composer\\Installers\\' => 20, + ), + 'A' => + array ( + 'Automattic\\WooCommerce\\Vendor\\' => 30, + 'Automattic\\WooCommerce\\Tests\\' => 29, + 'Automattic\\WooCommerce\\Testing\\Tools\\' => 37, + 'Automattic\\WooCommerce\\Blocks\\' => 30, + 'Automattic\\WooCommerce\\Admin\\' => 29, + 'Automattic\\WooCommerce\\' => 23, + 'Automattic\\Jetpack\\Autoloader\\' => 30, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'Symfony\\Component\\CssSelector\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/css-selector', + ), + 'Psr\\Container\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/container/src', + ), + 'Pelago\\' => + array ( + 0 => __DIR__ . '/..' . '/pelago/emogrifier/src', + ), + 'MaxMind\\Db\\' => + array ( + 0 => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db', + ), + 'Composer\\Installers\\' => + array ( + 0 => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers', + ), + 'Automattic\\WooCommerce\\Vendor\\' => + array ( + 0 => __DIR__ . '/../..' . '/lib/packages', + ), + 'Automattic\\WooCommerce\\Tests\\' => + array ( + 0 => __DIR__ . '/../..' . '/tests/php/src', + ), + 'Automattic\\WooCommerce\\Testing\\Tools\\' => + array ( + 0 => __DIR__ . '/../..' . '/tests/Tools', + ), + 'Automattic\\WooCommerce\\Blocks\\' => + array ( + 0 => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src', + ), + 'Automattic\\WooCommerce\\Admin\\' => + array ( + 0 => __DIR__ . '/../..' . '/packages/woocommerce-admin/src', + ), + 'Automattic\\WooCommerce\\' => + array ( + 0 => __DIR__ . '/../..' . '/src', + ), + 'Automattic\\Jetpack\\Autoloader\\' => + array ( + 0 => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src', + ), + ); + + public static $prefixesPsr0 = array ( + 'A' => + array ( + 'Automattic\\WooCommerce\\Vendor\\' => + array ( + 0 => __DIR__ . '/../..' . '/lib/packages', + ), + ), + ); + + public static $classMap = array ( + 'Automattic\\Jetpack\\Autoloader\\AutoloadFileWriter' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/AutoloadFileWriter.php', + 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php', + 'Automattic\\Jetpack\\Autoloader\\AutoloadProcessor' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/AutoloadProcessor.php', + 'Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php', + 'Automattic\\Jetpack\\Autoloader\\ManifestGenerator' => __DIR__ . '/..' . '/automattic/jetpack-autoloader/src/ManifestGenerator.php', + 'Automattic\\Jetpack\\Constants' => __DIR__ . '/..' . '/automattic/jetpack-constants/src/class-constants.php', + 'Automattic\\WooCommerce\\Admin\\API\\Coupons' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Coupons.php', + 'Automattic\\WooCommerce\\Admin\\API\\CustomAttributeTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/CustomAttributeTraits.php', + 'Automattic\\WooCommerce\\Admin\\API\\Customers' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Customers.php', + 'Automattic\\WooCommerce\\Admin\\API\\Data' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Data.php', + 'Automattic\\WooCommerce\\Admin\\API\\DataCountries' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/DataCountries.php', + 'Automattic\\WooCommerce\\Admin\\API\\DataDownloadIPs' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/DataDownloadIPs.php', + 'Automattic\\WooCommerce\\Admin\\API\\Features' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Features.php', + 'Automattic\\WooCommerce\\Admin\\API\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Init.php', + 'Automattic\\WooCommerce\\Admin\\API\\Leaderboards' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Leaderboards.php', + 'Automattic\\WooCommerce\\Admin\\API\\Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Marketing.php', + 'Automattic\\WooCommerce\\Admin\\API\\MarketingOverview' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/MarketingOverview.php', + 'Automattic\\WooCommerce\\Admin\\API\\NavigationFavorites' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/NavigationFavorites.php', + 'Automattic\\WooCommerce\\Admin\\API\\NoteActions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/NoteActions.php', + 'Automattic\\WooCommerce\\Admin\\API\\Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Notes.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingFreeExtensions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingFreeExtensions.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingPayments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingPayments.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProductTypes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingProductTypes.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProfile' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingProfile.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingTasks' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingTasks.php', + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingThemes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/OnboardingThemes.php', + 'Automattic\\WooCommerce\\Admin\\API\\Options' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Options.php', + 'Automattic\\WooCommerce\\Admin\\API\\Orders' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Orders.php', + 'Automattic\\WooCommerce\\Admin\\API\\Plugins' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Plugins.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributeTerms' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductAttributeTerms.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductAttributes.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductCategories' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductCategories.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductReviews' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductReviews.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductVariations' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductVariations.php', + 'Automattic\\WooCommerce\\Admin\\API\\Products' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Products.php', + 'Automattic\\WooCommerce\\Admin\\API\\ProductsLowInStock' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/ProductsLowInStock.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Cache' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Cache.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Categories/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Categories/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Categories/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStoreInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/DataStoreInterface.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Files\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Files/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Export\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Export/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/ExportableInterface.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/ExportableTraits.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Import\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Import/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ParameterException' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/ParameterException.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\PerformanceIndicators\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/PerformanceIndicators/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Revenue/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Revenue/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\SqlQuery' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/SqlQuery.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Stock/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\TimeInterval' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/TimeInterval.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Controller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Controller.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Query' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Query.php', + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Segmenter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Segmenter.php', + 'Automattic\\WooCommerce\\Admin\\API\\SettingOptions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/SettingOptions.php', + 'Automattic\\WooCommerce\\Admin\\API\\Taxes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Taxes.php', + 'Automattic\\WooCommerce\\Admin\\API\\Themes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/API/Themes.php', + 'Automattic\\WooCommerce\\Admin\\CategoryLookup' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/CategoryLookup.php', + 'Automattic\\WooCommerce\\Admin\\Composer\\Package' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Composer/Package.php', + 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\CurrentDateTimeProvider' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/DateTimeProvider/CurrentDateTimeProvider.php', + 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\DateTimeProviderInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/DateTimeProvider/DateTimeProviderInterface.php', + 'Automattic\\WooCommerce\\Admin\\DeprecatedClassFacade' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/DeprecatedClassFacade.php', + 'Automattic\\WooCommerce\\Admin\\Events' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Events.php', + 'Automattic\\WooCommerce\\Admin\\FeaturePlugin' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/FeaturePlugin.php', + 'Automattic\\WooCommerce\\Admin\\Features\\ActivityPanels' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/ActivityPanels.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Analytics' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Analytics.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Coupons' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Coupons.php', + 'Automattic\\WooCommerce\\Admin\\Features\\CouponsMovedTrait' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/CouponsMovedTrait.php', + 'Automattic\\WooCommerce\\Admin\\Features\\CustomerEffortScoreTracks' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/CustomerEffortScoreTracks.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Features' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Features.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Homescreen' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Homescreen.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Marketing.php', + 'Automattic\\WooCommerce\\Admin\\Features\\MobileAppBanner' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/MobileAppBanner.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\CoreMenu' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/CoreMenu.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Favorites' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/Favorites.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Menu' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/Menu.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Screen' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Navigation/Screen.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Onboarding' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Onboarding.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Task' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Task.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskList' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskList.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskLists' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskLists.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Appearance' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Appearance.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Payments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Payments.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Products' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Products.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Purchase' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Purchase.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Shipping' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Shipping.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\StoreDetails' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/StoreDetails.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Tax' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Tax.php', + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\WooCommercePayments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/WooCommercePayments.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DataSourcePoller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DataSourcePoller.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DefaultPaymentGateways' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\EvaluateSuggestion' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\PaymentGatewaysController' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DataSourcePoller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DefaultFreeExtensions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DefaultFreeExtensions.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\EvaluateExtension' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/EvaluateExtension.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteInboxNotifications' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/RemoteInboxNotifications.php', + 'Automattic\\WooCommerce\\Admin\\Features\\Settings' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/Settings.php', + 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBanner' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/ShippingLabelBanner.php', + 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBannerDisplayRules' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/ShippingLabelBannerDisplayRules.php', + 'Automattic\\WooCommerce\\Admin\\Features\\TransientNotices' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/TransientNotices.php', + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\DataSourcePoller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/WcPayPromotion/DataSourcePoller.php', + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\Init' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/WcPayPromotion/Init.php', + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\WCPaymentGatewayPreInstallWCPayPromotion' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Features/WcPayPromotion/WCPaymentGatewayPreInstallWCPayPromotion.php', + 'Automattic\\WooCommerce\\Admin\\Install' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Install.php', + 'Automattic\\WooCommerce\\Admin\\Loader' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Loader.php', + 'Automattic\\WooCommerce\\Admin\\Marketing\\InstalledExtensions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Marketing/InstalledExtensions.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\AddFirstProduct' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/AddFirstProduct.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\AddingAndManangingProducts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/AddingAndManangingProducts.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\ChooseNiche' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/ChooseNiche.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\ChoosingTheme' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/ChoosingTheme.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\CouponPageMoved' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/CouponPageMoved.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizeStoreWithBlocks' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/CustomizeStoreWithBlocks.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizingProductCatalog' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/CustomizingProductCatalog.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\DataStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DataStore.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\DeactivatePlugin' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeactivatePlugin.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\DrawAttention' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DrawAttention.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\EUVATNumber' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/EUVATNumber.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\EditProductsOnTheMove' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/EditProductsOnTheMove.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\FilterByProductVariationsInReports' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/FilterByProductVariationsInReports.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\FirstDownlaodableProduct' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/FirstDownlaodableProduct.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\FirstProduct' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/FirstProduct.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\GettingStartedInEcommerceWebinar' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/GettingStartedInEcommerceWebinar.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\GivingFeedbackNotes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/GivingFeedbackNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstProductAndPayment' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/InsightFirstProductAndPayment.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstSale' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/InsightFirstSale.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\InstallJPAndWCSPlugins' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\LaunchChecklist' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/LaunchChecklist.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\LearnMoreAboutVariableProducts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/LearnMoreAboutVariableProducts.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\ManageOrdersOnTheGo' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/ManageOrdersOnTheGo.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\ManageStoreActivityFromHomeScreen' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/ManageStoreActivityFromHomeScreen.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/Marketing.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MarketingJetpack' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MarketingJetpack.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\MerchantEmailNotifications' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/MerchantEmailNotifications.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\NotificationEmail' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/NotificationEmail.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MigrateFromShopify' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MigrateFromShopify.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\MobileApp' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/MobileApp.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedback' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NavigationFeedback.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedbackFollowUp' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NavigationFeedbackFollowUp.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationNudge' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NavigationNudge.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NeedSomeInspiration' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NeedSomeInspiration.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NewSalesRecord' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NewSalesRecord.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\Note' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/Note.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NoteTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NoteTraits.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/Notes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\NotesUnavailableException' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/NotesUnavailableException.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingPayments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/OnboardingPayments.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/OnboardingTraits.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\OnlineClothingStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/OnlineClothingStore.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\OrderMilestones' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/OrderMilestones.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\PerformanceOnMobile' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/PerformanceOnMobile.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\PersonalizeStore' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/PersonalizeStore.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\RealTimeOrderAlerts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/RealTimeOrderAlerts.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\SellingOnlineCourses' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/SellingOnlineCourses.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\SetUpAdditionalPaymentTypes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/SetUpAdditionalPaymentTypes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\StartDropshippingBusiness' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/StartDropshippingBusiness.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\TestCheckout' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/TestCheckout.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\TrackingOptIn' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/TrackingOptIn.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\UnsecuredReportFiles' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/UnsecuredReportFiles.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Note' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Choose_Niche' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Coupon_Page_Moved' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Customize_Store_With_Blocks' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Deactivate_Plugin' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Draw_Attention' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_EU_VAT_Number' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Edit_Products_On_The_Move' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Facebook_Marketing_Expert' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_First_Product' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Giving_Feedback_Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Insight_First_Sale' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Install_JP_And_WCS_Plugins' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Launch_Checklist' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Migrate_From_Shopify' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Mobile_App' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Need_Some_Inspiration' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_New_Sales_Record' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Email_Marketing' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Payments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Online_Clothing_Store' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Order_Milestones' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Performance_On_Mobile' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Personalize_Store' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Real_Time_Order_Alerts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Selling_Online_Courses' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Set_Up_Additional_Payment_Types' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Start_Dropshipping_Business' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Test_Checkout' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Tracking_Opt_In' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Payments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Subscriptions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Woo_Subscriptions_Notes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WelcomeToWooCommerceForStoreUsers' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/WelcomeToWooCommerceForStoreUsers.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommercePayments' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/WooCommercePayments.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommerceSubscriptions' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/WooCommerceSubscriptions.php', + 'Automattic\\WooCommerce\\Admin\\Notes\\WooSubscriptionsNotes' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Notes/WooSubscriptionsNotes.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\Order' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/Order.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/OrderRefund.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/OrderTraits.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgrader' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/ThemeUpgrader.php', + 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgraderSkin' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Overrides/ThemeUpgraderSkin.php', + 'Automattic\\WooCommerce\\Admin\\PageController' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PageController.php', + 'Automattic\\WooCommerce\\Admin\\PaymentPlugins' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PaymentPlugins.php', + 'Automattic\\WooCommerce\\Admin\\PluginsHelper' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PluginsHelper.php', + 'Automattic\\WooCommerce\\Admin\\PluginsInstaller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PluginsInstaller.php', + 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProvider' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProvider.php', + 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProviderInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProviderInterface.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationCountryRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationStateRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationStateRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ComparisonOperation' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ComparisonOperation.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\DataSourcePoller' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/DataSourcePoller.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluateAndGetStatus' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluateAndGetStatus.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluationLogger' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluationLogger.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\FailRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/FailRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\GetRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/GetRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\IsEcommerceRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/IsEcommerceRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NotRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NotRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NoteStatusRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NoteStatusRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OnboardingProfileRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OnboardingProfileRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OptionRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OptionRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrderCountRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrderCountRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrdersProvider' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrdersProvider.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PassRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PassRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginVersionRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginVersionRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginsActivatedRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginsActivatedRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ProductCountRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ProductCountRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishAfterTimeRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishAfterTimeRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishBeforeTimeRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishBeforeTimeRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RemoteInboxNotificationsEngine' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleEvaluator' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleEvaluator.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleProcessorInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleProcessorInterface.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\SpecRunner' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/SpecRunner.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateSetupForProducts' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateSetupForProducts.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerInterface.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerService' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerService.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayColumn' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayColumn.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayFlatten' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayFlatten.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayKeys' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayKeys.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArraySearch' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArraySearch.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayValues' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayValues.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\Count' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/Count.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\DotNotation' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/DotNotation.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForProvider' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForProvider.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WooCommerceAdminUpdatedRuleProcessor' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WooCommerceAdminUpdatedRuleProcessor.php', + 'Automattic\\WooCommerce\\Admin\\ReportCSVEmail' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/ReportCSVEmail.php', + 'Automattic\\WooCommerce\\Admin\\ReportCSVExporter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/ReportCSVExporter.php', + 'Automattic\\WooCommerce\\Admin\\ReportExporter' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/ReportExporter.php', + 'Automattic\\WooCommerce\\Admin\\ReportsSync' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/ReportsSync.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\CustomersScheduler' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/CustomersScheduler.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportInterface' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/ImportInterface.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportScheduler' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/ImportScheduler.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\MailchimpScheduler' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/MailchimpScheduler.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\OrdersScheduler' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/OrdersScheduler.php', + 'Automattic\\WooCommerce\\Admin\\Schedulers\\SchedulerTraits' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Schedulers/SchedulerTraits.php', + 'Automattic\\WooCommerce\\Admin\\Survey' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/Survey.php', + 'Automattic\\WooCommerce\\Admin\\WCAdminHelper' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/WCAdminHelper.php', + 'Automattic\\WooCommerce\\Admin\\WCAdminSharedSettings' => __DIR__ . '/../..' . '/packages/woocommerce-admin/src/WCAdminSharedSettings.php', + 'Automattic\\WooCommerce\\Autoloader' => __DIR__ . '/../..' . '/src/Autoloader.php', + 'Automattic\\WooCommerce\\Blocks\\Assets' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Assets.php', + 'Automattic\\WooCommerce\\Blocks\\AssetsController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/AssetsController.php', + 'Automattic\\WooCommerce\\Blocks\\Assets\\Api' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Assets/Api.php', + 'Automattic\\WooCommerce\\Blocks\\Assets\\AssetDataRegistry' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Assets/AssetDataRegistry.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypesController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypesController.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractBlock' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AbstractBlock.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractDynamicBlock' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractProductGrid' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ActiveFilters' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllProducts' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AllProducts.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllReviews' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AtomicBlock' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AttributeFilter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Cart' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/Cart.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\CartI2' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/CartI2.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Checkout' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/Checkout.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedCategory' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedProduct' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\HandpickedProducts' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\MiniCart' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\PriceFilter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductBestSellers' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductBestSellers.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategories' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategories.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategory' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductNew' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductOnSale' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductOnSale.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductSearch' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTag' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTopRated' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductsByAttribute' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ProductsByAttribute.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByCategory' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByProduct' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\SingleProduct' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php', + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\StockFilter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Bootstrap' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Bootstrap.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Package' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Package.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\CreateAccount' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\DraftOrders' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\Email\\CustomerNewAccount' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\ExtendRestApi' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\FeatureGating' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php', + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\GoogleAnalytics' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php', + 'Automattic\\WooCommerce\\Blocks\\InboxNotifications' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/InboxNotifications.php', + 'Automattic\\WooCommerce\\Blocks\\Installer' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Installer.php', + 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationInterface' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Integrations/IntegrationInterface.php', + 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationRegistry' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Integrations/IntegrationRegistry.php', + 'Automattic\\WooCommerce\\Blocks\\Library' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Library.php', + 'Automattic\\WooCommerce\\Blocks\\Package' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Package.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Api' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Api.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\AbstractPaymentMethodType' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/AbstractPaymentMethodType.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\BankTransfer' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/BankTransfer.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\CashOnDelivery' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/CashOnDelivery.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Cheque' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/Cheque.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\PayPal' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/PayPal.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Stripe' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/Integrations/Stripe.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentContext' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/PaymentContext.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodRegistry' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/PaymentMethodRegistry.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodTypeInterface' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/PaymentMethodTypeInterface.php', + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentResult' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Payments/PaymentResult.php', + 'Automattic\\WooCommerce\\Blocks\\Registry\\AbstractDependencyType' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Registry/AbstractDependencyType.php', + 'Automattic\\WooCommerce\\Blocks\\Registry\\Container' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Registry/Container.php', + 'Automattic\\WooCommerce\\Blocks\\Registry\\FactoryType' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Registry/FactoryType.php', + 'Automattic\\WooCommerce\\Blocks\\Registry\\SharedType' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Registry/SharedType.php', + 'Automattic\\WooCommerce\\Blocks\\RestApi' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/RestApi.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\CurrencyFormatter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/CurrencyFormatter.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\DefaultFormatter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/DefaultFormatter.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\FormatterInterface' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/FormatterInterface.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\HtmlFormatter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/HtmlFormatter.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\MoneyFormatter' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Formatters/MoneyFormatter.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\RoutesController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/RoutesController.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractCartRoute' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractCartRoute.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractRoute' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractRoute.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractTermsRoute' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractTermsRoute.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Batch' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/Batch.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Cart' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/Cart.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartAddItem' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartAddItem.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartApplyCoupon' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartApplyCoupon.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCoupons' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCoupons.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCouponsByCode' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCouponsByCode.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartExtensions' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartExtensions.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItems' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItems.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItemsByKey' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItemsByKey.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveCoupon' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveCoupon.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveItem' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveItem.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartSelectShippingRate' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartSelectShippingRate.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateCustomer' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateCustomer.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateItem' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateItem.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Checkout' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/Checkout.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributeTerms' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributeTerms.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributes' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributes.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributesById' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributesById.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategories' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategories.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategoriesById' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategoriesById.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCollectionData' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCollectionData.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductReviews' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductReviews.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductTags' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductTags.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Products' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/Products.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductsById' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductsById.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteInterface' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteInterface.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\SchemaController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/SchemaController.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractAddressSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractAddressSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\BillingAddressSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/BillingAddressSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartCouponSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartCouponSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartExtensionsSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartExtensionsSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartFeeSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartFeeSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartItemSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartItemSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartShippingRateSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartShippingRateSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CheckoutSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CheckoutSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ErrorSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ErrorSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ImageAttachmentSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ImageAttachmentSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\OrderCouponSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/OrderCouponSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductAttributeSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductAttributeSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCategorySchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCategorySchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCollectionDataSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCollectionDataSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductReviewSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductReviewSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ShippingAddressSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ShippingAddressSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\TermSchema' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Schemas/TermSchema.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\CartController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/CartController.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\InvalidStockLevelsInCartException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/InvalidStockLevelsInCartException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NotPurchasableException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NotPurchasableException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NoticeHandler' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NoticeHandler.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OrderController' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OutOfStockException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OutOfStockException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\Pagination' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/Pagination.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\PartialOutOfStockException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/PartialOutOfStockException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQuery' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQuery.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQueryFilters' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQueryFilters.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\StockAvailabilityException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/StockAvailabilityException.php', + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\TooManyInCartException' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/StoreApi/Utilities/TooManyInCartException.php', + 'Automattic\\WooCommerce\\Blocks\\Utils\\ArrayUtils' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Utils/ArrayUtils.php', + 'Automattic\\WooCommerce\\Blocks\\Utils\\BlocksWpQuery' => __DIR__ . '/../..' . '/packages/woocommerce-blocks/src/Utils/BlocksWpQuery.php', + 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStock' => __DIR__ . '/../..' . '/src/Checkout/Helpers/ReserveStock.php', + 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStockException' => __DIR__ . '/../..' . '/src/Checkout/Helpers/ReserveStockException.php', + 'Automattic\\WooCommerce\\Container' => __DIR__ . '/../..' . '/src/Container.php', + 'Automattic\\WooCommerce\\Internal\\AssignDefaultCategory' => __DIR__ . '/../..' . '/src/Internal/AssignDefaultCategory.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\AbstractServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/AbstractServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ContainerException' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ContainerException.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\Definition' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/Definition.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ExtendedContainer' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ExtendedContainer.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\AssignDefaultCategoryServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\DownloadPermissionsAdjusterServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProductAttributesLookupServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProxiesServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\RestockRefundedItemsAdjusterServiceProvider' => __DIR__ . '/../..' . '/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php', + 'Automattic\\WooCommerce\\Internal\\DownloadPermissionsAdjuster' => __DIR__ . '/../..' . '/src/Internal/DownloadPermissionsAdjuster.php', + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\DataRegenerator' => __DIR__ . '/../..' . '/src/Internal/ProductAttributesLookup/DataRegenerator.php', + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\Filterer' => __DIR__ . '/../..' . '/src/Internal/ProductAttributesLookup/Filterer.php', + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\LookupDataStore' => __DIR__ . '/../..' . '/src/Internal/ProductAttributesLookup/LookupDataStore.php', + 'Automattic\\WooCommerce\\Internal\\RestApiUtil' => __DIR__ . '/../..' . '/src/Internal/RestApiUtil.php', + 'Automattic\\WooCommerce\\Internal\\RestockRefundedItemsAdjuster' => __DIR__ . '/../..' . '/src/Internal/RestockRefundedItemsAdjuster.php', + 'Automattic\\WooCommerce\\Internal\\WCCom\\ConnectionHelper' => __DIR__ . '/../..' . '/src/Internal/WCCom/ConnectionHelper.php', + 'Automattic\\WooCommerce\\Packages' => __DIR__ . '/../..' . '/src/Packages.php', + 'Automattic\\WooCommerce\\Proxies\\ActionsProxy' => __DIR__ . '/../..' . '/src/Proxies/ActionsProxy.php', + 'Automattic\\WooCommerce\\Proxies\\LegacyProxy' => __DIR__ . '/../..' . '/src/Proxies/LegacyProxy.php', + 'Automattic\\WooCommerce\\RestApi\\Package' => __DIR__ . '/../..' . '/includes/rest-api/Package.php', + 'Automattic\\WooCommerce\\RestApi\\Server' => __DIR__ . '/../..' . '/includes/rest-api/Server.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\AdminNotesHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CouponHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CustomerHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\OrderHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ProductHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\QueueHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\SettingsHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php', + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ShippingHelper' => __DIR__ . '/../..' . '/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php', + 'Automattic\\WooCommerce\\RestApi\\Utilities\\ImageAttachment' => __DIR__ . '/../..' . '/includes/rest-api/Utilities/ImageAttachment.php', + 'Automattic\\WooCommerce\\RestApi\\Utilities\\SingletonTrait' => __DIR__ . '/../..' . '/includes/rest-api/Utilities/SingletonTrait.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\CodeHacker' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/CodeHacker.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\BypassFinalsHack' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\CodeHack' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/Hacks/CodeHack.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\FunctionsMockerHack' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\StaticMockerHack' => __DIR__ . '/../..' . '/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\DependencyManagement\\MockableLegacyProxy' => __DIR__ . '/../..' . '/tests/Tools/DependencyManagement/MockableLegacyProxy.php', + 'Automattic\\WooCommerce\\Testing\\Tools\\FakeQueue' => __DIR__ . '/../..' . '/tests/Tools/FakeQueue.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\AssignDefaultCategoryTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/AssignDefaultCategoryTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\AbstractServiceProviderTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithDependencies' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithDependencies.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithInjectionMethodArgumentWithoutTypeHint' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithInjectionMethodArgumentWithoutTypeHint.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithNonFinalInjectionMethod' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithNonFinalInjectionMethod.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithPrivateInjectionMethod' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateInjectionMethod.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithScalarInjectionMethodArgument' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithScalarInjectionMethodArgument.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\DependencyClass' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/DependencyClass.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExtendedContainerTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/DependencyManagement/ExtendedContainerTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\DownloadPermissionsAdjusterTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\DataRegeneratorTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\FiltererTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\LookupDataStoreTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\RestApiUtilTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/RestApiUtilTest.php', + 'Automattic\\WooCommerce\\Tests\\Internal\\WCCom\\ConnectionHelperTest' => __DIR__ . '/../..' . '/tests/php/src/Internal/WCCom/ConnectionHelperTest.php', + 'Automattic\\WooCommerce\\Tests\\Proxies\\ClassThatDependsOnLegacyCodeTest' => __DIR__ . '/../..' . '/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php', + 'Automattic\\WooCommerce\\Tests\\Proxies\\ExampleClasses\\ClassThatDependsOnLegacyCode' => __DIR__ . '/../..' . '/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php', + 'Automattic\\WooCommerce\\Tests\\Proxies\\LegacyProxyTest' => __DIR__ . '/../..' . '/tests/php/src/Proxies/LegacyProxyTest.php', + 'Automattic\\WooCommerce\\Tests\\Proxies\\MockableLegacyProxyTest' => __DIR__ . '/../..' . '/tests/php/src/Proxies/MockableLegacyProxyTest.php', + 'Automattic\\WooCommerce\\Tests\\Utilities\\ArrayUtilTest' => __DIR__ . '/../..' . '/tests/php/src/Utilities/ArrayUtilTest.php', + 'Automattic\\WooCommerce\\Tests\\Utilities\\NumberUtilTest' => __DIR__ . '/../..' . '/tests/php/src/Utilities/NumberUtilTest.php', + 'Automattic\\WooCommerce\\Tests\\Utilities\\StringUtilTest' => __DIR__ . '/../..' . '/tests/php/src/Utilities/StringUtilTest.php', + 'Automattic\\WooCommerce\\Utilities\\ArrayUtil' => __DIR__ . '/../..' . '/src/Utilities/ArrayUtil.php', + 'Automattic\\WooCommerce\\Utilities\\NumberUtil' => __DIR__ . '/../..' . '/src/Utilities/NumberUtil.php', + 'Automattic\\WooCommerce\\Utilities\\StringUtil' => __DIR__ . '/../..' . '/src/Utilities/StringUtil.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ArgumentResolverInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverTrait' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ArgumentResolverTrait.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassName' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ClassName.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ClassNameInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameWithOptionalValue' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/ClassNameWithOptionalValue.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgument' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/RawArgument.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgumentInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Argument/RawArgumentInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Container' => __DIR__ . '/../..' . '/lib/packages/League/Container/Container.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/ContainerAwareInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareTrait' => __DIR__ . '/../..' . '/lib/packages/League/Container/ContainerAwareTrait.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\Definition' => __DIR__ . '/../..' . '/lib/packages/League/Container/Definition/Definition.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregate' => __DIR__ . '/../..' . '/lib/packages/League/Container/Definition/DefinitionAggregate.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregateInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Definition/DefinitionInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\ContainerException' => __DIR__ . '/../..' . '/lib/packages/League/Container/Exception/ContainerException.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\NotFoundException' => __DIR__ . '/../..' . '/lib/packages/League/Container/Exception/NotFoundException.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\Inflector' => __DIR__ . '/../..' . '/lib/packages/League/Container/Inflector/Inflector.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregate' => __DIR__ . '/../..' . '/lib/packages/League/Container/Inflector/InflectorAggregate.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregateInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/Inflector/InflectorInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ReflectionContainer' => __DIR__ . '/../..' . '/lib/packages/League/Container/ReflectionContainer.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\AbstractServiceProvider' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\BootableServiceProviderInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregate' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregate.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregateInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php', + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderInterface' => __DIR__ . '/../..' . '/lib/packages/League/Container/ServiceProvider/ServiceProviderInterface.php', + 'Composer\\Installers\\AglInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AglInstaller.php', + 'Composer\\Installers\\AimeosInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AimeosInstaller.php', + 'Composer\\Installers\\AnnotateCmsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php', + 'Composer\\Installers\\AsgardInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AsgardInstaller.php', + 'Composer\\Installers\\AttogramInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/AttogramInstaller.php', + 'Composer\\Installers\\BaseInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BaseInstaller.php', + 'Composer\\Installers\\BitrixInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BitrixInstaller.php', + 'Composer\\Installers\\BonefishInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/BonefishInstaller.php', + 'Composer\\Installers\\CakePHPInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CakePHPInstaller.php', + 'Composer\\Installers\\ChefInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ChefInstaller.php', + 'Composer\\Installers\\CiviCrmInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CiviCrmInstaller.php', + 'Composer\\Installers\\ClanCatsFrameworkInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php', + 'Composer\\Installers\\CockpitInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CockpitInstaller.php', + 'Composer\\Installers\\CodeIgniterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php', + 'Composer\\Installers\\Concrete5Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Concrete5Installer.php', + 'Composer\\Installers\\CraftInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CraftInstaller.php', + 'Composer\\Installers\\CroogoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/CroogoInstaller.php', + 'Composer\\Installers\\DecibelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DecibelInstaller.php', + 'Composer\\Installers\\DframeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DframeInstaller.php', + 'Composer\\Installers\\DokuWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DokuWikiInstaller.php', + 'Composer\\Installers\\DolibarrInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DolibarrInstaller.php', + 'Composer\\Installers\\DrupalInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/DrupalInstaller.php', + 'Composer\\Installers\\ElggInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ElggInstaller.php', + 'Composer\\Installers\\EliasisInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/EliasisInstaller.php', + 'Composer\\Installers\\ExpressionEngineInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php', + 'Composer\\Installers\\EzPlatformInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/EzPlatformInstaller.php', + 'Composer\\Installers\\FuelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/FuelInstaller.php', + 'Composer\\Installers\\FuelphpInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/FuelphpInstaller.php', + 'Composer\\Installers\\GravInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/GravInstaller.php', + 'Composer\\Installers\\HuradInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/HuradInstaller.php', + 'Composer\\Installers\\ImageCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ImageCMSInstaller.php', + 'Composer\\Installers\\Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Installer.php', + 'Composer\\Installers\\ItopInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ItopInstaller.php', + 'Composer\\Installers\\JoomlaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/JoomlaInstaller.php', + 'Composer\\Installers\\KanboardInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KanboardInstaller.php', + 'Composer\\Installers\\KirbyInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KirbyInstaller.php', + 'Composer\\Installers\\KnownInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KnownInstaller.php', + 'Composer\\Installers\\KodiCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KodiCMSInstaller.php', + 'Composer\\Installers\\KohanaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/KohanaInstaller.php', + 'Composer\\Installers\\LanManagementSystemInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php', + 'Composer\\Installers\\LaravelInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LaravelInstaller.php', + 'Composer\\Installers\\LavaLiteInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LavaLiteInstaller.php', + 'Composer\\Installers\\LithiumInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/LithiumInstaller.php', + 'Composer\\Installers\\MODULEWorkInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php', + 'Composer\\Installers\\MODXEvoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MODXEvoInstaller.php', + 'Composer\\Installers\\MagentoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MagentoInstaller.php', + 'Composer\\Installers\\MajimaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MajimaInstaller.php', + 'Composer\\Installers\\MakoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MakoInstaller.php', + 'Composer\\Installers\\MantisBTInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MantisBTInstaller.php', + 'Composer\\Installers\\MauticInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MauticInstaller.php', + 'Composer\\Installers\\MayaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MayaInstaller.php', + 'Composer\\Installers\\MediaWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MediaWikiInstaller.php', + 'Composer\\Installers\\MiaoxingInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MiaoxingInstaller.php', + 'Composer\\Installers\\MicroweberInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MicroweberInstaller.php', + 'Composer\\Installers\\ModxInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ModxInstaller.php', + 'Composer\\Installers\\MoodleInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/MoodleInstaller.php', + 'Composer\\Installers\\OctoberInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OctoberInstaller.php', + 'Composer\\Installers\\OntoWikiInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OntoWikiInstaller.php', + 'Composer\\Installers\\OsclassInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OsclassInstaller.php', + 'Composer\\Installers\\OxidInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/OxidInstaller.php', + 'Composer\\Installers\\PPIInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PPIInstaller.php', + 'Composer\\Installers\\PantheonInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PantheonInstaller.php', + 'Composer\\Installers\\PhiftyInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PhiftyInstaller.php', + 'Composer\\Installers\\PhpBBInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PhpBBInstaller.php', + 'Composer\\Installers\\PimcoreInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PimcoreInstaller.php', + 'Composer\\Installers\\PiwikInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PiwikInstaller.php', + 'Composer\\Installers\\PlentymarketsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php', + 'Composer\\Installers\\Plugin' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Plugin.php', + 'Composer\\Installers\\PortoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PortoInstaller.php', + 'Composer\\Installers\\PrestashopInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PrestashopInstaller.php', + 'Composer\\Installers\\ProcessWireInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ProcessWireInstaller.php', + 'Composer\\Installers\\PuppetInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PuppetInstaller.php', + 'Composer\\Installers\\PxcmsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/PxcmsInstaller.php', + 'Composer\\Installers\\RadPHPInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RadPHPInstaller.php', + 'Composer\\Installers\\ReIndexInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ReIndexInstaller.php', + 'Composer\\Installers\\Redaxo5Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Redaxo5Installer.php', + 'Composer\\Installers\\RedaxoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RedaxoInstaller.php', + 'Composer\\Installers\\RoundcubeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/RoundcubeInstaller.php', + 'Composer\\Installers\\SMFInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SMFInstaller.php', + 'Composer\\Installers\\ShopwareInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ShopwareInstaller.php', + 'Composer\\Installers\\SilverStripeInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SilverStripeInstaller.php', + 'Composer\\Installers\\SiteDirectInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SiteDirectInstaller.php', + 'Composer\\Installers\\StarbugInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/StarbugInstaller.php', + 'Composer\\Installers\\SyDESInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SyDESInstaller.php', + 'Composer\\Installers\\SyliusInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/SyliusInstaller.php', + 'Composer\\Installers\\Symfony1Installer' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/Symfony1Installer.php', + 'Composer\\Installers\\TYPO3CmsInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php', + 'Composer\\Installers\\TYPO3FlowInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php', + 'Composer\\Installers\\TaoInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TaoInstaller.php', + 'Composer\\Installers\\TastyIgniterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php', + 'Composer\\Installers\\TheliaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TheliaInstaller.php', + 'Composer\\Installers\\TuskInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/TuskInstaller.php', + 'Composer\\Installers\\UserFrostingInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/UserFrostingInstaller.php', + 'Composer\\Installers\\VanillaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/VanillaInstaller.php', + 'Composer\\Installers\\VgmcpInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/VgmcpInstaller.php', + 'Composer\\Installers\\WHMCSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WHMCSInstaller.php', + 'Composer\\Installers\\WinterInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WinterInstaller.php', + 'Composer\\Installers\\WolfCMSInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WolfCMSInstaller.php', + 'Composer\\Installers\\WordPressInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/WordPressInstaller.php', + 'Composer\\Installers\\YawikInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/YawikInstaller.php', + 'Composer\\Installers\\ZendInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ZendInstaller.php', + 'Composer\\Installers\\ZikulaInstaller' => __DIR__ . '/..' . '/composer/installers/src/Composer/Installers/ZikulaInstaller.php', + 'MaxMind\\Db\\Reader' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader.php', + 'MaxMind\\Db\\Reader\\Decoder' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php', + 'MaxMind\\Db\\Reader\\InvalidDatabaseException' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php', + 'MaxMind\\Db\\Reader\\Metadata' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php', + 'MaxMind\\Db\\Reader\\Util' => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php', + 'Pelago\\Emogrifier' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier.php', + 'Pelago\\Emogrifier\\CssInliner' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/CssInliner.php', + 'Pelago\\Emogrifier\\HtmlProcessor\\AbstractHtmlProcessor' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php', + 'Pelago\\Emogrifier\\HtmlProcessor\\CssToAttributeConverter' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php', + 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlNormalizer' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php', + 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlPruner' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlPruner.php', + 'Pelago\\Emogrifier\\Utilities\\ArrayIntersector' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/Utilities/ArrayIntersector.php', + 'Pelago\\Emogrifier\\Utilities\\CssConcatenator' => __DIR__ . '/..' . '/pelago/emogrifier/src/Emogrifier/Utilities/CssConcatenator.php', + 'Psr\\Container\\ContainerExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerExceptionInterface.php', + 'Psr\\Container\\ContainerInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerInterface.php', + 'Psr\\Container\\NotFoundExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/NotFoundExceptionInterface.php', + 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => __DIR__ . '/..' . '/symfony/css-selector/CssSelectorConverter.php', + 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExceptionInterface.php', + 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExpressionErrorException.php', + 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/InternalErrorException.php', + 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ParseException.php', + 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/SyntaxErrorException.php', + 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AbstractNode.php', + 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AttributeNode.php', + 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ClassNode.php', + 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/CombinedSelectorNode.php', + 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ElementNode.php', + 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/FunctionNode.php', + 'Symfony\\Component\\CssSelector\\Node\\HashNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/HashNode.php', + 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/NegationNode.php', + 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => __DIR__ . '/..' . '/symfony/css-selector/Node/NodeInterface.php', + 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/PseudoNode.php', + 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/SelectorNode.php', + 'Symfony\\Component\\CssSelector\\Node\\Specificity' => __DIR__ . '/..' . '/symfony/css-selector/Node/Specificity.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/CommentHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HandlerInterface.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HashHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/NumberHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/StringHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Parser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Parser.php', + 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/ParserInterface.php', + 'Symfony\\Component\\CssSelector\\Parser\\Reader' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Reader.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ClassParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ElementParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/HashParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Token' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Token.php', + 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => __DIR__ . '/..' . '/symfony/css-selector/Parser/TokenStream.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AbstractExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/CombinationExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/FunctionExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/HtmlExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/NodeExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Translator' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Translator.php', + 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/TranslatorInterface.php', + 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => __DIR__ . '/..' . '/symfony/css-selector/XPath/XPathExpr.php', + 'WC_REST_CRUD_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php', + 'WC_REST_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php', + 'WC_REST_Coupons_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php', + 'WC_REST_Coupons_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php', + 'WC_REST_Coupons_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php', + 'WC_REST_Customer_Downloads_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php', + 'WC_REST_Customer_Downloads_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php', + 'WC_REST_Customer_Downloads_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php', + 'WC_REST_Customers_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php', + 'WC_REST_Customers_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php', + 'WC_REST_Customers_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php', + 'WC_REST_Data_Continents_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php', + 'WC_REST_Data_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php', + 'WC_REST_Data_Countries_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php', + 'WC_REST_Data_Currencies_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php', + 'WC_REST_Network_Orders_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php', + 'WC_REST_Network_Orders_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php', + 'WC_REST_Order_Notes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php', + 'WC_REST_Order_Notes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php', + 'WC_REST_Order_Notes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php', + 'WC_REST_Order_Refunds_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php', + 'WC_REST_Order_Refunds_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php', + 'WC_REST_Order_Refunds_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php', + 'WC_REST_Orders_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php', + 'WC_REST_Orders_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php', + 'WC_REST_Orders_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php', + 'WC_REST_Payment_Gateways_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php', + 'WC_REST_Payment_Gateways_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php', + 'WC_REST_Posts_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php', + 'WC_REST_Product_Attribute_Terms_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php', + 'WC_REST_Product_Attribute_Terms_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php', + 'WC_REST_Product_Attribute_Terms_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php', + 'WC_REST_Product_Attributes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php', + 'WC_REST_Product_Attributes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php', + 'WC_REST_Product_Attributes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php', + 'WC_REST_Product_Categories_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php', + 'WC_REST_Product_Categories_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php', + 'WC_REST_Product_Categories_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php', + 'WC_REST_Product_Reviews_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php', + 'WC_REST_Product_Reviews_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php', + 'WC_REST_Product_Reviews_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php', + 'WC_REST_Product_Shipping_Classes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php', + 'WC_REST_Product_Shipping_Classes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php', + 'WC_REST_Product_Shipping_Classes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php', + 'WC_REST_Product_Tags_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php', + 'WC_REST_Product_Tags_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php', + 'WC_REST_Product_Tags_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php', + 'WC_REST_Product_Variations_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php', + 'WC_REST_Product_Variations_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php', + 'WC_REST_Products_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php', + 'WC_REST_Products_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php', + 'WC_REST_Products_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php', + 'WC_REST_Report_Coupons_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php', + 'WC_REST_Report_Customers_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php', + 'WC_REST_Report_Orders_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php', + 'WC_REST_Report_Products_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php', + 'WC_REST_Report_Reviews_Totals_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php', + 'WC_REST_Report_Sales_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php', + 'WC_REST_Report_Sales_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php', + 'WC_REST_Report_Sales_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php', + 'WC_REST_Report_Top_Sellers_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php', + 'WC_REST_Report_Top_Sellers_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php', + 'WC_REST_Report_Top_Sellers_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php', + 'WC_REST_Reports_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php', + 'WC_REST_Reports_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php', + 'WC_REST_Reports_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php', + 'WC_REST_Setting_Options_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php', + 'WC_REST_Setting_Options_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php', + 'WC_REST_Settings_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php', + 'WC_REST_Settings_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php', + 'WC_REST_Shipping_Methods_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php', + 'WC_REST_Shipping_Methods_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php', + 'WC_REST_Shipping_Zone_Locations_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php', + 'WC_REST_Shipping_Zone_Locations_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php', + 'WC_REST_Shipping_Zone_Methods_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php', + 'WC_REST_Shipping_Zone_Methods_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php', + 'WC_REST_Shipping_Zones_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php', + 'WC_REST_Shipping_Zones_Controller_Base' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php', + 'WC_REST_Shipping_Zones_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php', + 'WC_REST_System_Status_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php', + 'WC_REST_System_Status_Tools_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php', + 'WC_REST_System_Status_Tools_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php', + 'WC_REST_System_Status_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php', + 'WC_REST_Tax_Classes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php', + 'WC_REST_Tax_Classes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php', + 'WC_REST_Tax_Classes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php', + 'WC_REST_Taxes_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php', + 'WC_REST_Taxes_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php', + 'WC_REST_Taxes_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php', + 'WC_REST_Telemetry_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php', + 'WC_REST_Terms_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php', + 'WC_REST_Webhook_Deliveries_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php', + 'WC_REST_Webhook_Deliveries_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php', + 'WC_REST_Webhooks_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php', + 'WC_REST_Webhooks_V1_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php', + 'WC_REST_Webhooks_V2_Controller' => __DIR__ . '/../..' . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit7fa27687a59114a5aec1ac3080434897::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit7fa27687a59114a5aec1ac3080434897::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInit7fa27687a59114a5aec1ac3080434897::$prefixesPsr0; + $loader->classMap = ComposerStaticInit7fa27687a59114a5aec1ac3080434897::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..70b680d --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,670 @@ +[ + { + "name": "automattic/jetpack-autoloader", + "version": "2.10.1", + "version_normalized": "2.10.1.0", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-autoloader.git", + "reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/20393c4677765c3e737dcb5aee7a3f7b90dce4b3", + "reference": "20393c4677765c3e737dcb5aee7a3f7b90dce4b3", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0" + }, + "require-dev": { + "automattic/jetpack-changelogger": "^1.1", + "yoast/phpunit-polyfills": "0.2.0" + }, + "time": "2021-03-30T15:15:59+00:00", + "type": "composer-plugin", + "extra": { + "class": "Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin", + "mirror-repo": "Automattic/jetpack-autoloader", + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-autoloader/compare/v${old}...v${new}" + }, + "branch-alias": { + "dev-master": "2.10.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/AutoloadGenerator.php" + ], + "psr-4": { + "Automattic\\Jetpack\\Autoloader\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Creates a custom autoloader for a plugin or theme.", + "support": { + "source": "https://github.com/Automattic/jetpack-autoloader/tree/2.10.1" + } + }, + { + "name": "automattic/jetpack-constants", + "version": "v1.5.1", + "version_normalized": "1.5.1.0", + "source": { + "type": "git", + "url": "https://github.com/Automattic/jetpack-constants.git", + "reference": "18f772daddc8be5df76c9f4a92e017a3c2569a5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/jetpack-constants/zipball/18f772daddc8be5df76c9f4a92e017a3c2569a5b", + "reference": "18f772daddc8be5df76c9f4a92e017a3c2569a5b", + "shasum": "" + }, + "require-dev": { + "php-mock/php-mock": "^2.1", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5" + }, + "time": "2020-10-28T19:00:31+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "A wrapper for defining constants in a more testable way.", + "support": { + "source": "https://github.com/Automattic/jetpack-constants/tree/v1.5.1" + } + }, + { + "name": "composer/installers", + "version": "v1.12.0", + "version_normalized": "1.12.0.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/d20a64ed3c94748397ff5973488761b22f6d3f19", + "reference": "d20a64ed3c94748397ff5973488761b22f6d3f19", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0" + }, + "replace": { + "roundcube/plugin-installer": "*", + "shama/baton": "*" + }, + "require-dev": { + "composer/composer": "1.6.* || ^2.0", + "composer/semver": "^1 || ^3", + "phpstan/phpstan": "^0.12.55", + "phpstan/phpstan-phpunit": "^0.12.16", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^2.3" + }, + "time": "2021-09-13T08:19:44+00:00", + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "MantisBT", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Starbug", + "Thelia", + "Whmcs", + "WolfCMS", + "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "joomla", + "known", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "mediawiki", + "miaoxing", + "modulework", + "modx", + "moodle", + "osclass", + "pantheon", + "phpbb", + "piwik", + "ppi", + "processwire", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "sylius", + "symfony", + "tastyigniter", + "typo3", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "support": { + "issues": "https://github.com/composer/installers/issues", + "source": "https://github.com/composer/installers/tree/v1.12.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ] + }, + { + "name": "maxmind-db/reader", + "version": "v1.6.0", + "version_normalized": "1.6.0.0", + "source": { + "type": "git", + "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", + "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/febd4920bf17c1da84cef58e56a8227dfb37fbe4", + "reference": "febd4920bf17c1da84cef58e56a8227dfb37fbe4", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "conflict": { + "ext-maxminddb": "<1.6.0,>=2.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "2.*", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpcov": "^3.0", + "phpunit/phpunit": "5.*", + "squizlabs/php_codesniffer": "3.*" + }, + "suggest": { + "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" + }, + "time": "2019-12-19T22:59:03+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "MaxMind\\Db\\": "src/MaxMind/Db" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory J. Oschwald", + "email": "goschwald@maxmind.com", + "homepage": "https://www.maxmind.com/" + } + ], + "description": "MaxMind DB Reader API", + "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", + "keywords": [ + "database", + "geoip", + "geoip2", + "geolocation", + "maxmind" + ], + "support": { + "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", + "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.6.0" + } + }, + { + "name": "pelago/emogrifier", + "version": "v3.1.0", + "version_normalized": "3.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/emogrifier.git", + "reference": "f6a5c7d44612d86c3901c93f1592f5440e6b2cd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/emogrifier/zipball/f6a5c7d44612d86c3901c93f1592f5440e6b2cd8", + "reference": "f6a5c7d44612d86c3901c93f1592f5440e6b2cd8", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.6 || ~7.0 || ~7.1 || ~7.2 || ~7.3 || ~7.4", + "symfony/css-selector": "^2.8 || ^3.0 || ^4.0 || ^5.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.15.3", + "phpmd/phpmd": "^2.7.0", + "phpunit/phpunit": "^5.7.27", + "squizlabs/php_codesniffer": "^3.5.0" + }, + "time": "2019-12-26T19:37:31+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Pelago\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Zoli Szabó", + "email": "zoli.szabo+github@gmail.com" + }, + { + "name": "John Reeve", + "email": "jreeve@pelagodesign.com" + }, + { + "name": "Jake Hotson", + "email": "jake@qzdesign.co.uk" + }, + { + "name": "Cameron Brooks" + }, + { + "name": "Jaime Prado" + } + ], + "description": "Converts CSS styles into inline style attributes in your HTML code", + "homepage": "https://www.myintervals.com/emogrifier.php", + "keywords": [ + "css", + "email", + "pre-processing" + ], + "support": { + "issues": "https://github.com/MyIntervals/emogrifier/issues", + "source": "https://github.com/MyIntervals/emogrifier" + } + }, + { + "name": "psr/container", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2017-02-14T16:28:37+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/master" + } + }, + { + "name": "symfony/css-selector", + "version": "v3.3.6", + "version_normalized": "3.3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "4d882dced7b995d5274293039370148e291808f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/4d882dced7b995d5274293039370148e291808f2", + "reference": "4d882dced7b995d5274293039370148e291808f2", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "time": "2017-05-01T15:01:29+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/master" + } + }, + { + "name": "woocommerce/action-scheduler", + "version": "3.3.0", + "version_normalized": "3.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/action-scheduler.git", + "reference": "5588a831cd2453ecf7d4803f3a81063e13cde93d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/action-scheduler/zipball/5588a831cd2453ecf7d4803f3a81063e13cde93d", + "reference": "5588a831cd2453ecf7d4803f3a81063e13cde93d", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^7.5", + "woocommerce/woocommerce-sniffs": "0.1.0", + "wp-cli/wp-cli": "~2.5.0" + }, + "time": "2021-09-15T21:08:48+00:00", + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "test": "Run unit tests", + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + } + }, + "installation-source": "dist", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "description": "Action Scheduler for WordPress and WooCommerce", + "homepage": "https://actionscheduler.org/", + "support": { + "issues": "https://github.com/woocommerce/action-scheduler/issues", + "source": "https://github.com/woocommerce/action-scheduler/tree/3.3.0" + } + }, + { + "name": "woocommerce/woocommerce-admin", + "version": "2.8.0", + "version_normalized": "2.8.0.0", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-admin.git", + "reference": "63b93a95db4bf788f42587a41f2378128a2adfdf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/63b93a95db4bf788f42587a41f2378128a2adfdf", + "reference": "63b93a95db4bf788f42587a41f2378128a2adfdf", + "shasum": "" + }, + "require": { + "automattic/jetpack-autoloader": "^2.9.1", + "composer/installers": "^1.9.0", + "php": ">=7.0" + }, + "require-dev": { + "automattic/jetpack-changelogger": "^1.1", + "bamarni/composer-bin-plugin": "^1.4", + "suin/phpcs-psr4-sniff": "^2.2", + "woocommerce/woocommerce-sniffs": "0.1.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "time": "2021-11-02T19:28:38+00:00", + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "test": "Run unit tests", + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + }, + "bamarni-bin": { + "target-directory": "bin/composer" + }, + "changelogger": { + "changelog": "./changelog.txt", + "formatter": { + "filename": "bin/changelogger/WCAdminFormatter.php" + }, + "versioning": "semver", + "changes-dir": "./changelogs", + "types": [ + "Fix", + "Add", + "Update", + "Dev", + "Tweak", + "Performance", + "Enhancement" + ] + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Automattic\\WooCommerce\\Admin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "description": "A modern, javascript-driven WooCommerce Admin experience.", + "homepage": "https://github.com/woocommerce/woocommerce-admin", + "support": { + "issues": "https://github.com/woocommerce/woocommerce-admin/issues", + "source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.8.0" + } + }, + { + "name": "woocommerce/woocommerce-blocks", + "version": "v6.1.0", + "version_normalized": "6.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git", + "reference": "8556efd69e85c01f5571d39e6581d9b8486b682f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/8556efd69e85c01f5571d39e6581d9b8486b682f", + "reference": "8556efd69e85c01f5571d39e6581d9b8486b682f", + "shasum": "" + }, + "require": { + "automattic/jetpack-autoloader": "^2.9.1", + "composer/installers": "^1.7.0" + }, + "require-dev": { + "woocommerce/woocommerce-sniffs": "0.1.0", + "wp-phpunit/wp-phpunit": "^5.4", + "yoast/phpunit-polyfills": "^1.0" + }, + "time": "2021-10-12T13:07:11+00:00", + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Automattic\\WooCommerce\\Blocks\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "description": "WooCommerce blocks for the Gutenberg editor.", + "homepage": "https://woocommerce.com/", + "keywords": [ + "blocks", + "gutenberg", + "woocommerce" + ], + "support": { + "issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues", + "source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v6.1.0" + } + } +] diff --git a/vendor/composer/installers/LICENSE b/vendor/composer/installers/LICENSE new file mode 100644 index 0000000..85f97fc --- /dev/null +++ b/vendor/composer/installers/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Kyle Robinson Young + +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. \ No newline at end of file diff --git a/vendor/composer/installers/phpstan.neon.dist b/vendor/composer/installers/phpstan.neon.dist new file mode 100644 index 0000000..8e3d81e --- /dev/null +++ b/vendor/composer/installers/phpstan.neon.dist @@ -0,0 +1,10 @@ +parameters: + level: 5 + paths: + - src + - tests + excludes_analyse: + - tests/Composer/Installers/Test/PolyfillTestCase.php + +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon diff --git a/vendor/composer/installers/src/Composer/Installers/AglInstaller.php b/vendor/composer/installers/src/Composer/Installers/AglInstaller.php new file mode 100644 index 0000000..01b8a41 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/AglInstaller.php @@ -0,0 +1,21 @@ + 'More/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $vars['name'] = preg_replace_callback('/(?:^|_|-)(.?)/', function ($matches) { + return strtoupper($matches[1]); + }, $vars['name']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/AimeosInstaller.php b/vendor/composer/installers/src/Composer/Installers/AimeosInstaller.php new file mode 100644 index 0000000..79a0e95 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/AimeosInstaller.php @@ -0,0 +1,9 @@ + 'ext/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php b/vendor/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php new file mode 100644 index 0000000..89d7ad9 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php @@ -0,0 +1,11 @@ + 'addons/modules/{$name}/', + 'component' => 'addons/components/{$name}/', + 'service' => 'addons/services/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/AsgardInstaller.php b/vendor/composer/installers/src/Composer/Installers/AsgardInstaller.php new file mode 100644 index 0000000..22dad1b --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/AsgardInstaller.php @@ -0,0 +1,49 @@ + 'Modules/{$name}/', + 'theme' => 'Themes/{$name}/' + ); + + /** + * Format package name. + * + * For package type asgard-module, cut off a trailing '-plugin' if present. + * + * For package type asgard-theme, cut off a trailing '-theme' if present. + * + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] === 'asgard-module') { + return $this->inflectPluginVars($vars); + } + + if ($vars['type'] === 'asgard-theme') { + return $this->inflectThemeVars($vars); + } + + return $vars; + } + + protected function inflectPluginVars($vars) + { + $vars['name'] = preg_replace('/-module$/', '', $vars['name']); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } + + protected function inflectThemeVars($vars) + { + $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/AttogramInstaller.php b/vendor/composer/installers/src/Composer/Installers/AttogramInstaller.php new file mode 100644 index 0000000..d62fd8f --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/AttogramInstaller.php @@ -0,0 +1,9 @@ + 'modules/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/BaseInstaller.php b/vendor/composer/installers/src/Composer/Installers/BaseInstaller.php new file mode 100644 index 0000000..70dde90 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/BaseInstaller.php @@ -0,0 +1,137 @@ +composer = $composer; + $this->package = $package; + $this->io = $io; + } + + /** + * Return the install path based on package type. + * + * @param PackageInterface $package + * @param string $frameworkType + * @return string + */ + public function getInstallPath(PackageInterface $package, $frameworkType = '') + { + $type = $this->package->getType(); + + $prettyName = $this->package->getPrettyName(); + if (strpos($prettyName, '/') !== false) { + list($vendor, $name) = explode('/', $prettyName); + } else { + $vendor = ''; + $name = $prettyName; + } + + $availableVars = $this->inflectPackageVars(compact('name', 'vendor', 'type')); + + $extra = $package->getExtra(); + if (!empty($extra['installer-name'])) { + $availableVars['name'] = $extra['installer-name']; + } + + if ($this->composer->getPackage()) { + $extra = $this->composer->getPackage()->getExtra(); + if (!empty($extra['installer-paths'])) { + $customPath = $this->mapCustomInstallPaths($extra['installer-paths'], $prettyName, $type, $vendor); + if ($customPath !== false) { + return $this->templatePath($customPath, $availableVars); + } + } + } + + $packageType = substr($type, strlen($frameworkType) + 1); + $locations = $this->getLocations(); + if (!isset($locations[$packageType])) { + throw new \InvalidArgumentException(sprintf('Package type "%s" is not supported', $type)); + } + + return $this->templatePath($locations[$packageType], $availableVars); + } + + /** + * For an installer to override to modify the vars per installer. + * + * @param array $vars This will normally receive array{name: string, vendor: string, type: string} + * @return array + */ + public function inflectPackageVars($vars) + { + return $vars; + } + + /** + * Gets the installer's locations + * + * @return array map of package types => install path + */ + public function getLocations() + { + return $this->locations; + } + + /** + * Replace vars in a path + * + * @param string $path + * @param array $vars + * @return string + */ + protected function templatePath($path, array $vars = array()) + { + if (strpos($path, '{') !== false) { + extract($vars); + preg_match_all('@\{\$([A-Za-z0-9_]*)\}@i', $path, $matches); + if (!empty($matches[1])) { + foreach ($matches[1] as $var) { + $path = str_replace('{$' . $var . '}', $$var, $path); + } + } + } + + return $path; + } + + /** + * Search through a passed paths array for a custom install path. + * + * @param array $paths + * @param string $name + * @param string $type + * @param string $vendor = NULL + * @return string|false + */ + protected function mapCustomInstallPaths(array $paths, $name, $type, $vendor = NULL) + { + foreach ($paths as $path => $names) { + $names = (array) $names; + if (in_array($name, $names) || in_array('type:' . $type, $names) || in_array('vendor:' . $vendor, $names)) { + return $path; + } + } + + return false; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/BitrixInstaller.php b/vendor/composer/installers/src/Composer/Installers/BitrixInstaller.php new file mode 100644 index 0000000..e80cd1e --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/BitrixInstaller.php @@ -0,0 +1,126 @@ +.`. + * - `bitrix-d7-component` — copy the component to directory `bitrix/components//`. + * - `bitrix-d7-template` — copy the template to directory `bitrix/templates/_`. + * + * You can set custom path to directory with Bitrix kernel in `composer.json`: + * + * ```json + * { + * "extra": { + * "bitrix-dir": "s1/bitrix" + * } + * } + * ``` + * + * @author Nik Samokhvalov + * @author Denis Kulichkin + */ +class BitrixInstaller extends BaseInstaller +{ + protected $locations = array( + 'module' => '{$bitrix_dir}/modules/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) + 'component' => '{$bitrix_dir}/components/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) + 'theme' => '{$bitrix_dir}/templates/{$name}/', // deprecated, remove on the major release (Backward compatibility will be broken) + 'd7-module' => '{$bitrix_dir}/modules/{$vendor}.{$name}/', + 'd7-component' => '{$bitrix_dir}/components/{$vendor}/{$name}/', + 'd7-template' => '{$bitrix_dir}/templates/{$vendor}_{$name}/', + ); + + /** + * @var array Storage for informations about duplicates at all the time of installation packages. + */ + private static $checkedDuplicates = array(); + + /** + * {@inheritdoc} + */ + public function inflectPackageVars($vars) + { + if ($this->composer->getPackage()) { + $extra = $this->composer->getPackage()->getExtra(); + + if (isset($extra['bitrix-dir'])) { + $vars['bitrix_dir'] = $extra['bitrix-dir']; + } + } + + if (!isset($vars['bitrix_dir'])) { + $vars['bitrix_dir'] = 'bitrix'; + } + + return parent::inflectPackageVars($vars); + } + + /** + * {@inheritdoc} + */ + protected function templatePath($path, array $vars = array()) + { + $templatePath = parent::templatePath($path, $vars); + $this->checkDuplicates($templatePath, $vars); + + return $templatePath; + } + + /** + * Duplicates search packages. + * + * @param string $path + * @param array $vars + */ + protected function checkDuplicates($path, array $vars = array()) + { + $packageType = substr($vars['type'], strlen('bitrix') + 1); + $localDir = explode('/', $vars['bitrix_dir']); + array_pop($localDir); + $localDir[] = 'local'; + $localDir = implode('/', $localDir); + + $oldPath = str_replace( + array('{$bitrix_dir}', '{$name}'), + array($localDir, $vars['name']), + $this->locations[$packageType] + ); + + if (in_array($oldPath, static::$checkedDuplicates)) { + return; + } + + if ($oldPath !== $path && file_exists($oldPath) && $this->io && $this->io->isInteractive()) { + + $this->io->writeError(' Duplication of packages:'); + $this->io->writeError(' Package ' . $oldPath . ' will be called instead package ' . $path . ''); + + while (true) { + switch ($this->io->ask(' Delete ' . $oldPath . ' [y,n,?]? ', '?')) { + case 'y': + $fs = new Filesystem(); + $fs->removeDirectory($oldPath); + break 2; + + case 'n': + break 2; + + case '?': + default: + $this->io->writeError(array( + ' y - delete package ' . $oldPath . ' and to continue with the installation', + ' n - don\'t delete and to continue with the installation', + )); + $this->io->writeError(' ? - print help'); + break; + } + } + } + + static::$checkedDuplicates[] = $oldPath; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/BonefishInstaller.php b/vendor/composer/installers/src/Composer/Installers/BonefishInstaller.php new file mode 100644 index 0000000..da3aad2 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/BonefishInstaller.php @@ -0,0 +1,9 @@ + 'Packages/{$vendor}/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/CakePHPInstaller.php b/vendor/composer/installers/src/Composer/Installers/CakePHPInstaller.php new file mode 100644 index 0000000..1e2ddd0 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/CakePHPInstaller.php @@ -0,0 +1,66 @@ + 'Plugin/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + if ($this->matchesCakeVersion('>=', '3.0.0')) { + return $vars; + } + + $nameParts = explode('/', $vars['name']); + foreach ($nameParts as &$value) { + $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); + $value = str_replace(array('-', '_'), ' ', $value); + $value = str_replace(' ', '', ucwords($value)); + } + $vars['name'] = implode('/', $nameParts); + + return $vars; + } + + /** + * Change the default plugin location when cakephp >= 3.0 + */ + public function getLocations() + { + if ($this->matchesCakeVersion('>=', '3.0.0')) { + $this->locations['plugin'] = $this->composer->getConfig()->get('vendor-dir') . '/{$vendor}/{$name}/'; + } + return $this->locations; + } + + /** + * Check if CakePHP version matches against a version + * + * @param string $matcher + * @param string $version + * @return bool + * @phpstan-param Constraint::STR_OP_* $matcher + */ + protected function matchesCakeVersion($matcher, $version) + { + $repositoryManager = $this->composer->getRepositoryManager(); + if (! $repositoryManager) { + return false; + } + + $repos = $repositoryManager->getLocalRepository(); + if (!$repos) { + return false; + } + + return $repos->findPackage('cakephp/cakephp', new Constraint($matcher, $version)) !== null; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/ChefInstaller.php b/vendor/composer/installers/src/Composer/Installers/ChefInstaller.php new file mode 100644 index 0000000..ab2f9aa --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ChefInstaller.php @@ -0,0 +1,11 @@ + 'Chef/{$vendor}/{$name}/', + 'role' => 'Chef/roles/{$name}/', + ); +} + diff --git a/vendor/composer/installers/src/Composer/Installers/CiviCrmInstaller.php b/vendor/composer/installers/src/Composer/Installers/CiviCrmInstaller.php new file mode 100644 index 0000000..6673aea --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/CiviCrmInstaller.php @@ -0,0 +1,9 @@ + 'ext/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php b/vendor/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php new file mode 100644 index 0000000..c887815 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php @@ -0,0 +1,10 @@ + 'CCF/orbit/{$name}/', + 'theme' => 'CCF/app/themes/{$name}/', + ); +} \ No newline at end of file diff --git a/vendor/composer/installers/src/Composer/Installers/CockpitInstaller.php b/vendor/composer/installers/src/Composer/Installers/CockpitInstaller.php new file mode 100644 index 0000000..053f3ff --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/CockpitInstaller.php @@ -0,0 +1,32 @@ + 'cockpit/modules/addons/{$name}/', + ); + + /** + * Format module name. + * + * Strip `module-` prefix from package name. + * + * {@inheritDoc} + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] == 'cockpit-module') { + return $this->inflectModuleVars($vars); + } + + return $vars; + } + + public function inflectModuleVars($vars) + { + $vars['name'] = ucfirst(preg_replace('/cockpit-/i', '', $vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php b/vendor/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php new file mode 100644 index 0000000..3b4a4ec --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php @@ -0,0 +1,11 @@ + 'application/libraries/{$name}/', + 'third-party' => 'application/third_party/{$name}/', + 'module' => 'application/modules/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/Concrete5Installer.php b/vendor/composer/installers/src/Composer/Installers/Concrete5Installer.php new file mode 100644 index 0000000..5c01baf --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/Concrete5Installer.php @@ -0,0 +1,13 @@ + 'concrete/', + 'block' => 'application/blocks/{$name}/', + 'package' => 'packages/{$name}/', + 'theme' => 'application/themes/{$name}/', + 'update' => 'updates/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/CraftInstaller.php b/vendor/composer/installers/src/Composer/Installers/CraftInstaller.php new file mode 100644 index 0000000..d37a77a --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/CraftInstaller.php @@ -0,0 +1,35 @@ + 'craft/plugins/{$name}/', + ); + + /** + * Strip `craft-` prefix and/or `-plugin` suffix from package names + * + * @param array $vars + * + * @return array + */ + final public function inflectPackageVars($vars) + { + return $this->inflectPluginVars($vars); + } + + private function inflectPluginVars($vars) + { + $vars['name'] = preg_replace('/-' . self::NAME_SUFFIX . '$/i', '', $vars['name']); + $vars['name'] = preg_replace('/^' . self::NAME_PREFIX . '-/i', '', $vars['name']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/CroogoInstaller.php b/vendor/composer/installers/src/Composer/Installers/CroogoInstaller.php new file mode 100644 index 0000000..d94219d --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/CroogoInstaller.php @@ -0,0 +1,21 @@ + 'Plugin/{$name}/', + 'theme' => 'View/Themed/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower(str_replace(array('-', '_'), ' ', $vars['name'])); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/DecibelInstaller.php b/vendor/composer/installers/src/Composer/Installers/DecibelInstaller.php new file mode 100644 index 0000000..f4837a6 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/DecibelInstaller.php @@ -0,0 +1,10 @@ + 'app/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/DframeInstaller.php b/vendor/composer/installers/src/Composer/Installers/DframeInstaller.php new file mode 100644 index 0000000..7078816 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/DframeInstaller.php @@ -0,0 +1,10 @@ + 'modules/{$vendor}/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/DokuWikiInstaller.php b/vendor/composer/installers/src/Composer/Installers/DokuWikiInstaller.php new file mode 100644 index 0000000..cfd638d --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/DokuWikiInstaller.php @@ -0,0 +1,50 @@ + 'lib/plugins/{$name}/', + 'template' => 'lib/tpl/{$name}/', + ); + + /** + * Format package name. + * + * For package type dokuwiki-plugin, cut off a trailing '-plugin', + * or leading dokuwiki_ if present. + * + * For package type dokuwiki-template, cut off a trailing '-template' if present. + * + */ + public function inflectPackageVars($vars) + { + + if ($vars['type'] === 'dokuwiki-plugin') { + return $this->inflectPluginVars($vars); + } + + if ($vars['type'] === 'dokuwiki-template') { + return $this->inflectTemplateVars($vars); + } + + return $vars; + } + + protected function inflectPluginVars($vars) + { + $vars['name'] = preg_replace('/-plugin$/', '', $vars['name']); + $vars['name'] = preg_replace('/^dokuwiki_?-?/', '', $vars['name']); + + return $vars; + } + + protected function inflectTemplateVars($vars) + { + $vars['name'] = preg_replace('/-template$/', '', $vars['name']); + $vars['name'] = preg_replace('/^dokuwiki_?-?/', '', $vars['name']); + + return $vars; + } + +} diff --git a/vendor/composer/installers/src/Composer/Installers/DolibarrInstaller.php b/vendor/composer/installers/src/Composer/Installers/DolibarrInstaller.php new file mode 100644 index 0000000..21f7e8e --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/DolibarrInstaller.php @@ -0,0 +1,16 @@ + + */ +class DolibarrInstaller extends BaseInstaller +{ + //TODO: Add support for scripts and themes + protected $locations = array( + 'module' => 'htdocs/custom/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/DrupalInstaller.php b/vendor/composer/installers/src/Composer/Installers/DrupalInstaller.php new file mode 100644 index 0000000..7328239 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/DrupalInstaller.php @@ -0,0 +1,22 @@ + 'core/', + 'module' => 'modules/{$name}/', + 'theme' => 'themes/{$name}/', + 'library' => 'libraries/{$name}/', + 'profile' => 'profiles/{$name}/', + 'database-driver' => 'drivers/lib/Drupal/Driver/Database/{$name}/', + 'drush' => 'drush/{$name}/', + 'custom-theme' => 'themes/custom/{$name}/', + 'custom-module' => 'modules/custom/{$name}/', + 'custom-profile' => 'profiles/custom/{$name}/', + 'drupal-multisite' => 'sites/{$name}/', + 'console' => 'console/{$name}/', + 'console-language' => 'console/language/{$name}/', + 'config' => 'config/sync/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/ElggInstaller.php b/vendor/composer/installers/src/Composer/Installers/ElggInstaller.php new file mode 100644 index 0000000..c0bb609 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ElggInstaller.php @@ -0,0 +1,9 @@ + 'mod/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/EliasisInstaller.php b/vendor/composer/installers/src/Composer/Installers/EliasisInstaller.php new file mode 100644 index 0000000..6f3dc97 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/EliasisInstaller.php @@ -0,0 +1,12 @@ + 'components/{$name}/', + 'module' => 'modules/{$name}/', + 'plugin' => 'plugins/{$name}/', + 'template' => 'templates/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php b/vendor/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php new file mode 100644 index 0000000..d5321a8 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php @@ -0,0 +1,29 @@ + 'system/expressionengine/third_party/{$name}/', + 'theme' => 'themes/third_party/{$name}/', + ); + + private $ee3Locations = array( + 'addon' => 'system/user/addons/{$name}/', + 'theme' => 'themes/user/{$name}/', + ); + + public function getInstallPath(PackageInterface $package, $frameworkType = '') + { + + $version = "{$frameworkType}Locations"; + $this->locations = $this->$version; + + return parent::getInstallPath($package, $frameworkType); + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/EzPlatformInstaller.php b/vendor/composer/installers/src/Composer/Installers/EzPlatformInstaller.php new file mode 100644 index 0000000..f30ebcc --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/EzPlatformInstaller.php @@ -0,0 +1,10 @@ + 'web/assets/ezplatform/', + 'assets' => 'web/assets/ezplatform/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/FuelInstaller.php b/vendor/composer/installers/src/Composer/Installers/FuelInstaller.php new file mode 100644 index 0000000..6eba2e3 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/FuelInstaller.php @@ -0,0 +1,11 @@ + 'fuel/app/modules/{$name}/', + 'package' => 'fuel/packages/{$name}/', + 'theme' => 'fuel/app/themes/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/FuelphpInstaller.php b/vendor/composer/installers/src/Composer/Installers/FuelphpInstaller.php new file mode 100644 index 0000000..29d980b --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/FuelphpInstaller.php @@ -0,0 +1,9 @@ + 'components/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/GravInstaller.php b/vendor/composer/installers/src/Composer/Installers/GravInstaller.php new file mode 100644 index 0000000..dbe63e0 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/GravInstaller.php @@ -0,0 +1,30 @@ + 'user/plugins/{$name}/', + 'theme' => 'user/themes/{$name}/', + ); + + /** + * Format package name + * + * @param array $vars + * + * @return array + */ + public function inflectPackageVars($vars) + { + $restrictedWords = implode('|', array_keys($this->locations)); + + $vars['name'] = strtolower($vars['name']); + $vars['name'] = preg_replace('/^(?:grav-)?(?:(?:'.$restrictedWords.')-)?(.*?)(?:-(?:'.$restrictedWords.'))?$/ui', + '$1', + $vars['name'] + ); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/HuradInstaller.php b/vendor/composer/installers/src/Composer/Installers/HuradInstaller.php new file mode 100644 index 0000000..8fe017f --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/HuradInstaller.php @@ -0,0 +1,25 @@ + 'plugins/{$name}/', + 'theme' => 'plugins/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $nameParts = explode('/', $vars['name']); + foreach ($nameParts as &$value) { + $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); + $value = str_replace(array('-', '_'), ' ', $value); + $value = str_replace(' ', '', ucwords($value)); + } + $vars['name'] = implode('/', $nameParts); + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/ImageCMSInstaller.php b/vendor/composer/installers/src/Composer/Installers/ImageCMSInstaller.php new file mode 100644 index 0000000..5e2142e --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ImageCMSInstaller.php @@ -0,0 +1,11 @@ + 'templates/{$name}/', + 'module' => 'application/modules/{$name}/', + 'library' => 'application/libraries/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/Installer.php b/vendor/composer/installers/src/Composer/Installers/Installer.php new file mode 100644 index 0000000..9c9c24f --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/Installer.php @@ -0,0 +1,298 @@ + 'AimeosInstaller', + 'asgard' => 'AsgardInstaller', + 'attogram' => 'AttogramInstaller', + 'agl' => 'AglInstaller', + 'annotatecms' => 'AnnotateCmsInstaller', + 'bitrix' => 'BitrixInstaller', + 'bonefish' => 'BonefishInstaller', + 'cakephp' => 'CakePHPInstaller', + 'chef' => 'ChefInstaller', + 'civicrm' => 'CiviCrmInstaller', + 'ccframework' => 'ClanCatsFrameworkInstaller', + 'cockpit' => 'CockpitInstaller', + 'codeigniter' => 'CodeIgniterInstaller', + 'concrete5' => 'Concrete5Installer', + 'craft' => 'CraftInstaller', + 'croogo' => 'CroogoInstaller', + 'dframe' => 'DframeInstaller', + 'dokuwiki' => 'DokuWikiInstaller', + 'dolibarr' => 'DolibarrInstaller', + 'decibel' => 'DecibelInstaller', + 'drupal' => 'DrupalInstaller', + 'elgg' => 'ElggInstaller', + 'eliasis' => 'EliasisInstaller', + 'ee3' => 'ExpressionEngineInstaller', + 'ee2' => 'ExpressionEngineInstaller', + 'ezplatform' => 'EzPlatformInstaller', + 'fuel' => 'FuelInstaller', + 'fuelphp' => 'FuelphpInstaller', + 'grav' => 'GravInstaller', + 'hurad' => 'HuradInstaller', + 'tastyigniter' => 'TastyIgniterInstaller', + 'imagecms' => 'ImageCMSInstaller', + 'itop' => 'ItopInstaller', + 'joomla' => 'JoomlaInstaller', + 'kanboard' => 'KanboardInstaller', + 'kirby' => 'KirbyInstaller', + 'known' => 'KnownInstaller', + 'kodicms' => 'KodiCMSInstaller', + 'kohana' => 'KohanaInstaller', + 'lms' => 'LanManagementSystemInstaller', + 'laravel' => 'LaravelInstaller', + 'lavalite' => 'LavaLiteInstaller', + 'lithium' => 'LithiumInstaller', + 'magento' => 'MagentoInstaller', + 'majima' => 'MajimaInstaller', + 'mantisbt' => 'MantisBTInstaller', + 'mako' => 'MakoInstaller', + 'maya' => 'MayaInstaller', + 'mautic' => 'MauticInstaller', + 'mediawiki' => 'MediaWikiInstaller', + 'miaoxing' => 'MiaoxingInstaller', + 'microweber' => 'MicroweberInstaller', + 'modulework' => 'MODULEWorkInstaller', + 'modx' => 'ModxInstaller', + 'modxevo' => 'MODXEvoInstaller', + 'moodle' => 'MoodleInstaller', + 'october' => 'OctoberInstaller', + 'ontowiki' => 'OntoWikiInstaller', + 'oxid' => 'OxidInstaller', + 'osclass' => 'OsclassInstaller', + 'pxcms' => 'PxcmsInstaller', + 'phpbb' => 'PhpBBInstaller', + 'pimcore' => 'PimcoreInstaller', + 'piwik' => 'PiwikInstaller', + 'plentymarkets'=> 'PlentymarketsInstaller', + 'ppi' => 'PPIInstaller', + 'puppet' => 'PuppetInstaller', + 'radphp' => 'RadPHPInstaller', + 'phifty' => 'PhiftyInstaller', + 'porto' => 'PortoInstaller', + 'processwire' => 'ProcessWireInstaller', + 'quicksilver' => 'PantheonInstaller', + 'redaxo' => 'RedaxoInstaller', + 'redaxo5' => 'Redaxo5Installer', + 'reindex' => 'ReIndexInstaller', + 'roundcube' => 'RoundcubeInstaller', + 'shopware' => 'ShopwareInstaller', + 'sitedirect' => 'SiteDirectInstaller', + 'silverstripe' => 'SilverStripeInstaller', + 'smf' => 'SMFInstaller', + 'starbug' => 'StarbugInstaller', + 'sydes' => 'SyDESInstaller', + 'sylius' => 'SyliusInstaller', + 'symfony1' => 'Symfony1Installer', + 'tao' => 'TaoInstaller', + 'thelia' => 'TheliaInstaller', + 'tusk' => 'TuskInstaller', + 'typo3-cms' => 'TYPO3CmsInstaller', + 'typo3-flow' => 'TYPO3FlowInstaller', + 'userfrosting' => 'UserFrostingInstaller', + 'vanilla' => 'VanillaInstaller', + 'whmcs' => 'WHMCSInstaller', + 'winter' => 'WinterInstaller', + 'wolfcms' => 'WolfCMSInstaller', + 'wordpress' => 'WordPressInstaller', + 'yawik' => 'YawikInstaller', + 'zend' => 'ZendInstaller', + 'zikula' => 'ZikulaInstaller', + 'prestashop' => 'PrestashopInstaller' + ); + + /** + * Installer constructor. + * + * Disables installers specified in main composer extra installer-disable + * list + * + * @param IOInterface $io + * @param Composer $composer + * @param string $type + * @param Filesystem|null $filesystem + * @param BinaryInstaller|null $binaryInstaller + */ + public function __construct( + IOInterface $io, + Composer $composer, + $type = 'library', + Filesystem $filesystem = null, + BinaryInstaller $binaryInstaller = null + ) { + parent::__construct($io, $composer, $type, $filesystem, + $binaryInstaller); + $this->removeDisabledInstallers(); + } + + /** + * {@inheritDoc} + */ + public function getInstallPath(PackageInterface $package) + { + $type = $package->getType(); + $frameworkType = $this->findFrameworkType($type); + + if ($frameworkType === false) { + throw new \InvalidArgumentException( + 'Sorry the package type of this package is not yet supported.' + ); + } + + $class = 'Composer\\Installers\\' . $this->supportedTypes[$frameworkType]; + $installer = new $class($package, $this->composer, $this->getIO()); + + return $installer->getInstallPath($package, $frameworkType); + } + + public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) + { + $installPath = $this->getPackageBasePath($package); + $io = $this->io; + $outputStatus = function () use ($io, $installPath) { + $io->write(sprintf('Deleting %s - %s', $installPath, !file_exists($installPath) ? 'deleted' : 'not deleted')); + }; + + $promise = parent::uninstall($repo, $package); + + // Composer v2 might return a promise here + if ($promise instanceof PromiseInterface) { + return $promise->then($outputStatus); + } + + // If not, execute the code right away as parent::uninstall executed synchronously (composer v1, or v2 without async) + $outputStatus(); + + return null; + } + + /** + * {@inheritDoc} + */ + public function supports($packageType) + { + $frameworkType = $this->findFrameworkType($packageType); + + if ($frameworkType === false) { + return false; + } + + $locationPattern = $this->getLocationPattern($frameworkType); + + return preg_match('#' . $frameworkType . '-' . $locationPattern . '#', $packageType, $matches) === 1; + } + + /** + * Finds a supported framework type if it exists and returns it + * + * @param string $type + * @return string|false + */ + protected function findFrameworkType($type) + { + krsort($this->supportedTypes); + + foreach ($this->supportedTypes as $key => $val) { + if ($key === substr($type, 0, strlen($key))) { + return substr($type, 0, strlen($key)); + } + } + + return false; + } + + /** + * Get the second part of the regular expression to check for support of a + * package type + * + * @param string $frameworkType + * @return string + */ + protected function getLocationPattern($frameworkType) + { + $pattern = false; + if (!empty($this->supportedTypes[$frameworkType])) { + $frameworkClass = 'Composer\\Installers\\' . $this->supportedTypes[$frameworkType]; + /** @var BaseInstaller $framework */ + $framework = new $frameworkClass(null, $this->composer, $this->getIO()); + $locations = array_keys($framework->getLocations()); + $pattern = $locations ? '(' . implode('|', $locations) . ')' : false; + } + + return $pattern ? : '(\w+)'; + } + + /** + * Get I/O object + * + * @return IOInterface + */ + private function getIO() + { + return $this->io; + } + + /** + * Look for installers set to be disabled in composer's extra config and + * remove them from the list of supported installers. + * + * Globals: + * - true, "all", and "*" - disable all installers. + * - false - enable all installers (useful with + * wikimedia/composer-merge-plugin or similar) + * + * @return void + */ + protected function removeDisabledInstallers() + { + $extra = $this->composer->getPackage()->getExtra(); + + if (!isset($extra['installer-disable']) || $extra['installer-disable'] === false) { + // No installers are disabled + return; + } + + // Get installers to disable + $disable = $extra['installer-disable']; + + // Ensure $disabled is an array + if (!is_array($disable)) { + $disable = array($disable); + } + + // Check which installers should be disabled + $all = array(true, "all", "*"); + $intersect = array_intersect($all, $disable); + if (!empty($intersect)) { + // Disable all installers + $this->supportedTypes = array(); + } else { + // Disable specified installers + foreach ($disable as $key => $installer) { + if (is_string($installer) && key_exists($installer, $this->supportedTypes)) { + unset($this->supportedTypes[$installer]); + } + } + } + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/ItopInstaller.php b/vendor/composer/installers/src/Composer/Installers/ItopInstaller.php new file mode 100644 index 0000000..c6c1b33 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ItopInstaller.php @@ -0,0 +1,9 @@ + 'extensions/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/JoomlaInstaller.php b/vendor/composer/installers/src/Composer/Installers/JoomlaInstaller.php new file mode 100644 index 0000000..9ee7759 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/JoomlaInstaller.php @@ -0,0 +1,15 @@ + 'components/{$name}/', + 'module' => 'modules/{$name}/', + 'template' => 'templates/{$name}/', + 'plugin' => 'plugins/{$name}/', + 'library' => 'libraries/{$name}/', + ); + + // TODO: Add inflector for mod_ and com_ names +} diff --git a/vendor/composer/installers/src/Composer/Installers/KanboardInstaller.php b/vendor/composer/installers/src/Composer/Installers/KanboardInstaller.php new file mode 100644 index 0000000..9cb7b8c --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/KanboardInstaller.php @@ -0,0 +1,18 @@ + 'plugins/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/KirbyInstaller.php b/vendor/composer/installers/src/Composer/Installers/KirbyInstaller.php new file mode 100644 index 0000000..36b2f84 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/KirbyInstaller.php @@ -0,0 +1,11 @@ + 'site/plugins/{$name}/', + 'field' => 'site/fields/{$name}/', + 'tag' => 'site/tags/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/KnownInstaller.php b/vendor/composer/installers/src/Composer/Installers/KnownInstaller.php new file mode 100644 index 0000000..c5d08c5 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/KnownInstaller.php @@ -0,0 +1,11 @@ + 'IdnoPlugins/{$name}/', + 'theme' => 'Themes/{$name}/', + 'console' => 'ConsolePlugins/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/KodiCMSInstaller.php b/vendor/composer/installers/src/Composer/Installers/KodiCMSInstaller.php new file mode 100644 index 0000000..7143e23 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/KodiCMSInstaller.php @@ -0,0 +1,10 @@ + 'cms/plugins/{$name}/', + 'media' => 'cms/media/vendor/{$name}/' + ); +} \ No newline at end of file diff --git a/vendor/composer/installers/src/Composer/Installers/KohanaInstaller.php b/vendor/composer/installers/src/Composer/Installers/KohanaInstaller.php new file mode 100644 index 0000000..dcd6d26 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/KohanaInstaller.php @@ -0,0 +1,9 @@ + 'modules/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php b/vendor/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php new file mode 100644 index 0000000..903143a --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php @@ -0,0 +1,27 @@ + 'plugins/{$name}/', + 'template' => 'templates/{$name}/', + 'document-template' => 'documents/templates/{$name}/', + 'userpanel-module' => 'userpanel/modules/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } + +} diff --git a/vendor/composer/installers/src/Composer/Installers/LaravelInstaller.php b/vendor/composer/installers/src/Composer/Installers/LaravelInstaller.php new file mode 100644 index 0000000..be4d53a --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/LaravelInstaller.php @@ -0,0 +1,9 @@ + 'libraries/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/LavaLiteInstaller.php b/vendor/composer/installers/src/Composer/Installers/LavaLiteInstaller.php new file mode 100644 index 0000000..412c0b5 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/LavaLiteInstaller.php @@ -0,0 +1,10 @@ + 'packages/{$vendor}/{$name}/', + 'theme' => 'public/themes/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/LithiumInstaller.php b/vendor/composer/installers/src/Composer/Installers/LithiumInstaller.php new file mode 100644 index 0000000..47bbd4c --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/LithiumInstaller.php @@ -0,0 +1,10 @@ + 'libraries/{$name}/', + 'source' => 'libraries/_source/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php b/vendor/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php new file mode 100644 index 0000000..9c2e9fb --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php @@ -0,0 +1,9 @@ + 'modules/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/MODXEvoInstaller.php b/vendor/composer/installers/src/Composer/Installers/MODXEvoInstaller.php new file mode 100644 index 0000000..5a66460 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MODXEvoInstaller.php @@ -0,0 +1,16 @@ + 'assets/snippets/{$name}/', + 'plugin' => 'assets/plugins/{$name}/', + 'module' => 'assets/modules/{$name}/', + 'template' => 'assets/templates/{$name}/', + 'lib' => 'assets/lib/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/MagentoInstaller.php b/vendor/composer/installers/src/Composer/Installers/MagentoInstaller.php new file mode 100644 index 0000000..cf18e94 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MagentoInstaller.php @@ -0,0 +1,11 @@ + 'app/design/frontend/{$name}/', + 'skin' => 'skin/frontend/default/{$name}/', + 'library' => 'lib/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/MajimaInstaller.php b/vendor/composer/installers/src/Composer/Installers/MajimaInstaller.php new file mode 100644 index 0000000..e463756 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MajimaInstaller.php @@ -0,0 +1,37 @@ + 'plugins/{$name}/', + ); + + /** + * Transforms the names + * @param array $vars + * @return array + */ + public function inflectPackageVars($vars) + { + return $this->correctPluginName($vars); + } + + /** + * Change hyphenated names to camelcase + * @param array $vars + * @return array + */ + private function correctPluginName($vars) + { + $camelCasedName = preg_replace_callback('/(-[a-z])/', function ($matches) { + return strtoupper($matches[0][1]); + }, $vars['name']); + $vars['name'] = ucfirst($camelCasedName); + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/MakoInstaller.php b/vendor/composer/installers/src/Composer/Installers/MakoInstaller.php new file mode 100644 index 0000000..ca3cfac --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MakoInstaller.php @@ -0,0 +1,9 @@ + 'app/packages/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/MantisBTInstaller.php b/vendor/composer/installers/src/Composer/Installers/MantisBTInstaller.php new file mode 100644 index 0000000..dadb1db --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MantisBTInstaller.php @@ -0,0 +1,23 @@ + 'plugins/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/MauticInstaller.php b/vendor/composer/installers/src/Composer/Installers/MauticInstaller.php new file mode 100644 index 0000000..c3dd2b6 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MauticInstaller.php @@ -0,0 +1,48 @@ + 'plugins/{$name}/', + 'theme' => 'themes/{$name}/', + 'core' => 'app/', + ); + + private function getDirectoryName() + { + $extra = $this->package->getExtra(); + if (!empty($extra['install-directory-name'])) { + return $extra['install-directory-name']; + } + + return $this->toCamelCase($this->package->getPrettyName()); + } + + /** + * @param string $packageName + * + * @return string + */ + private function toCamelCase($packageName) + { + return str_replace(' ', '', ucwords(str_replace('-', ' ', basename($packageName)))); + } + + /** + * Format package name of mautic-plugins to CamelCase + */ + public function inflectPackageVars($vars) + { + + if ($vars['type'] == 'mautic-plugin' || $vars['type'] == 'mautic-theme') { + $directoryName = $this->getDirectoryName(); + $vars['name'] = $directoryName; + } + + return $vars; + } + +} diff --git a/vendor/composer/installers/src/Composer/Installers/MayaInstaller.php b/vendor/composer/installers/src/Composer/Installers/MayaInstaller.php new file mode 100644 index 0000000..30a9167 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MayaInstaller.php @@ -0,0 +1,33 @@ + 'modules/{$name}/', + ); + + /** + * Format package name. + * + * For package type maya-module, cut off a trailing '-module' if present. + * + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] === 'maya-module') { + return $this->inflectModuleVars($vars); + } + + return $vars; + } + + protected function inflectModuleVars($vars) + { + $vars['name'] = preg_replace('/-module$/', '', $vars['name']); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/MediaWikiInstaller.php b/vendor/composer/installers/src/Composer/Installers/MediaWikiInstaller.php new file mode 100644 index 0000000..f5a8957 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MediaWikiInstaller.php @@ -0,0 +1,51 @@ + 'core/', + 'extension' => 'extensions/{$name}/', + 'skin' => 'skins/{$name}/', + ); + + /** + * Format package name. + * + * For package type mediawiki-extension, cut off a trailing '-extension' if present and transform + * to CamelCase keeping existing uppercase chars. + * + * For package type mediawiki-skin, cut off a trailing '-skin' if present. + * + */ + public function inflectPackageVars($vars) + { + + if ($vars['type'] === 'mediawiki-extension') { + return $this->inflectExtensionVars($vars); + } + + if ($vars['type'] === 'mediawiki-skin') { + return $this->inflectSkinVars($vars); + } + + return $vars; + } + + protected function inflectExtensionVars($vars) + { + $vars['name'] = preg_replace('/-extension$/', '', $vars['name']); + $vars['name'] = str_replace('-', ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } + + protected function inflectSkinVars($vars) + { + $vars['name'] = preg_replace('/-skin$/', '', $vars['name']); + + return $vars; + } + +} diff --git a/vendor/composer/installers/src/Composer/Installers/MiaoxingInstaller.php b/vendor/composer/installers/src/Composer/Installers/MiaoxingInstaller.php new file mode 100644 index 0000000..66d8369 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MiaoxingInstaller.php @@ -0,0 +1,10 @@ + 'plugins/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/MicroweberInstaller.php b/vendor/composer/installers/src/Composer/Installers/MicroweberInstaller.php new file mode 100644 index 0000000..b7d9703 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MicroweberInstaller.php @@ -0,0 +1,119 @@ + 'userfiles/modules/{$install_item_dir}/', + 'module-skin' => 'userfiles/modules/{$install_item_dir}/templates/', + 'template' => 'userfiles/templates/{$install_item_dir}/', + 'element' => 'userfiles/elements/{$install_item_dir}/', + 'vendor' => 'vendor/{$install_item_dir}/', + 'components' => 'components/{$install_item_dir}/' + ); + + /** + * Format package name. + * + * For package type microweber-module, cut off a trailing '-module' if present + * + * For package type microweber-template, cut off a trailing '-template' if present. + * + */ + public function inflectPackageVars($vars) + { + + + if ($this->package->getTargetDir()) { + $vars['install_item_dir'] = $this->package->getTargetDir(); + } else { + $vars['install_item_dir'] = $vars['name']; + if ($vars['type'] === 'microweber-template') { + return $this->inflectTemplateVars($vars); + } + if ($vars['type'] === 'microweber-templates') { + return $this->inflectTemplatesVars($vars); + } + if ($vars['type'] === 'microweber-core') { + return $this->inflectCoreVars($vars); + } + if ($vars['type'] === 'microweber-adapter') { + return $this->inflectCoreVars($vars); + } + if ($vars['type'] === 'microweber-module') { + return $this->inflectModuleVars($vars); + } + if ($vars['type'] === 'microweber-modules') { + return $this->inflectModulesVars($vars); + } + if ($vars['type'] === 'microweber-skin') { + return $this->inflectSkinVars($vars); + } + if ($vars['type'] === 'microweber-element' or $vars['type'] === 'microweber-elements') { + return $this->inflectElementVars($vars); + } + } + + + return $vars; + } + + protected function inflectTemplateVars($vars) + { + $vars['install_item_dir'] = preg_replace('/-template$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/template-$/', '', $vars['install_item_dir']); + + return $vars; + } + + protected function inflectTemplatesVars($vars) + { + $vars['install_item_dir'] = preg_replace('/-templates$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/templates-$/', '', $vars['install_item_dir']); + + return $vars; + } + + protected function inflectCoreVars($vars) + { + $vars['install_item_dir'] = preg_replace('/-providers$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/-provider$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/-adapter$/', '', $vars['install_item_dir']); + + return $vars; + } + + protected function inflectModuleVars($vars) + { + $vars['install_item_dir'] = preg_replace('/-module$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/module-$/', '', $vars['install_item_dir']); + + return $vars; + } + + protected function inflectModulesVars($vars) + { + $vars['install_item_dir'] = preg_replace('/-modules$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/modules-$/', '', $vars['install_item_dir']); + + return $vars; + } + + protected function inflectSkinVars($vars) + { + $vars['install_item_dir'] = preg_replace('/-skin$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/skin-$/', '', $vars['install_item_dir']); + + return $vars; + } + + protected function inflectElementVars($vars) + { + $vars['install_item_dir'] = preg_replace('/-elements$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/elements-$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/-element$/', '', $vars['install_item_dir']); + $vars['install_item_dir'] = preg_replace('/element-$/', '', $vars['install_item_dir']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/ModxInstaller.php b/vendor/composer/installers/src/Composer/Installers/ModxInstaller.php new file mode 100644 index 0000000..0ee140a --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ModxInstaller.php @@ -0,0 +1,12 @@ + 'core/packages/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/MoodleInstaller.php b/vendor/composer/installers/src/Composer/Installers/MoodleInstaller.php new file mode 100644 index 0000000..0531799 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/MoodleInstaller.php @@ -0,0 +1,59 @@ + 'mod/{$name}/', + 'admin_report' => 'admin/report/{$name}/', + 'atto' => 'lib/editor/atto/plugins/{$name}/', + 'tool' => 'admin/tool/{$name}/', + 'assignment' => 'mod/assignment/type/{$name}/', + 'assignsubmission' => 'mod/assign/submission/{$name}/', + 'assignfeedback' => 'mod/assign/feedback/{$name}/', + 'auth' => 'auth/{$name}/', + 'availability' => 'availability/condition/{$name}/', + 'block' => 'blocks/{$name}/', + 'booktool' => 'mod/book/tool/{$name}/', + 'cachestore' => 'cache/stores/{$name}/', + 'cachelock' => 'cache/locks/{$name}/', + 'calendartype' => 'calendar/type/{$name}/', + 'fileconverter' => 'files/converter/{$name}/', + 'format' => 'course/format/{$name}/', + 'coursereport' => 'course/report/{$name}/', + 'customcertelement' => 'mod/customcert/element/{$name}/', + 'datafield' => 'mod/data/field/{$name}/', + 'datapreset' => 'mod/data/preset/{$name}/', + 'editor' => 'lib/editor/{$name}/', + 'enrol' => 'enrol/{$name}/', + 'filter' => 'filter/{$name}/', + 'gradeexport' => 'grade/export/{$name}/', + 'gradeimport' => 'grade/import/{$name}/', + 'gradereport' => 'grade/report/{$name}/', + 'gradingform' => 'grade/grading/form/{$name}/', + 'local' => 'local/{$name}/', + 'logstore' => 'admin/tool/log/store/{$name}/', + 'ltisource' => 'mod/lti/source/{$name}/', + 'ltiservice' => 'mod/lti/service/{$name}/', + 'message' => 'message/output/{$name}/', + 'mnetservice' => 'mnet/service/{$name}/', + 'plagiarism' => 'plagiarism/{$name}/', + 'portfolio' => 'portfolio/{$name}/', + 'qbehaviour' => 'question/behaviour/{$name}/', + 'qformat' => 'question/format/{$name}/', + 'qtype' => 'question/type/{$name}/', + 'quizaccess' => 'mod/quiz/accessrule/{$name}/', + 'quiz' => 'mod/quiz/report/{$name}/', + 'report' => 'report/{$name}/', + 'repository' => 'repository/{$name}/', + 'scormreport' => 'mod/scorm/report/{$name}/', + 'search' => 'search/engine/{$name}/', + 'theme' => 'theme/{$name}/', + 'tinymce' => 'lib/editor/tinymce/plugins/{$name}/', + 'profilefield' => 'user/profile/field/{$name}/', + 'webservice' => 'webservice/{$name}/', + 'workshopallocation' => 'mod/workshop/allocation/{$name}/', + 'workshopeval' => 'mod/workshop/eval/{$name}/', + 'workshopform' => 'mod/workshop/form/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/OctoberInstaller.php b/vendor/composer/installers/src/Composer/Installers/OctoberInstaller.php new file mode 100644 index 0000000..489ef02 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/OctoberInstaller.php @@ -0,0 +1,48 @@ + 'modules/{$name}/', + 'plugin' => 'plugins/{$vendor}/{$name}/', + 'theme' => 'themes/{$vendor}-{$name}/' + ); + + /** + * Format package name. + * + * For package type october-plugin, cut off a trailing '-plugin' if present. + * + * For package type october-theme, cut off a trailing '-theme' if present. + * + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] === 'october-plugin') { + return $this->inflectPluginVars($vars); + } + + if ($vars['type'] === 'october-theme') { + return $this->inflectThemeVars($vars); + } + + return $vars; + } + + protected function inflectPluginVars($vars) + { + $vars['name'] = preg_replace('/^oc-|-plugin$/', '', $vars['name']); + $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); + + return $vars; + } + + protected function inflectThemeVars($vars) + { + $vars['name'] = preg_replace('/^oc-|-theme$/', '', $vars['name']); + $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/OntoWikiInstaller.php b/vendor/composer/installers/src/Composer/Installers/OntoWikiInstaller.php new file mode 100644 index 0000000..5dd3438 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/OntoWikiInstaller.php @@ -0,0 +1,24 @@ + 'extensions/{$name}/', + 'theme' => 'extensions/themes/{$name}/', + 'translation' => 'extensions/translations/{$name}/', + ); + + /** + * Format package name to lower case and remove ".ontowiki" suffix + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower($vars['name']); + $vars['name'] = preg_replace('/.ontowiki$/', '', $vars['name']); + $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); + $vars['name'] = preg_replace('/-translation$/', '', $vars['name']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/OsclassInstaller.php b/vendor/composer/installers/src/Composer/Installers/OsclassInstaller.php new file mode 100644 index 0000000..3ca7954 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/OsclassInstaller.php @@ -0,0 +1,14 @@ + 'oc-content/plugins/{$name}/', + 'theme' => 'oc-content/themes/{$name}/', + 'language' => 'oc-content/languages/{$name}/', + ); + +} diff --git a/vendor/composer/installers/src/Composer/Installers/OxidInstaller.php b/vendor/composer/installers/src/Composer/Installers/OxidInstaller.php new file mode 100644 index 0000000..1797a22 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/OxidInstaller.php @@ -0,0 +1,59 @@ +.+)\/.+/'; + + protected $locations = array( + 'module' => 'modules/{$name}/', + 'theme' => 'application/views/{$name}/', + 'out' => 'out/{$name}/', + ); + + /** + * getInstallPath + * + * @param PackageInterface $package + * @param string $frameworkType + * @return string + */ + public function getInstallPath(PackageInterface $package, $frameworkType = '') + { + $installPath = parent::getInstallPath($package, $frameworkType); + $type = $this->package->getType(); + if ($type === 'oxid-module') { + $this->prepareVendorDirectory($installPath); + } + return $installPath; + } + + /** + * prepareVendorDirectory + * + * Makes sure there is a vendormetadata.php file inside + * the vendor folder if there is a vendor folder. + * + * @param string $installPath + * @return void + */ + protected function prepareVendorDirectory($installPath) + { + $matches = ''; + $hasVendorDirectory = preg_match(self::VENDOR_PATTERN, $installPath, $matches); + if (!$hasVendorDirectory) { + return; + } + + $vendorDirectory = $matches['vendor']; + $vendorPath = getcwd() . '/modules/' . $vendorDirectory; + if (!file_exists($vendorPath)) { + mkdir($vendorPath, 0755, true); + } + + $vendorMetaDataPath = $vendorPath . '/vendormetadata.php'; + touch($vendorMetaDataPath); + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/PPIInstaller.php b/vendor/composer/installers/src/Composer/Installers/PPIInstaller.php new file mode 100644 index 0000000..170136f --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PPIInstaller.php @@ -0,0 +1,9 @@ + 'modules/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/PantheonInstaller.php b/vendor/composer/installers/src/Composer/Installers/PantheonInstaller.php new file mode 100644 index 0000000..439f61a --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PantheonInstaller.php @@ -0,0 +1,12 @@ + */ + protected $locations = array( + 'script' => 'web/private/scripts/quicksilver/{$name}', + 'module' => 'web/private/scripts/quicksilver/{$name}', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/PhiftyInstaller.php b/vendor/composer/installers/src/Composer/Installers/PhiftyInstaller.php new file mode 100644 index 0000000..4e59a8a --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PhiftyInstaller.php @@ -0,0 +1,11 @@ + 'bundles/{$name}/', + 'library' => 'libraries/{$name}/', + 'framework' => 'frameworks/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/PhpBBInstaller.php b/vendor/composer/installers/src/Composer/Installers/PhpBBInstaller.php new file mode 100644 index 0000000..deb2b77 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PhpBBInstaller.php @@ -0,0 +1,11 @@ + 'ext/{$vendor}/{$name}/', + 'language' => 'language/{$name}/', + 'style' => 'styles/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/PimcoreInstaller.php b/vendor/composer/installers/src/Composer/Installers/PimcoreInstaller.php new file mode 100644 index 0000000..4781fa6 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PimcoreInstaller.php @@ -0,0 +1,21 @@ + 'plugins/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/PiwikInstaller.php b/vendor/composer/installers/src/Composer/Installers/PiwikInstaller.php new file mode 100644 index 0000000..c17f457 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PiwikInstaller.php @@ -0,0 +1,32 @@ + 'plugins/{$name}/', + ); + + /** + * Format package name to CamelCase + * @param array $vars + * + * @return array + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php b/vendor/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php new file mode 100644 index 0000000..903e55f --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php @@ -0,0 +1,29 @@ + '{$name}/' + ); + + /** + * Remove hyphen, "plugin" and format to camelcase + * @param array $vars + * + * @return array + */ + public function inflectPackageVars($vars) + { + $vars['name'] = explode("-", $vars['name']); + foreach ($vars['name'] as $key => $name) { + $vars['name'][$key] = ucfirst($vars['name'][$key]); + if (strcasecmp($name, "Plugin") == 0) { + unset($vars['name'][$key]); + } + } + $vars['name'] = implode("",$vars['name']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/Plugin.php b/vendor/composer/installers/src/Composer/Installers/Plugin.php new file mode 100644 index 0000000..e60da0e --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/Plugin.php @@ -0,0 +1,27 @@ +installer = new Installer($io, $composer); + $composer->getInstallationManager()->addInstaller($this->installer); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + $composer->getInstallationManager()->removeInstaller($this->installer); + } + + public function uninstall(Composer $composer, IOInterface $io) + { + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/PortoInstaller.php b/vendor/composer/installers/src/Composer/Installers/PortoInstaller.php new file mode 100644 index 0000000..dbf85e6 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PortoInstaller.php @@ -0,0 +1,9 @@ + 'app/Containers/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/PrestashopInstaller.php b/vendor/composer/installers/src/Composer/Installers/PrestashopInstaller.php new file mode 100644 index 0000000..4c8421e --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PrestashopInstaller.php @@ -0,0 +1,10 @@ + 'modules/{$name}/', + 'theme' => 'themes/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/ProcessWireInstaller.php b/vendor/composer/installers/src/Composer/Installers/ProcessWireInstaller.php new file mode 100644 index 0000000..e6834a0 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ProcessWireInstaller.php @@ -0,0 +1,22 @@ + 'site/modules/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/PuppetInstaller.php b/vendor/composer/installers/src/Composer/Installers/PuppetInstaller.php new file mode 100644 index 0000000..77cc3dd --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PuppetInstaller.php @@ -0,0 +1,11 @@ + 'modules/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/PxcmsInstaller.php b/vendor/composer/installers/src/Composer/Installers/PxcmsInstaller.php new file mode 100644 index 0000000..6551058 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/PxcmsInstaller.php @@ -0,0 +1,63 @@ + 'app/Modules/{$name}/', + 'theme' => 'themes/{$name}/', + ); + + /** + * Format package name. + * + * @param array $vars + * + * @return array + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] === 'pxcms-module') { + return $this->inflectModuleVars($vars); + } + + if ($vars['type'] === 'pxcms-theme') { + return $this->inflectThemeVars($vars); + } + + return $vars; + } + + /** + * For package type pxcms-module, cut off a trailing '-plugin' if present. + * + * return string + */ + protected function inflectModuleVars($vars) + { + $vars['name'] = str_replace('pxcms-', '', $vars['name']); // strip out pxcms- just incase (legacy) + $vars['name'] = str_replace('module-', '', $vars['name']); // strip out module- + $vars['name'] = preg_replace('/-module$/', '', $vars['name']); // strip out -module + $vars['name'] = str_replace('-', '_', $vars['name']); // make -'s be _'s + $vars['name'] = ucwords($vars['name']); // make module name camelcased + + return $vars; + } + + + /** + * For package type pxcms-module, cut off a trailing '-plugin' if present. + * + * return string + */ + protected function inflectThemeVars($vars) + { + $vars['name'] = str_replace('pxcms-', '', $vars['name']); // strip out pxcms- just incase (legacy) + $vars['name'] = str_replace('theme-', '', $vars['name']); // strip out theme- + $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); // strip out -theme + $vars['name'] = str_replace('-', '_', $vars['name']); // make -'s be _'s + $vars['name'] = ucwords($vars['name']); // make module name camelcased + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/RadPHPInstaller.php b/vendor/composer/installers/src/Composer/Installers/RadPHPInstaller.php new file mode 100644 index 0000000..0f78b5c --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/RadPHPInstaller.php @@ -0,0 +1,24 @@ + 'src/{$name}/' + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $nameParts = explode('/', $vars['name']); + foreach ($nameParts as &$value) { + $value = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $value)); + $value = str_replace(array('-', '_'), ' ', $value); + $value = str_replace(' ', '', ucwords($value)); + } + $vars['name'] = implode('/', $nameParts); + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/ReIndexInstaller.php b/vendor/composer/installers/src/Composer/Installers/ReIndexInstaller.php new file mode 100644 index 0000000..252c733 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ReIndexInstaller.php @@ -0,0 +1,10 @@ + 'themes/{$name}/', + 'plugin' => 'plugins/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/Redaxo5Installer.php b/vendor/composer/installers/src/Composer/Installers/Redaxo5Installer.php new file mode 100644 index 0000000..23a2034 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/Redaxo5Installer.php @@ -0,0 +1,10 @@ + 'redaxo/src/addons/{$name}/', + 'bestyle-plugin' => 'redaxo/src/addons/be_style/plugins/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/RedaxoInstaller.php b/vendor/composer/installers/src/Composer/Installers/RedaxoInstaller.php new file mode 100644 index 0000000..0954457 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/RedaxoInstaller.php @@ -0,0 +1,10 @@ + 'redaxo/include/addons/{$name}/', + 'bestyle-plugin' => 'redaxo/include/addons/be_style/plugins/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/RoundcubeInstaller.php b/vendor/composer/installers/src/Composer/Installers/RoundcubeInstaller.php new file mode 100644 index 0000000..d8d795b --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/RoundcubeInstaller.php @@ -0,0 +1,22 @@ + 'plugins/{$name}/', + ); + + /** + * Lowercase name and changes the name to a underscores + * + * @param array $vars + * @return array + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower(str_replace('-', '_', $vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/SMFInstaller.php b/vendor/composer/installers/src/Composer/Installers/SMFInstaller.php new file mode 100644 index 0000000..1acd3b1 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/SMFInstaller.php @@ -0,0 +1,10 @@ + 'Sources/{$name}/', + 'theme' => 'Themes/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/ShopwareInstaller.php b/vendor/composer/installers/src/Composer/Installers/ShopwareInstaller.php new file mode 100644 index 0000000..7d20d27 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ShopwareInstaller.php @@ -0,0 +1,60 @@ + 'engine/Shopware/Plugins/Local/Backend/{$name}/', + 'core-plugin' => 'engine/Shopware/Plugins/Local/Core/{$name}/', + 'frontend-plugin' => 'engine/Shopware/Plugins/Local/Frontend/{$name}/', + 'theme' => 'templates/{$name}/', + 'plugin' => 'custom/plugins/{$name}/', + 'frontend-theme' => 'themes/Frontend/{$name}/', + ); + + /** + * Transforms the names + * @param array $vars + * @return array + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] === 'shopware-theme') { + return $this->correctThemeName($vars); + } + + return $this->correctPluginName($vars); + } + + /** + * Changes the name to a camelcased combination of vendor and name + * @param array $vars + * @return array + */ + private function correctPluginName($vars) + { + $camelCasedName = preg_replace_callback('/(-[a-z])/', function ($matches) { + return strtoupper($matches[0][1]); + }, $vars['name']); + + $vars['name'] = ucfirst($vars['vendor']) . ucfirst($camelCasedName); + + return $vars; + } + + /** + * Changes the name to a underscore separated name + * @param array $vars + * @return array + */ + private function correctThemeName($vars) + { + $vars['name'] = str_replace('-', '_', $vars['name']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/SilverStripeInstaller.php b/vendor/composer/installers/src/Composer/Installers/SilverStripeInstaller.php new file mode 100644 index 0000000..81910e9 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/SilverStripeInstaller.php @@ -0,0 +1,35 @@ + '{$name}/', + 'theme' => 'themes/{$name}/', + ); + + /** + * Return the install path based on package type. + * + * Relies on built-in BaseInstaller behaviour with one exception: silverstripe/framework + * must be installed to 'sapphire' and not 'framework' if the version is <3.0.0 + * + * @param PackageInterface $package + * @param string $frameworkType + * @return string + */ + public function getInstallPath(PackageInterface $package, $frameworkType = '') + { + if ( + $package->getName() == 'silverstripe/framework' + && preg_match('/^\d+\.\d+\.\d+/', $package->getVersion()) + && version_compare($package->getVersion(), '2.999.999') < 0 + ) { + return $this->templatePath($this->locations['module'], array('name' => 'sapphire')); + } + + return parent::getInstallPath($package, $frameworkType); + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/SiteDirectInstaller.php b/vendor/composer/installers/src/Composer/Installers/SiteDirectInstaller.php new file mode 100644 index 0000000..762d94c --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/SiteDirectInstaller.php @@ -0,0 +1,25 @@ + 'modules/{$vendor}/{$name}/', + 'plugin' => 'plugins/{$vendor}/{$name}/' + ); + + public function inflectPackageVars($vars) + { + return $this->parseVars($vars); + } + + protected function parseVars($vars) + { + $vars['vendor'] = strtolower($vars['vendor']) == 'sitedirect' ? 'SiteDirect' : $vars['vendor']; + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/StarbugInstaller.php b/vendor/composer/installers/src/Composer/Installers/StarbugInstaller.php new file mode 100644 index 0000000..a31c9fd --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/StarbugInstaller.php @@ -0,0 +1,12 @@ + 'modules/{$name}/', + 'theme' => 'themes/{$name}/', + 'custom-module' => 'app/modules/{$name}/', + 'custom-theme' => 'app/themes/{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/SyDESInstaller.php b/vendor/composer/installers/src/Composer/Installers/SyDESInstaller.php new file mode 100644 index 0000000..8626a9b --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/SyDESInstaller.php @@ -0,0 +1,47 @@ + 'app/modules/{$name}/', + 'theme' => 'themes/{$name}/', + ); + + /** + * Format module name. + * + * Strip `sydes-` prefix and a trailing '-theme' or '-module' from package name if present. + * + * {@inerhitDoc} + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] == 'sydes-module') { + return $this->inflectModuleVars($vars); + } + + if ($vars['type'] === 'sydes-theme') { + return $this->inflectThemeVars($vars); + } + + return $vars; + } + + public function inflectModuleVars($vars) + { + $vars['name'] = preg_replace('/(^sydes-|-module$)/i', '', $vars['name']); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } + + protected function inflectThemeVars($vars) + { + $vars['name'] = preg_replace('/(^sydes-|-theme$)/', '', $vars['name']); + $vars['name'] = strtolower($vars['name']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/SyliusInstaller.php b/vendor/composer/installers/src/Composer/Installers/SyliusInstaller.php new file mode 100644 index 0000000..4357a35 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/SyliusInstaller.php @@ -0,0 +1,9 @@ + 'themes/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/Symfony1Installer.php b/vendor/composer/installers/src/Composer/Installers/Symfony1Installer.php new file mode 100644 index 0000000..1675c4f --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/Symfony1Installer.php @@ -0,0 +1,26 @@ + + */ +class Symfony1Installer extends BaseInstaller +{ + protected $locations = array( + 'plugin' => 'plugins/{$name}/', + ); + + /** + * Format package name to CamelCase + */ + public function inflectPackageVars($vars) + { + $vars['name'] = preg_replace_callback('/(-[a-z])/', function ($matches) { + return strtoupper($matches[0][1]); + }, $vars['name']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php b/vendor/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php new file mode 100644 index 0000000..b1663e8 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php @@ -0,0 +1,16 @@ + + */ +class TYPO3CmsInstaller extends BaseInstaller +{ + protected $locations = array( + 'extension' => 'typo3conf/ext/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php b/vendor/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php new file mode 100644 index 0000000..42572f4 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php @@ -0,0 +1,38 @@ + 'Packages/Application/{$name}/', + 'framework' => 'Packages/Framework/{$name}/', + 'plugin' => 'Packages/Plugins/{$name}/', + 'site' => 'Packages/Sites/{$name}/', + 'boilerplate' => 'Packages/Boilerplates/{$name}/', + 'build' => 'Build/{$name}/', + ); + + /** + * Modify the package name to be a TYPO3 Flow style key. + * + * @param array $vars + * @return array + */ + public function inflectPackageVars($vars) + { + $autoload = $this->package->getAutoload(); + if (isset($autoload['psr-0']) && is_array($autoload['psr-0'])) { + $namespace = key($autoload['psr-0']); + $vars['name'] = str_replace('\\', '.', $namespace); + } + if (isset($autoload['psr-4']) && is_array($autoload['psr-4'])) { + $namespace = key($autoload['psr-4']); + $vars['name'] = rtrim(str_replace('\\', '.', $namespace), '.'); + } + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/TaoInstaller.php b/vendor/composer/installers/src/Composer/Installers/TaoInstaller.php new file mode 100644 index 0000000..4f79a45 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/TaoInstaller.php @@ -0,0 +1,30 @@ + '{$name}' + ); + + public function inflectPackageVars($vars) + { + $extra = $this->package->getExtra(); + + if (array_key_exists(self::EXTRA_TAO_EXTENSION_NAME, $extra)) { + $vars['name'] = $extra[self::EXTRA_TAO_EXTENSION_NAME]; + return $vars; + } + + $vars['name'] = str_replace('extension-', '', $vars['name']); + $vars['name'] = str_replace('-', ' ', $vars['name']); + $vars['name'] = lcfirst(str_replace(' ', '', ucwords($vars['name']))); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php b/vendor/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php new file mode 100644 index 0000000..e20e65b --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php @@ -0,0 +1,32 @@ + 'extensions/{$vendor}/{$name}/', + 'theme' => 'themes/{$name}/', + ); + + /** + * Format package name. + * + * Cut off leading 'ti-ext-' or 'ti-theme-' if present. + * Strip vendor name of characters that is not alphanumeric or an underscore + * + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] === 'tastyigniter-extension') { + $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); + $vars['name'] = preg_replace('/^ti-ext-/', '', $vars['name']); + } + + if ($vars['type'] === 'tastyigniter-theme') { + $vars['name'] = preg_replace('/^ti-theme-/', '', $vars['name']); + } + + return $vars; + } +} \ No newline at end of file diff --git a/vendor/composer/installers/src/Composer/Installers/TheliaInstaller.php b/vendor/composer/installers/src/Composer/Installers/TheliaInstaller.php new file mode 100644 index 0000000..158af52 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/TheliaInstaller.php @@ -0,0 +1,12 @@ + 'local/modules/{$name}/', + 'frontoffice-template' => 'templates/frontOffice/{$name}/', + 'backoffice-template' => 'templates/backOffice/{$name}/', + 'email-template' => 'templates/email/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/TuskInstaller.php b/vendor/composer/installers/src/Composer/Installers/TuskInstaller.php new file mode 100644 index 0000000..7c0113b --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/TuskInstaller.php @@ -0,0 +1,14 @@ + + */ + class TuskInstaller extends BaseInstaller + { + protected $locations = array( + 'task' => '.tusk/tasks/{$name}/', + 'command' => '.tusk/commands/{$name}/', + 'asset' => 'assets/tusk/{$name}/', + ); + } diff --git a/vendor/composer/installers/src/Composer/Installers/UserFrostingInstaller.php b/vendor/composer/installers/src/Composer/Installers/UserFrostingInstaller.php new file mode 100644 index 0000000..fcb414a --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/UserFrostingInstaller.php @@ -0,0 +1,9 @@ + 'app/sprinkles/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/VanillaInstaller.php b/vendor/composer/installers/src/Composer/Installers/VanillaInstaller.php new file mode 100644 index 0000000..24ca645 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/VanillaInstaller.php @@ -0,0 +1,10 @@ + 'plugins/{$name}/', + 'theme' => 'themes/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/VgmcpInstaller.php b/vendor/composer/installers/src/Composer/Installers/VgmcpInstaller.php new file mode 100644 index 0000000..7d90c5e --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/VgmcpInstaller.php @@ -0,0 +1,49 @@ + 'src/{$vendor}/{$name}/', + 'theme' => 'themes/{$name}/' + ); + + /** + * Format package name. + * + * For package type vgmcp-bundle, cut off a trailing '-bundle' if present. + * + * For package type vgmcp-theme, cut off a trailing '-theme' if present. + * + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] === 'vgmcp-bundle') { + return $this->inflectPluginVars($vars); + } + + if ($vars['type'] === 'vgmcp-theme') { + return $this->inflectThemeVars($vars); + } + + return $vars; + } + + protected function inflectPluginVars($vars) + { + $vars['name'] = preg_replace('/-bundle$/', '', $vars['name']); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } + + protected function inflectThemeVars($vars) + { + $vars['name'] = preg_replace('/-theme$/', '', $vars['name']); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/WHMCSInstaller.php b/vendor/composer/installers/src/Composer/Installers/WHMCSInstaller.php new file mode 100644 index 0000000..b65dbba --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/WHMCSInstaller.php @@ -0,0 +1,21 @@ + 'modules/addons/{$vendor}_{$name}/', + 'fraud' => 'modules/fraud/{$vendor}_{$name}/', + 'gateways' => 'modules/gateways/{$vendor}_{$name}/', + 'notifications' => 'modules/notifications/{$vendor}_{$name}/', + 'registrars' => 'modules/registrars/{$vendor}_{$name}/', + 'reports' => 'modules/reports/{$vendor}_{$name}/', + 'security' => 'modules/security/{$vendor}_{$name}/', + 'servers' => 'modules/servers/{$vendor}_{$name}/', + 'social' => 'modules/social/{$vendor}_{$name}/', + 'support' => 'modules/support/{$vendor}_{$name}/', + 'templates' => 'templates/{$vendor}_{$name}/', + 'includes' => 'includes/{$vendor}_{$name}/' + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/WinterInstaller.php b/vendor/composer/installers/src/Composer/Installers/WinterInstaller.php new file mode 100644 index 0000000..cff1bf1 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/WinterInstaller.php @@ -0,0 +1,58 @@ + 'modules/{$name}/', + 'plugin' => 'plugins/{$vendor}/{$name}/', + 'theme' => 'themes/{$name}/' + ); + + /** + * Format package name. + * + * For package type winter-plugin, cut off a trailing '-plugin' if present. + * + * For package type winter-theme, cut off a trailing '-theme' if present. + * + */ + public function inflectPackageVars($vars) + { + if ($vars['type'] === 'winter-module') { + return $this->inflectModuleVars($vars); + } + + if ($vars['type'] === 'winter-plugin') { + return $this->inflectPluginVars($vars); + } + + if ($vars['type'] === 'winter-theme') { + return $this->inflectThemeVars($vars); + } + + return $vars; + } + + protected function inflectModuleVars($vars) + { + $vars['name'] = preg_replace('/^wn-|-module$/', '', $vars['name']); + + return $vars; + } + + protected function inflectPluginVars($vars) + { + $vars['name'] = preg_replace('/^wn-|-plugin$/', '', $vars['name']); + $vars['vendor'] = preg_replace('/[^a-z0-9_]/i', '', $vars['vendor']); + + return $vars; + } + + protected function inflectThemeVars($vars) + { + $vars['name'] = preg_replace('/^wn-|-theme$/', '', $vars['name']); + + return $vars; + } +} diff --git a/vendor/composer/installers/src/Composer/Installers/WolfCMSInstaller.php b/vendor/composer/installers/src/Composer/Installers/WolfCMSInstaller.php new file mode 100644 index 0000000..cb38788 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/WolfCMSInstaller.php @@ -0,0 +1,9 @@ + 'wolf/plugins/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/WordPressInstaller.php b/vendor/composer/installers/src/Composer/Installers/WordPressInstaller.php new file mode 100644 index 0000000..91c46ad --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/WordPressInstaller.php @@ -0,0 +1,12 @@ + 'wp-content/plugins/{$name}/', + 'theme' => 'wp-content/themes/{$name}/', + 'muplugin' => 'wp-content/mu-plugins/{$name}/', + 'dropin' => 'wp-content/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/YawikInstaller.php b/vendor/composer/installers/src/Composer/Installers/YawikInstaller.php new file mode 100644 index 0000000..27f429f --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/YawikInstaller.php @@ -0,0 +1,32 @@ + 'module/{$name}/', + ); + + /** + * Format package name to CamelCase + * @param array $vars + * + * @return array + */ + public function inflectPackageVars($vars) + { + $vars['name'] = strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $vars['name'])); + $vars['name'] = str_replace(array('-', '_'), ' ', $vars['name']); + $vars['name'] = str_replace(' ', '', ucwords($vars['name'])); + + return $vars; + } +} \ No newline at end of file diff --git a/vendor/composer/installers/src/Composer/Installers/ZendInstaller.php b/vendor/composer/installers/src/Composer/Installers/ZendInstaller.php new file mode 100644 index 0000000..bde9bc8 --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ZendInstaller.php @@ -0,0 +1,11 @@ + 'library/{$name}/', + 'extra' => 'extras/library/{$name}/', + 'module' => 'module/{$name}/', + ); +} diff --git a/vendor/composer/installers/src/Composer/Installers/ZikulaInstaller.php b/vendor/composer/installers/src/Composer/Installers/ZikulaInstaller.php new file mode 100644 index 0000000..56cdf5d --- /dev/null +++ b/vendor/composer/installers/src/Composer/Installers/ZikulaInstaller.php @@ -0,0 +1,10 @@ + 'modules/{$vendor}-{$name}/', + 'theme' => 'themes/{$vendor}-{$name}/' + ); +} diff --git a/vendor/composer/installers/src/bootstrap.php b/vendor/composer/installers/src/bootstrap.php new file mode 100644 index 0000000..0de276e --- /dev/null +++ b/vendor/composer/installers/src/bootstrap.php @@ -0,0 +1,13 @@ + array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/XPathExpr.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Translator' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Translator.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/NodeExtension.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/HtmlExtension.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/AbstractExtension.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/FunctionExtension.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/Extension/CombinationExtension.php' + ), + 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/XPath/TranslatorInterface.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\XPath\\TranslatorTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/XPath/TranslatorTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\CssSelectorConverterTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/CssSelectorConverterTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\HashNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/HashNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\NegationNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/NegationNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\AbstractNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/AbstractNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\SelectorNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/SelectorNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\PseudoNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/PseudoNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\ClassNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/ClassNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\AttributeNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/AttributeNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\FunctionNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/FunctionNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\SpecificityTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/SpecificityTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\CombinedSelectorNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/CombinedSelectorNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Node\\ElementNodeTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Node/ElementNodeTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\ParserTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/ParserTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\ReaderTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/ReaderTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\TokenStreamTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/TokenStreamTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Shortcut\\HashParserTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Shortcut/HashParserTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Shortcut\\ClassParserTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Shortcut/ClassParserTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Shortcut\\ElementParserTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Shortcut/ElementParserTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Shortcut\\EmptyStringParserTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Shortcut/EmptyStringParserTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\CommentHandlerTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/CommentHandlerTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\HashHandlerTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/HashHandlerTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\AbstractHandlerTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/AbstractHandlerTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\IdentifierHandlerTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/IdentifierHandlerTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\WhitespaceHandlerTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/WhitespaceHandlerTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\NumberHandlerTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/NumberHandlerTest.php' + ), + 'Symfony\\Component\\CssSelector\\Tests\\Parser\\Handler\\StringHandlerTest' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Tests/Parser/Handler/StringHandlerTest.php' + ), + 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Exception/ExpressionErrorException.php' + ), + 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Exception/InternalErrorException.php' + ), + 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Exception/ParseException.php' + ), + 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Exception/ExceptionInterface.php' + ), + 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Exception/SyntaxErrorException.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/SelectorNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/ClassNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\HashNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/HashNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/FunctionNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/AttributeNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/NegationNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/CombinedSelectorNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/ElementNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/NodeInterface.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\Specificity' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/Specificity.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/AbstractNode.php' + ), + 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Node/PseudoNode.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/TokenStream.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Token' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Token.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Reader' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Reader.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ClassParser.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ElementParser.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/HashParser.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/NumberHandler.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/HandlerInterface.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/StringHandler.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/CommentHandler.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Handler/HashHandler.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\Parser' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/Parser.php' + ), + 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/Parser/ParserInterface.php' + ), + 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => array( + 'version' => '3.3.6.0', + 'path' => $vendorDir . '/symfony/css-selector/CssSelectorConverter.php' + ), + 'Psr\\Container\\NotFoundExceptionInterface' => array( + 'version' => '1.0.0.0', + 'path' => $vendorDir . '/psr/container/src/NotFoundExceptionInterface.php' + ), + 'Psr\\Container\\ContainerExceptionInterface' => array( + 'version' => '1.0.0.0', + 'path' => $vendorDir . '/psr/container/src/ContainerExceptionInterface.php' + ), + 'Psr\\Container\\ContainerInterface' => array( + 'version' => '1.0.0.0', + 'path' => $vendorDir . '/psr/container/src/ContainerInterface.php' + ), + 'Pelago\\Emogrifier' => array( + 'version' => '3.1.0.0', + 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier.php' + ), + 'Pelago\\Emogrifier\\CssInliner' => array( + 'version' => '3.1.0.0', + 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/CssInliner.php' + ), + 'Pelago\\Emogrifier\\HtmlProcessor\\CssToAttributeConverter' => array( + 'version' => '3.1.0.0', + 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/CssToAttributeConverter.php' + ), + 'Pelago\\Emogrifier\\HtmlProcessor\\AbstractHtmlProcessor' => array( + 'version' => '3.1.0.0', + 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/AbstractHtmlProcessor.php' + ), + 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlNormalizer' => array( + 'version' => '3.1.0.0', + 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlNormalizer.php' + ), + 'Pelago\\Emogrifier\\HtmlProcessor\\HtmlPruner' => array( + 'version' => '3.1.0.0', + 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/HtmlProcessor/HtmlPruner.php' + ), + 'Pelago\\Emogrifier\\Utilities\\CssConcatenator' => array( + 'version' => '3.1.0.0', + 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/Utilities/CssConcatenator.php' + ), + 'Pelago\\Emogrifier\\Utilities\\ArrayIntersector' => array( + 'version' => '3.1.0.0', + 'path' => $vendorDir . '/pelago/emogrifier/src/Emogrifier/Utilities/ArrayIntersector.php' + ), + 'MaxMind\\Db\\Reader\\Decoder' => array( + 'version' => '1.6.0.0', + 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php' + ), + 'MaxMind\\Db\\Reader\\Metadata' => array( + 'version' => '1.6.0.0', + 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php' + ), + 'MaxMind\\Db\\Reader\\InvalidDatabaseException' => array( + 'version' => '1.6.0.0', + 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php' + ), + 'MaxMind\\Db\\Reader\\Util' => array( + 'version' => '1.6.0.0', + 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php' + ), + 'MaxMind\\Db\\Reader' => array( + 'version' => '1.6.0.0', + 'path' => $vendorDir . '/maxmind-db/reader/src/MaxMind/Db/Reader.php' + ), + 'Composer\\Installers\\SyliusInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SyliusInstaller.php' + ), + 'Composer\\Installers\\GravInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/GravInstaller.php' + ), + 'Composer\\Installers\\CodeIgniterInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CodeIgniterInstaller.php' + ), + 'Composer\\Installers\\BonefishInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/BonefishInstaller.php' + ), + 'Composer\\Installers\\ItopInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ItopInstaller.php' + ), + 'Composer\\Installers\\DrupalInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DrupalInstaller.php' + ), + 'Composer\\Installers\\DokuWikiInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DokuWikiInstaller.php' + ), + 'Composer\\Installers\\YawikInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/YawikInstaller.php' + ), + 'Composer\\Installers\\ZikulaInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ZikulaInstaller.php' + ), + 'Composer\\Installers\\BaseInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/BaseInstaller.php' + ), + 'Composer\\Installers\\LanManagementSystemInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/LanManagementSystemInstaller.php' + ), + 'Composer\\Installers\\BitrixInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/BitrixInstaller.php' + ), + 'Composer\\Installers\\CraftInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CraftInstaller.php' + ), + 'Composer\\Installers\\ImageCMSInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ImageCMSInstaller.php' + ), + 'Composer\\Installers\\SilverStripeInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SilverStripeInstaller.php' + ), + 'Composer\\Installers\\Installer' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Installer.php' + ), + 'Composer\\Installers\\CakePHPInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CakePHPInstaller.php' + ), + 'Composer\\Installers\\ShopwareInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ShopwareInstaller.php' + ), + 'Composer\\Installers\\ExpressionEngineInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ExpressionEngineInstaller.php' + ), + 'Composer\\Installers\\TastyIgniterInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TastyIgniterInstaller.php' + ), + 'Composer\\Installers\\PhpBBInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PhpBBInstaller.php' + ), + 'Composer\\Installers\\DecibelInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DecibelInstaller.php' + ), + 'Composer\\Installers\\OxidInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/OxidInstaller.php' + ), + 'Composer\\Installers\\MediaWikiInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MediaWikiInstaller.php' + ), + 'Composer\\Installers\\CroogoInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CroogoInstaller.php' + ), + 'Composer\\Installers\\LavaLiteInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/LavaLiteInstaller.php' + ), + 'Composer\\Installers\\ClanCatsFrameworkInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ClanCatsFrameworkInstaller.php' + ), + 'Composer\\Installers\\PrestashopInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PrestashopInstaller.php' + ), + 'Composer\\Installers\\KnownInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KnownInstaller.php' + ), + 'Composer\\Installers\\FuelInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelInstaller.php' + ), + 'Composer\\Installers\\AnnotateCmsInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AnnotateCmsInstaller.php' + ), + 'Composer\\Installers\\PiwikInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PiwikInstaller.php' + ), + 'Composer\\Installers\\KirbyInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KirbyInstaller.php' + ), + 'Composer\\Installers\\OctoberInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/OctoberInstaller.php' + ), + 'Composer\\Installers\\HuradInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/HuradInstaller.php' + ), + 'Composer\\Installers\\KodiCMSInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KodiCMSInstaller.php' + ), + 'Composer\\Installers\\CiviCrmInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CiviCrmInstaller.php' + ), + 'Composer\\Installers\\KanboardInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KanboardInstaller.php' + ), + 'Composer\\Installers\\StarbugInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/StarbugInstaller.php' + ), + 'Composer\\Installers\\WordPressInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/WordPressInstaller.php' + ), + 'Composer\\Installers\\OsclassInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/OsclassInstaller.php' + ), + 'Composer\\Installers\\EliasisInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/EliasisInstaller.php' + ), + 'Composer\\Installers\\TYPO3FlowInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TYPO3FlowInstaller.php' + ), + 'Composer\\Installers\\ChefInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ChefInstaller.php' + ), + 'Composer\\Installers\\Plugin' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Plugin.php' + ), + 'Composer\\Installers\\JoomlaInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/JoomlaInstaller.php' + ), + 'Composer\\Installers\\PhiftyInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PhiftyInstaller.php' + ), + 'Composer\\Installers\\MODXEvoInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MODXEvoInstaller.php' + ), + 'Composer\\Installers\\MODULEWorkInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MODULEWorkInstaller.php' + ), + 'Composer\\Installers\\CockpitInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/CockpitInstaller.php' + ), + 'Composer\\Installers\\FuelphpInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/FuelphpInstaller.php' + ), + 'Composer\\Installers\\MakoInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MakoInstaller.php' + ), + 'Composer\\Installers\\ElggInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ElggInstaller.php' + ), + 'Composer\\Installers\\WHMCSInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/WHMCSInstaller.php' + ), + 'Composer\\Installers\\DolibarrInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DolibarrInstaller.php' + ), + 'Composer\\Installers\\AttogramInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AttogramInstaller.php' + ), + 'Composer\\Installers\\AglInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AglInstaller.php' + ), + 'Composer\\Installers\\OntoWikiInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/OntoWikiInstaller.php' + ), + 'Composer\\Installers\\MauticInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MauticInstaller.php' + ), + 'Composer\\Installers\\Concrete5Installer' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Concrete5Installer.php' + ), + 'Composer\\Installers\\SiteDirectInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SiteDirectInstaller.php' + ), + 'Composer\\Installers\\ZendInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ZendInstaller.php' + ), + 'Composer\\Installers\\LithiumInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/LithiumInstaller.php' + ), + 'Composer\\Installers\\KohanaInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/KohanaInstaller.php' + ), + 'Composer\\Installers\\Symfony1Installer' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Symfony1Installer.php' + ), + 'Composer\\Installers\\ModxInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ModxInstaller.php' + ), + 'Composer\\Installers\\MajimaInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MajimaInstaller.php' + ), + 'Composer\\Installers\\PPIInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PPIInstaller.php' + ), + 'Composer\\Installers\\WolfCMSInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/WolfCMSInstaller.php' + ), + 'Composer\\Installers\\MantisBTInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MantisBTInstaller.php' + ), + 'Composer\\Installers\\ProcessWireInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ProcessWireInstaller.php' + ), + 'Composer\\Installers\\PantheonInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PantheonInstaller.php' + ), + 'Composer\\Installers\\WinterInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/WinterInstaller.php' + ), + 'Composer\\Installers\\TheliaInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TheliaInstaller.php' + ), + 'Composer\\Installers\\DframeInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/DframeInstaller.php' + ), + 'Composer\\Installers\\RedaxoInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/RedaxoInstaller.php' + ), + 'Composer\\Installers\\PlentymarketsInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PlentymarketsInstaller.php' + ), + 'Composer\\Installers\\MoodleInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MoodleInstaller.php' + ), + 'Composer\\Installers\\MicroweberInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MicroweberInstaller.php' + ), + 'Composer\\Installers\\RoundcubeInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/RoundcubeInstaller.php' + ), + 'Composer\\Installers\\RadPHPInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/RadPHPInstaller.php' + ), + 'Composer\\Installers\\PuppetInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PuppetInstaller.php' + ), + 'Composer\\Installers\\MiaoxingInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MiaoxingInstaller.php' + ), + 'Composer\\Installers\\PimcoreInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PimcoreInstaller.php' + ), + 'Composer\\Installers\\TaoInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TaoInstaller.php' + ), + 'Composer\\Installers\\SyDESInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SyDESInstaller.php' + ), + 'Composer\\Installers\\ReIndexInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/ReIndexInstaller.php' + ), + 'Composer\\Installers\\MagentoInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MagentoInstaller.php' + ), + 'Composer\\Installers\\TYPO3CmsInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TYPO3CmsInstaller.php' + ), + 'Composer\\Installers\\AsgardInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AsgardInstaller.php' + ), + 'Composer\\Installers\\EzPlatformInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/EzPlatformInstaller.php' + ), + 'Composer\\Installers\\Redaxo5Installer' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/Redaxo5Installer.php' + ), + 'Composer\\Installers\\PortoInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PortoInstaller.php' + ), + 'Composer\\Installers\\LaravelInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/LaravelInstaller.php' + ), + 'Composer\\Installers\\MayaInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/MayaInstaller.php' + ), + 'Composer\\Installers\\SMFInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/SMFInstaller.php' + ), + 'Composer\\Installers\\TuskInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/TuskInstaller.php' + ), + 'Composer\\Installers\\PxcmsInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/PxcmsInstaller.php' + ), + 'Composer\\Installers\\VgmcpInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/VgmcpInstaller.php' + ), + 'Composer\\Installers\\VanillaInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/VanillaInstaller.php' + ), + 'Composer\\Installers\\UserFrostingInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/UserFrostingInstaller.php' + ), + 'Composer\\Installers\\AimeosInstaller' => array( + 'version' => '1.12.0.0', + 'path' => $vendorDir . '/composer/installers/src/Composer/Installers/AimeosInstaller.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Container' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Container.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassName' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Argument/ClassName.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Argument/ClassNameInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ClassNameWithOptionalValue' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Argument/ClassNameWithOptionalValue.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgument' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Argument/RawArgument.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\RawArgumentInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Argument/RawArgumentInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverTrait' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Argument/ArgumentResolverTrait.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Argument\\ArgumentResolverInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Argument/ArgumentResolverInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\Inflector' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Inflector/Inflector.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregate' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorAggregate.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Inflector\\InflectorAggregateInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Inflector/InflectorAggregateInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\ContainerException' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Exception/ContainerException.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Exception\\NotFoundException' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Exception/NotFoundException.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareTrait' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/ContainerAwareTrait.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregateInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionAggregateInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionAggregate' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionAggregate.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\DefinitionInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Definition/DefinitionInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\Definition\\Definition' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/Definition/Definition.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregateInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregateInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\AbstractServiceProvider' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/AbstractServiceProvider.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderAggregate' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderAggregate.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\ServiceProviderInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/ServiceProviderInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ServiceProvider\\BootableServiceProviderInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/ServiceProvider/BootableServiceProviderInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ContainerAwareInterface' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/ContainerAwareInterface.php' + ), + 'Automattic\\WooCommerce\\Vendor\\League\\Container\\ReflectionContainer' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/lib/packages/League/Container/ReflectionContainer.php' + ), + 'Automattic\\WooCommerce\\Tests\\Proxies\\MockableLegacyProxyTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Proxies/MockableLegacyProxyTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Proxies\\ExampleClasses\\ClassThatDependsOnLegacyCode' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php' + ), + 'Automattic\\WooCommerce\\Tests\\Proxies\\ClassThatDependsOnLegacyCodeTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Proxies\\LegacyProxyTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Proxies/LegacyProxyTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Utilities\\NumberUtilTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Utilities/NumberUtilTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Utilities\\StringUtilTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Utilities/StringUtilTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Utilities\\ArrayUtilTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Utilities/ArrayUtilTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\AssignDefaultCategoryTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/AssignDefaultCategoryTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExtendedContainerTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExtendedContainerTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\DependencyClass' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/DependencyClass.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithInjectionMethodArgumentWithoutTypeHint' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithInjectionMethodArgumentWithoutTypeHint.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithScalarInjectionMethodArgument' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithScalarInjectionMethodArgument.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithPrivateInjectionMethod' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateInjectionMethod.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithNonFinalInjectionMethod' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithNonFinalInjectionMethod.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\ExampleClasses\\ClassWithDependencies' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithDependencies.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DependencyManagement\\AbstractServiceProviderTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\DownloadPermissionsAdjusterTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/DownloadPermissionsAdjusterTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\RestApiUtilTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/RestApiUtilTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\WCCom\\ConnectionHelperTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/WCCom/ConnectionHelperTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\LookupDataStoreTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/LookupDataStoreTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\FiltererTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/FiltererTest.php' + ), + 'Automattic\\WooCommerce\\Tests\\Internal\\ProductAttributesLookup\\DataRegeneratorTest' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/php/src/Internal/ProductAttributesLookup/DataRegeneratorTest.php' + ), + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\CodeHack' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/Tools/CodeHacking/Hacks/CodeHack.php' + ), + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\StaticMockerHack' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/Tools/CodeHacking/Hacks/StaticMockerHack.php' + ), + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\BypassFinalsHack' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/Tools/CodeHacking/Hacks/BypassFinalsHack.php' + ), + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\Hacks\\FunctionsMockerHack' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/Tools/CodeHacking/Hacks/FunctionsMockerHack.php' + ), + 'Automattic\\WooCommerce\\Testing\\Tools\\CodeHacking\\CodeHacker' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/Tools/CodeHacking/CodeHacker.php' + ), + 'Automattic\\WooCommerce\\Testing\\Tools\\FakeQueue' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/Tools/FakeQueue.php' + ), + 'Automattic\\WooCommerce\\Testing\\Tools\\DependencyManagement\\MockableLegacyProxy' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/Tools/DependencyManagement/MockableLegacyProxy.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\Email\\CustomerNewAccount' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/Email/CustomerNewAccount.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\CreateAccount' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/CreateAccount.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\DraftOrders' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/DraftOrders.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\GoogleAnalytics' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/GoogleAnalytics.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\FeatureGating' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/FeatureGating.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Domain\\Services\\ExtendRestApi' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Services/ExtendRestApi.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Domain\\Package' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Package.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Domain\\Bootstrap' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Domain/Bootstrap.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypesController' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypesController.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Installer' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Installer.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Library' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Library.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationRegistry' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Integrations/IntegrationRegistry.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Integrations\\IntegrationInterface' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Integrations/IntegrationInterface.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveCoupon' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveCoupon.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCoupons' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCoupons.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Batch' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Batch.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductReviews' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductReviews.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartExtensions' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartExtensions.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartSelectShippingRate' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartSelectShippingRate.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Cart' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Cart.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartApplyCoupon' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartApplyCoupon.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategories' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategories.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductsById' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductsById.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartAddItem' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartAddItem.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductTags' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductTags.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributeTerms' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributeTerms.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItems' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItems.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCollectionData' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCollectionData.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributesById' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributesById.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartItemsByKey' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartItemsByKey.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductAttributes' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductAttributes.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractTermsRoute' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractTermsRoute.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\ProductCategoriesById' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/ProductCategoriesById.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractCartRoute' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractCartRoute.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartCouponsByCode' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartCouponsByCode.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Checkout' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Checkout.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartRemoveItem' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartRemoveItem.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateCustomer' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateCustomer.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\Products' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/Products.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteException' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteException.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\CartUpdateItem' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/CartUpdateItem.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\AbstractRoute' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/AbstractRoute.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Routes\\RouteInterface' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Routes/RouteInterface.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\SchemaController' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/SchemaController.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\TooManyInCartException' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/TooManyInCartException.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OrderController' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OrderController.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQueryFilters' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQueryFilters.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NoticeHandler' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NoticeHandler.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\InvalidStockLevelsInCartException' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/InvalidStockLevelsInCartException.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\NotPurchasableException' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/NotPurchasableException.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\StockAvailabilityException' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/StockAvailabilityException.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\PartialOutOfStockException' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/PartialOutOfStockException.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\ProductQuery' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/ProductQuery.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\OutOfStockException' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/OutOfStockException.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\Pagination' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/Pagination.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Utilities\\CartController' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Utilities/CartController.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\RoutesController' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/RoutesController.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ShippingAddressSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ShippingAddressSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractAddressSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractAddressSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ImageAttachmentSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ImageAttachmentSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductReviewSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductReviewSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\BillingAddressSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/BillingAddressSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartExtensionsSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartExtensionsSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCategorySchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCategorySchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartCouponSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartCouponSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductAttributeSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductAttributeSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\TermSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/TermSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\OrderCouponSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/OrderCouponSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CheckoutSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CheckoutSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ErrorSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ErrorSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\AbstractSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/AbstractSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\ProductCollectionDataSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/ProductCollectionDataSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartItemSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartItemSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartShippingRateSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartShippingRateSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Schemas\\CartFeeSchema' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Schemas/CartFeeSchema.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\HtmlFormatter' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/HtmlFormatter.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\FormatterInterface' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/FormatterInterface.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\CurrencyFormatter' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/CurrencyFormatter.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\MoneyFormatter' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/MoneyFormatter.php' + ), + 'Automattic\\WooCommerce\\Blocks\\StoreApi\\Formatters\\DefaultFormatter' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/StoreApi/Formatters/DefaultFormatter.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AtomicBlock' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AtomicBlock.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\StockFilter' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/StockFilter.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductSearch' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductSearch.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\MiniCart' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/MiniCart.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllProducts' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AllProducts.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductsByAttribute' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductsByAttribute.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Cart' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/Cart.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\CartI2' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/CartI2.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\PriceFilter' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/PriceFilter.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategories' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategories.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\HandpickedProducts' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/HandpickedProducts.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedProduct' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedProduct.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductNew' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductNew.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByProduct' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByProduct.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\SingleProduct' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/SingleProduct.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractDynamicBlock' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractDynamicBlock.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTopRated' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductTopRated.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractBlock' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractBlock.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\Checkout' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/Checkout.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AttributeFilter' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AttributeFilter.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductOnSale' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductOnSale.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AbstractProductGrid' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AbstractProductGrid.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ReviewsByCategory' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ReviewsByCategory.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductBestSellers' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductBestSellers.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductCategory' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductCategory.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ActiveFilters' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ActiveFilters.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\FeaturedCategory' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/FeaturedCategory.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\ProductTag' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/ProductTag.php' + ), + 'Automattic\\WooCommerce\\Blocks\\BlockTypes\\AllReviews' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/BlockTypes/AllReviews.php' + ), + 'Automattic\\WooCommerce\\Blocks\\RestApi' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/RestApi.php' + ), + 'Automattic\\WooCommerce\\Blocks\\AssetsController' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/AssetsController.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Assets' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Assets.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentContext' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentContext.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\PayPal' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/PayPal.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\BankTransfer' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/BankTransfer.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Cheque' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/Cheque.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\CashOnDelivery' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/CashOnDelivery.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\AbstractPaymentMethodType' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/AbstractPaymentMethodType.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\Integrations\\Stripe' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Integrations/Stripe.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentResult' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentResult.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\Api' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/Api.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodTypeInterface' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentMethodTypeInterface.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Payments\\PaymentMethodRegistry' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Payments/PaymentMethodRegistry.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Registry\\AbstractDependencyType' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Registry/AbstractDependencyType.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Registry\\Container' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Registry/Container.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Registry\\FactoryType' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Registry/FactoryType.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Registry\\SharedType' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Registry/SharedType.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Utils\\BlocksWpQuery' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Utils/BlocksWpQuery.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Utils\\ArrayUtils' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Utils/ArrayUtils.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Assets\\AssetDataRegistry' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Assets/AssetDataRegistry.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Assets\\Api' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Assets/Api.php' + ), + 'Automattic\\WooCommerce\\Blocks\\Package' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/Package.php' + ), + 'Automattic\\WooCommerce\\Blocks\\InboxNotifications' => array( + 'version' => '6.1.0.0', + 'path' => $baseDir . '/packages/woocommerce-blocks/src/InboxNotifications.php' + ), + 'Automattic\\WooCommerce\\Admin\\Install' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Install.php' + ), + 'Automattic\\WooCommerce\\Admin\\Composer\\Package' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Composer/Package.php' + ), + 'Automattic\\WooCommerce\\Admin\\ReportCSVExporter' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/ReportCSVExporter.php' + ), + 'Automattic\\WooCommerce\\Admin\\CategoryLookup' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/CategoryLookup.php' + ), + 'Automattic\\WooCommerce\\Admin\\WCAdminSharedSettings' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/WCAdminSharedSettings.php' + ), + 'Automattic\\WooCommerce\\Admin\\Loader' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Loader.php' + ), + 'Automattic\\WooCommerce\\Admin\\WCAdminHelper' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/WCAdminHelper.php' + ), + 'Automattic\\WooCommerce\\Admin\\Overrides\\Order' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/Order.php' + ), + 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderTraits' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/OrderTraits.php' + ), + 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgrader' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/ThemeUpgrader.php' + ), + 'Automattic\\WooCommerce\\Admin\\Overrides\\ThemeUpgraderSkin' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/ThemeUpgraderSkin.php' + ), + 'Automattic\\WooCommerce\\Admin\\Overrides\\OrderRefund' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Overrides/OrderRefund.php' + ), + 'Automattic\\WooCommerce\\Admin\\DeprecatedClassFacade' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/DeprecatedClassFacade.php' + ), + 'Automattic\\WooCommerce\\Admin\\ReportsSync' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/ReportsSync.php' + ), + 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\DateTimeProviderInterface' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/DateTimeProvider/DateTimeProviderInterface.php' + ), + 'Automattic\\WooCommerce\\Admin\\DateTimeProvider\\CurrentDateTimeProvider' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/DateTimeProvider/CurrentDateTimeProvider.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Customers' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Customers.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Export\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Export/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Segmenter' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Segmenter.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Products\\Stats\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Products/Stats/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStoreInterface' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/DataStoreInterface.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Cache' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Cache.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\PerformanceIndicators\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/PerformanceIndicators/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableInterface' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ExportableInterface.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Stock\\Stats\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Stock/Stats/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Segmenter' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Segmenter.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Revenue/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Revenue\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Revenue/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Segmenter' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Segmenter.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Orders\\Stats\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Orders/Stats/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Segmenter' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Segmenter.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Taxes\\Stats\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Taxes/Stats/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Categories\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Categories/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ExportableTraits' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ExportableTraits.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Import\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Import/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Stats\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Stats/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Downloads\\Files\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Downloads/Files/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Segmenter' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Segmenter.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Variations\\Stats\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Variations/Stats/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\ParameterException' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/ParameterException.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Customers\\Stats\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Customers/Stats/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\TimeInterval' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/TimeInterval.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Controller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Controller.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Segmenter' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Segmenter.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\Coupons\\Stats\\Query' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/Coupons/Stats/Query.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Reports\\SqlQuery' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Reports/SqlQuery.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Options' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Options.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\CustomAttributeTraits' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/CustomAttributeTraits.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\ProductsLowInStock' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductsLowInStock.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\DataDownloadIPs' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/DataDownloadIPs.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\ProductReviews' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductReviews.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingPayments' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingPayments.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\MarketingOverview' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/MarketingOverview.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Themes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Themes.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProductTypes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingProductTypes.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Taxes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Taxes.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Leaderboards' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Leaderboards.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\ProductCategories' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductCategories.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Data' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Data.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributeTerms' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductAttributeTerms.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Features' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Features.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\ProductAttributes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductAttributes.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Init' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Init.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingTasks' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingTasks.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\ProductVariations' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/ProductVariations.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\NavigationFavorites' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/NavigationFavorites.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingProfile' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingProfile.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\SettingOptions' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/SettingOptions.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Products' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Products.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingFreeExtensions' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingFreeExtensions.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Orders' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Orders.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\OnboardingThemes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/OnboardingThemes.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Marketing' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Marketing.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Coupons' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Coupons.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\DataCountries' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/DataCountries.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Plugins' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Plugins.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\NoteActions' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/NoteActions.php' + ), + 'Automattic\\WooCommerce\\Admin\\API\\Notes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/API/Notes.php' + ), + 'Automattic\\WooCommerce\\Admin\\PaymentPlugins' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/PaymentPlugins.php' + ), + 'Automattic\\WooCommerce\\Admin\\Schedulers\\SchedulerTraits' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/SchedulerTraits.php' + ), + 'Automattic\\WooCommerce\\Admin\\Schedulers\\OrdersScheduler' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/OrdersScheduler.php' + ), + 'Automattic\\WooCommerce\\Admin\\Schedulers\\CustomersScheduler' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/CustomersScheduler.php' + ), + 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportScheduler' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/ImportScheduler.php' + ), + 'Automattic\\WooCommerce\\Admin\\Schedulers\\MailchimpScheduler' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/MailchimpScheduler.php' + ), + 'Automattic\\WooCommerce\\Admin\\Schedulers\\ImportInterface' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Schedulers/ImportInterface.php' + ), + 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProvider' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProvider.php' + ), + 'Automattic\\WooCommerce\\Admin\\PluginsProvider\\PluginsProviderInterface' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/PluginsProvider/PluginsProviderInterface.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\GettingStartedInEcommerceWebinar' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/GettingStartedInEcommerceWebinar.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\NeedSomeInspiration' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NeedSomeInspiration.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\Note' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/Note.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedbackFollowUp' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationFeedbackFollowUp.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\UnsecuredReportFiles' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/UnsecuredReportFiles.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\StartDropshippingBusiness' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/StartDropshippingBusiness.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WelcomeToWooCommerceForStoreUsers' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/WelcomeToWooCommerceForStoreUsers.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\InstallJPAndWCSPlugins' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\FirstDownlaodableProduct' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/FirstDownlaodableProduct.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\ManageOrdersOnTheGo' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/ManageOrdersOnTheGo.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\DataStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DataStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\NewSalesRecord' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NewSalesRecord.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\LearnMoreAboutVariableProducts' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/LearnMoreAboutVariableProducts.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\MarketingJetpack' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MarketingJetpack.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\DeactivatePlugin' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeactivatePlugin.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationNudge' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationNudge.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\GivingFeedbackNotes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/GivingFeedbackNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingPayments' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnboardingPayments.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizeStoreWithBlocks' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/CustomizeStoreWithBlocks.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\CouponPageMoved' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/CouponPageMoved.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\LaunchChecklist' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/LaunchChecklist.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\ChooseNiche' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/ChooseNiche.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstSale' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/InsightFirstSale.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommercePayments' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooCommercePayments.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\EUVATNumber' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/EUVATNumber.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\TestCheckout' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/TestCheckout.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\TrackingOptIn' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/TrackingOptIn.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\PersonalizeStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/PersonalizeStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\PerformanceOnMobile' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/PerformanceOnMobile.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\OnlineClothingStore' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnlineClothingStore.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Note' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Choose_Niche' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Coupon_Page_Moved' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Customize_Store_With_Blocks' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Deactivate_Plugin' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Draw_Attention' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Edit_Products_On_The_Move' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_EU_VAT_Number' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Facebook_Marketing_Expert' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_First_Product' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Giving_Feedback_Notes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Insight_First_Sale' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Install_JP_And_WCS_Plugins' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Launch_Checklist' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Marketing' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Migrate_From_Shopify' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Mobile_App' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Need_Some_Inspiration' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_New_Sales_Record' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Email_Marketing' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Onboarding_Payments' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Online_Clothing_Store' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Order_Milestones' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Performance_On_Mobile' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Personalize_Store' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Real_Time_Order_Alerts' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Selling_Online_Courses' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Set_Up_Additional_Payment_Types' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Start_Dropshipping_Business' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Test_Checkout' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Tracking_Opt_In' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_Woo_Subscriptions_Notes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Payments' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WC_Admin_Notes_WooCommerce_Subscriptions' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DeprecatedNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\NoteTraits' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NoteTraits.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\AddFirstProduct' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/AddFirstProduct.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\FilterByProductVariationsInReports' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/FilterByProductVariationsInReports.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\NavigationFeedback' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NavigationFeedback.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\NotificationEmail' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/NotificationEmail.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\MerchantEmailNotifications\\MerchantEmailNotifications' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MerchantEmailNotifications/MerchantEmailNotifications.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\SetUpAdditionalPaymentTypes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/SetUpAdditionalPaymentTypes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\SellingOnlineCourses' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/SellingOnlineCourses.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\DrawAttention' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/DrawAttention.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\RealTimeOrderAlerts' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/RealTimeOrderAlerts.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\MigrateFromShopify' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MigrateFromShopify.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\InsightFirstProductAndPayment' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/InsightFirstProductAndPayment.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\AddingAndManangingProducts' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/AddingAndManangingProducts.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WooSubscriptionsNotes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooSubscriptionsNotes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\WooCommerceSubscriptions' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/WooCommerceSubscriptions.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\EditProductsOnTheMove' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/EditProductsOnTheMove.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\CustomizingProductCatalog' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/CustomizingProductCatalog.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\NotesUnavailableException' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/NotesUnavailableException.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\FirstProduct' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/FirstProduct.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\MobileApp' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/MobileApp.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\OnboardingTraits' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/OnboardingTraits.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\Marketing' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/Marketing.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\ManageStoreActivityFromHomeScreen' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/ManageStoreActivityFromHomeScreen.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\ChoosingTheme' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/ChoosingTheme.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\OrderMilestones' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/OrderMilestones.php' + ), + 'Automattic\\WooCommerce\\Admin\\Notes\\Notes' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Notes/Notes.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteInboxNotifications' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteInboxNotifications.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\ActivityPanels' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/ActivityPanels.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBannerDisplayRules' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/ShippingLabelBannerDisplayRules.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\CouponsMovedTrait' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/CouponsMovedTrait.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\DataSourcePoller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/DataSourcePoller.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\WCPaymentGatewayPreInstallWCPayPromotion' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/WCPaymentGatewayPreInstallWCPayPromotion.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\WcPayPromotion\\Init' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/WcPayPromotion/Init.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Homescreen' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Homescreen.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Purchase' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Purchase.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Payments' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Payments.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\WooCommercePayments' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/WooCommercePayments.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Appearance' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Appearance.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Tax' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Tax.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\StoreDetails' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/StoreDetails.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Shipping' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Shipping.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Products' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Products.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Tasks\\Marketing' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Tasks/Marketing.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskList' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskList.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Init' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Init.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\TaskLists' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/TaskLists.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\OnboardingTasks\\Task' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/OnboardingTasks/Task.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Analytics' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Analytics.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Onboarding' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Onboarding.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\CustomerEffortScoreTracks' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/CustomerEffortScoreTracks.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Features' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Features.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\MobileAppBanner' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/MobileAppBanner.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Settings' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Settings.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\ShippingLabelBanner' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/ShippingLabelBanner.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DataSourcePoller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DataSourcePoller.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\EvaluateExtension' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/EvaluateExtension.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\Init' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/Init.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\RemoteFreeExtensions\\DefaultFreeExtensions' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/RemoteFreeExtensions/DefaultFreeExtensions.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DataSourcePoller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DataSourcePoller.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\PaymentGatewaysController' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/PaymentGatewaysController.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\Init' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/Init.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\EvaluateSuggestion' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/EvaluateSuggestion.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\PaymentGatewaySuggestions\\DefaultPaymentGateways' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/PaymentGatewaySuggestions/DefaultPaymentGateways.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\CoreMenu' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/CoreMenu.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Menu' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Menu.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Init' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Init.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Favorites' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Favorites.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Navigation\\Screen' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Navigation/Screen.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Marketing' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Marketing.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\Coupons' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/Coupons.php' + ), + 'Automattic\\WooCommerce\\Admin\\Features\\TransientNotices' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Features/TransientNotices.php' + ), + 'Automattic\\WooCommerce\\Admin\\Marketing\\InstalledExtensions' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Marketing/InstalledExtensions.php' + ), + 'Automattic\\WooCommerce\\Admin\\Events' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Events.php' + ), + 'Automattic\\WooCommerce\\Admin\\PageController' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/PageController.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\GetRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/GetRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleEvaluator' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleEvaluator.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\SpecRunner' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/SpecRunner.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\FailRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/FailRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\DataSourcePoller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/DataSourcePoller.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishBeforeTimeRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishBeforeTimeRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OptionRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OptionRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NotRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NotRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrderCountRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrderCountRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationCountryRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationCountryRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PassRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PassRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OrdersProvider' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OrdersProvider.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WooCommerceAdminUpdatedRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WooCommerceAdminUpdatedRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ProductCountRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ProductCountRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PublishAfterTimeRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PublishAfterTimeRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerInterface' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerInterface.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\Count' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/Count.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayFlatten' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayFlatten.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayKeys' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayKeys.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayValues' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayValues.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\DotNotation' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/DotNotation.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArraySearch' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArraySearch.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\Transformers\\ArrayColumn' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/Transformers/ArrayColumn.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\TransformerService' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/TransformerService.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluationLogger' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluationLogger.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\OnboardingProfileRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/OnboardingProfileRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateSetupForProducts' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateSetupForProducts.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\ComparisonOperation' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/ComparisonOperation.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginsActivatedRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginsActivatedRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\NoteStatusRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/NoteStatusRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\PluginVersionRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/PluginVersionRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForProvider' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForProvider.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\WCAdminActiveForRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/WCAdminActiveForRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RemoteInboxNotificationsEngine' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RemoteInboxNotificationsEngine.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\RuleProcessorInterface' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/RuleProcessorInterface.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\StoredStateRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/StoredStateRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\IsEcommerceRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/IsEcommerceRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\EvaluateAndGetStatus' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/EvaluateAndGetStatus.php' + ), + 'Automattic\\WooCommerce\\Admin\\RemoteInboxNotifications\\BaseLocationStateRuleProcessor' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/RemoteInboxNotifications/BaseLocationStateRuleProcessor.php' + ), + 'Automattic\\WooCommerce\\Admin\\PluginsInstaller' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/PluginsInstaller.php' + ), + 'Automattic\\WooCommerce\\Admin\\FeaturePlugin' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/FeaturePlugin.php' + ), + 'Automattic\\WooCommerce\\Admin\\Survey' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/Survey.php' + ), + 'Automattic\\WooCommerce\\Admin\\ReportExporter' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/ReportExporter.php' + ), + 'Automattic\\WooCommerce\\Admin\\ReportCSVEmail' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/ReportCSVEmail.php' + ), + 'Automattic\\WooCommerce\\Admin\\PluginsHelper' => array( + 'version' => '2.8.0.0', + 'path' => $baseDir . '/packages/woocommerce-admin/src/PluginsHelper.php' + ), + 'Automattic\\WooCommerce\\Container' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Container.php' + ), + 'Automattic\\WooCommerce\\Proxies\\ActionsProxy' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Proxies/ActionsProxy.php' + ), + 'Automattic\\WooCommerce\\Proxies\\LegacyProxy' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Proxies/LegacyProxy.php' + ), + 'Automattic\\WooCommerce\\Packages' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Packages.php' + ), + 'Automattic\\WooCommerce\\Utilities\\NumberUtil' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Utilities/NumberUtil.php' + ), + 'Automattic\\WooCommerce\\Utilities\\StringUtil' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Utilities/StringUtil.php' + ), + 'Automattic\\WooCommerce\\Utilities\\ArrayUtil' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Utilities/ArrayUtil.php' + ), + 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStockException' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Checkout/Helpers/ReserveStockException.php' + ), + 'Automattic\\WooCommerce\\Checkout\\Helpers\\ReserveStock' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Checkout/Helpers/ReserveStock.php' + ), + 'Automattic\\WooCommerce\\Internal\\RestockRefundedItemsAdjuster' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/RestockRefundedItemsAdjuster.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ContainerException' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/ContainerException.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\RestockRefundedItemsAdjusterServiceProvider' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/RestockRefundedItemsAdjusterServiceProvider.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProxiesServiceProvider' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/ProxiesServiceProvider.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\ProductAttributesLookupServiceProvider' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/ProductAttributesLookupServiceProvider.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\AssignDefaultCategoryServiceProvider' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/AssignDefaultCategoryServiceProvider.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ServiceProviders\\DownloadPermissionsAdjusterServiceProvider' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/ServiceProviders/DownloadPermissionsAdjusterServiceProvider.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\AbstractServiceProvider' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/AbstractServiceProvider.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\ExtendedContainer' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/ExtendedContainer.php' + ), + 'Automattic\\WooCommerce\\Internal\\DependencyManagement\\Definition' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DependencyManagement/Definition.php' + ), + 'Automattic\\WooCommerce\\Internal\\AssignDefaultCategory' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/AssignDefaultCategory.php' + ), + 'Automattic\\WooCommerce\\Internal\\WCCom\\ConnectionHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/WCCom/ConnectionHelper.php' + ), + 'Automattic\\WooCommerce\\Internal\\RestApiUtil' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/RestApiUtil.php' + ), + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\LookupDataStore' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/ProductAttributesLookup/LookupDataStore.php' + ), + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\Filterer' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/ProductAttributesLookup/Filterer.php' + ), + 'Automattic\\WooCommerce\\Internal\\ProductAttributesLookup\\DataRegenerator' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/ProductAttributesLookup/DataRegenerator.php' + ), + 'Automattic\\WooCommerce\\Internal\\DownloadPermissionsAdjuster' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Internal/DownloadPermissionsAdjuster.php' + ), + 'Automattic\\WooCommerce\\Autoloader' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/src/Autoloader.php' + ), + 'Automattic\\Jetpack\\Autoloader\\CustomAutoloaderPlugin' => array( + 'version' => '2.10.1.0', + 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/CustomAutoloaderPlugin.php' + ), + 'Automattic\\Jetpack\\Autoloader\\ManifestGenerator' => array( + 'version' => '2.10.1.0', + 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/ManifestGenerator.php' + ), + 'Automattic\\Jetpack\\Autoloader\\AutoloadGenerator' => array( + 'version' => '2.10.1.0', + 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadGenerator.php' + ), + 'Automattic\\Jetpack\\Autoloader\\AutoloadFileWriter' => array( + 'version' => '2.10.1.0', + 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadFileWriter.php' + ), + 'Automattic\\Jetpack\\Autoloader\\AutoloadProcessor' => array( + 'version' => '2.10.1.0', + 'path' => $vendorDir . '/automattic/jetpack-autoloader/src/AutoloadProcessor.php' + ), + 'WC_REST_Shipping_Methods_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php' + ), + 'WC_REST_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php' + ), + 'WC_REST_Product_Attributes_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php' + ), + 'WC_REST_System_Status_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-controller.php' + ), + 'WC_REST_Report_Sales_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php' + ), + 'WC_REST_Shipping_Zones_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php' + ), + 'WC_REST_Product_Variations_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-variations-controller.php' + ), + 'WC_REST_Network_Orders_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php' + ), + 'WC_REST_Product_Shipping_Classes_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php' + ), + 'WC_REST_Customer_Downloads_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php' + ), + 'WC_REST_Taxes_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-taxes-controller.php' + ), + 'WC_REST_Coupons_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php' + ), + 'WC_REST_Tax_Classes_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-tax-classes-controller.php' + ), + 'WC_REST_Posts_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php' + ), + 'WC_REST_Report_Coupons_Totals_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php' + ), + 'WC_REST_Setting_Options_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php' + ), + 'WC_REST_Terms_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-terms-controller.php' + ), + 'WC_REST_Orders_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php' + ), + 'WC_REST_Report_Orders_Totals_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php' + ), + 'WC_REST_Data_Continents_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php' + ), + 'WC_REST_Shipping_Zone_Locations_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-locations-controller.php' + ), + 'WC_REST_Shipping_Zone_Methods_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php' + ), + 'WC_REST_Report_Reviews_Totals_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php' + ), + 'WC_REST_Data_Countries_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php' + ), + 'WC_REST_Settings_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php' + ), + 'WC_REST_Data_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php' + ), + 'WC_REST_Product_Tags_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-tags-controller.php' + ), + 'WC_REST_Report_Customers_Totals_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php' + ), + 'WC_REST_Report_Top_Sellers_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-top-sellers-controller.php' + ), + 'WC_REST_CRUD_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-crud-controller.php' + ), + 'WC_REST_Customers_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php' + ), + 'WC_REST_System_Status_Tools_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-system-status-tools-controller.php' + ), + 'WC_REST_Order_Refunds_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php' + ), + 'WC_REST_Report_Products_Totals_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php' + ), + 'WC_REST_Payment_Gateways_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php' + ), + 'WC_REST_Order_Notes_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-order-notes-controller.php' + ), + 'WC_REST_Products_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php' + ), + 'WC_REST_Product_Categories_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-categories-controller.php' + ), + 'WC_REST_Data_Currencies_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php' + ), + 'WC_REST_Product_Reviews_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php' + ), + 'WC_REST_Shipping_Zones_Controller_Base' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php' + ), + 'WC_REST_Product_Attribute_Terms_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php' + ), + 'WC_REST_Reports_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-reports-controller.php' + ), + 'WC_REST_Webhooks_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php' + ), + 'WC_REST_Customers_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php' + ), + 'WC_REST_Reports_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-reports-v2-controller.php' + ), + 'WC_REST_Product_Reviews_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php' + ), + 'WC_REST_Order_Refunds_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php' + ), + 'WC_REST_Product_Variations_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-variations-v2-controller.php' + ), + 'WC_REST_Network_Orders_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php' + ), + 'WC_REST_Taxes_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-taxes-v2-controller.php' + ), + 'WC_REST_System_Status_Tools_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php' + ), + 'WC_REST_Setting_Options_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-setting-options-v2-controller.php' + ), + 'WC_REST_Coupons_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php' + ), + 'WC_REST_Webhooks_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php' + ), + 'WC_REST_System_Status_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php' + ), + 'WC_REST_Order_Notes_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php' + ), + 'WC_REST_Products_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php' + ), + 'WC_REST_Product_Attributes_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php' + ), + 'WC_REST_Customer_Downloads_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php' + ), + 'WC_REST_Product_Tags_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-tags-v2-controller.php' + ), + 'WC_REST_Report_Sales_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php' + ), + 'WC_REST_Product_Categories_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-categories-v2-controller.php' + ), + 'WC_REST_Payment_Gateways_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php' + ), + 'WC_REST_Shipping_Zones_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php' + ), + 'WC_REST_Tax_Classes_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php' + ), + 'WC_REST_Webhook_Deliveries_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-webhook-deliveries-v2-controller.php' + ), + 'WC_REST_Settings_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php' + ), + 'WC_REST_Product_Shipping_Classes_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php' + ), + 'WC_REST_Shipping_Zone_Locations_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php' + ), + 'WC_REST_Orders_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php' + ), + 'WC_REST_Shipping_Methods_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php' + ), + 'WC_REST_Report_Top_Sellers_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-report-top-sellers-v2-controller.php' + ), + 'WC_REST_Product_Attribute_Terms_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php' + ), + 'WC_REST_Shipping_Zone_Methods_V2_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php' + ), + 'WC_REST_Telemetry_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Telemetry/class-wc-rest-telemetry-controller.php' + ), + 'WC_REST_Webhooks_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php' + ), + 'WC_REST_Customers_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php' + ), + 'WC_REST_Customer_Downloads_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php' + ), + 'WC_REST_Product_Tags_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php' + ), + 'WC_REST_Tax_Classes_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php' + ), + 'WC_REST_Product_Reviews_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php' + ), + 'WC_REST_Report_Sales_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php' + ), + 'WC_REST_Product_Attributes_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php' + ), + 'WC_REST_Order_Notes_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php' + ), + 'WC_REST_Report_Top_Sellers_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php' + ), + 'WC_REST_Coupons_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php' + ), + 'WC_REST_Taxes_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php' + ), + 'WC_REST_Product_Shipping_Classes_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php' + ), + 'WC_REST_Orders_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php' + ), + 'WC_REST_Webhook_Deliveries_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php' + ), + 'WC_REST_Reports_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php' + ), + 'WC_REST_Products_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php' + ), + 'WC_REST_Order_Refunds_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php' + ), + 'WC_REST_Product_Attribute_Terms_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php' + ), + 'WC_REST_Product_Categories_V1_Controller' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php' + ), + 'Automattic\\WooCommerce\\RestApi\\Utilities\\SingletonTrait' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Utilities/SingletonTrait.php' + ), + 'Automattic\\WooCommerce\\RestApi\\Utilities\\ImageAttachment' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Utilities/ImageAttachment.php' + ), + 'Automattic\\WooCommerce\\RestApi\\Package' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Package.php' + ), + 'Automattic\\WooCommerce\\RestApi\\Server' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/includes/rest-api/Server.php' + ), + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ProductHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php' + ), + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\QueueHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php' + ), + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\SettingsHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php' + ), + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\OrderHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php' + ), + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CustomerHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php' + ), + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\ShippingHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php' + ), + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\AdminNotesHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php' + ), + 'Automattic\\WooCommerce\\RestApi\\UnitTests\\Helpers\\CouponHelper' => array( + 'version' => '5.9.0.0', + 'path' => $baseDir . '/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php' + ), + 'Automattic\\Jetpack\\Constants' => array( + 'version' => '1.5.1.0', + 'path' => $vendorDir . '/automattic/jetpack-constants/src/class-constants.php' + ), +); diff --git a/vendor/jetpack-autoloader/class-autoloader-handler.php b/vendor/jetpack-autoloader/class-autoloader-handler.php new file mode 100644 index 0000000..bfe1c29 --- /dev/null +++ b/vendor/jetpack-autoloader/class-autoloader-handler.php @@ -0,0 +1,147 @@ +php_autoloader = $php_autoloader; + $this->hook_manager = $hook_manager; + $this->manifest_reader = $manifest_reader; + $this->version_selector = $version_selector; + } + + /** + * Checks to see whether or not an autoloader is currently in the process of initializing. + * + * @return bool + */ + public function is_initializing() { + // If no version has been set it means that no autoloader has started initializing yet. + global $jetpack_autoloader_latest_version; + if ( ! isset( $jetpack_autoloader_latest_version ) ) { + return false; + } + + // When the version is set but the classmap is not it ALWAYS means that this is the + // latest autoloader and is being included by an older one. + global $jetpack_packages_classmap; + if ( empty( $jetpack_packages_classmap ) ) { + return true; + } + + // Version 2.4.0 added a new global and altered the reset semantics. We need to check + // the other global as well since it may also point at initialization. + // Note: We don't need to check for the class first because every autoloader that + // will set the latest version global requires this class in the classmap. + $replacing_version = $jetpack_packages_classmap[ AutoloadGenerator::class ]['version']; + if ( $this->version_selector->is_dev_version( $replacing_version ) || version_compare( $replacing_version, '2.4.0.0', '>=' ) ) { + global $jetpack_autoloader_loader; + if ( ! isset( $jetpack_autoloader_loader ) ) { + return true; + } + } + + return false; + } + + /** + * Activates an autoloader using the given plugins and activates it. + * + * @param string[] $plugins The plugins to initialize the autoloader for. + */ + public function activate_autoloader( $plugins ) { + global $jetpack_packages_psr4; + $jetpack_packages_psr4 = array(); + $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_psr4.php', $jetpack_packages_psr4 ); + + global $jetpack_packages_classmap; + $jetpack_packages_classmap = array(); + $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_classmap.php', $jetpack_packages_classmap ); + + global $jetpack_packages_filemap; + $jetpack_packages_filemap = array(); + $this->manifest_reader->read_manifests( $plugins, 'vendor/composer/jetpack_autoload_filemap.php', $jetpack_packages_filemap ); + + $loader = new Version_Loader( + $this->version_selector, + $jetpack_packages_classmap, + $jetpack_packages_psr4, + $jetpack_packages_filemap + ); + + $this->php_autoloader->register_autoloader( $loader ); + + // Now that the autoloader is active we can load the filemap. + $loader->load_filemap(); + } + + /** + * Resets the active autoloader and all related global state. + */ + public function reset_autoloader() { + $this->php_autoloader->unregister_autoloader(); + $this->hook_manager->reset(); + + // Clear all of the autoloader globals so that older autoloaders don't do anything strange. + global $jetpack_autoloader_latest_version; + $jetpack_autoloader_latest_version = null; + + global $jetpack_packages_classmap; + $jetpack_packages_classmap = array(); // Must be array to avoid exceptions in old autoloaders! + + global $jetpack_packages_psr4; + $jetpack_packages_psr4 = array(); // Must be array to avoid exceptions in old autoloaders! + + global $jetpack_packages_filemap; + $jetpack_packages_filemap = array(); // Must be array to avoid exceptions in old autoloaders! + } +} diff --git a/vendor/jetpack-autoloader/class-autoloader-locator.php b/vendor/jetpack-autoloader/class-autoloader-locator.php new file mode 100644 index 0000000..b5a94e0 --- /dev/null +++ b/vendor/jetpack-autoloader/class-autoloader-locator.php @@ -0,0 +1,90 @@ +version_selector = $version_selector; + } + + /** + * Finds the path to the plugin with the latest autoloader. + * + * @param array $plugin_paths An array of plugin paths. + * @param string $latest_version The latest version reference. + * + * @return string|null + */ + public function find_latest_autoloader( $plugin_paths, &$latest_version ) { + $latest_plugin = null; + + foreach ( $plugin_paths as $plugin_path ) { + $version = $this->get_autoloader_version( $plugin_path ); + if ( ! $this->version_selector->is_version_update_required( $latest_version, $version ) ) { + continue; + } + + $latest_version = $version; + $latest_plugin = $plugin_path; + } + + return $latest_plugin; + } + + /** + * Gets the path to the autoloader. + * + * @param string $plugin_path The path to the plugin. + * + * @return string + */ + public function get_autoloader_path( $plugin_path ) { + return trailingslashit( $plugin_path ) . 'vendor/autoload_packages.php'; + } + + /** + * Gets the version for the autoloader. + * + * @param string $plugin_path The path to the plugin. + * + * @return string|null + */ + public function get_autoloader_version( $plugin_path ) { + $classmap = trailingslashit( $plugin_path ) . 'vendor/composer/jetpack_autoload_classmap.php'; + if ( ! file_exists( $classmap ) ) { + return null; + } + + $classmap = require $classmap; + if ( isset( $classmap[ AutoloadGenerator::class ] ) ) { + return $classmap[ AutoloadGenerator::class ]['version']; + } + + return null; + } +} diff --git a/vendor/jetpack-autoloader/class-autoloader.php b/vendor/jetpack-autoloader/class-autoloader.php new file mode 100644 index 0000000..ef742b4 --- /dev/null +++ b/vendor/jetpack-autoloader/class-autoloader.php @@ -0,0 +1,90 @@ +get( Autoloader_Handler::class ); + + // If the autoloader is already initializing it means that it has included us as the latest. + $was_included_by_autoloader = $autoloader_handler->is_initializing(); + + /** @var Plugin_Locator $plugin_locator */ + $plugin_locator = $container->get( Plugin_Locator::class ); + + /** @var Plugins_Handler $plugins_handler */ + $plugins_handler = $container->get( Plugins_Handler::class ); + + // The current plugin is the one that we are attempting to initialize here. + $current_plugin = $plugin_locator->find_current_plugin(); + + // The active plugins are those that we were able to discover on the site. This list will not + // include mu-plugins, those activated by code, or those who are hidden by filtering. We also + // want to take care to not consider the current plugin unknown if it was included by an + // autoloader. This avoids the case where a plugin will be marked "active" while deactivated + // due to it having the latest autoloader. + $active_plugins = $plugins_handler->get_active_plugins( true, ! $was_included_by_autoloader ); + + // The cached plugins are all of those that were active or discovered by the autoloader during a previous request. + // Note that it's possible this list will include plugins that have since been deactivated, but after a request + // the cache should be updated and the deactivated plugins will be removed. + $cached_plugins = $plugins_handler->get_cached_plugins(); + + // We combine the active list and cached list to preemptively load classes for plugins that are + // presently unknown but will be loaded during the request. While this may result in us considering packages in + // deactivated plugins there shouldn't be any problems as a result and the eventual consistency is sufficient. + $all_plugins = array_merge( $active_plugins, $cached_plugins ); + + // In particular we also include the current plugin to address the case where it is the latest autoloader + // but also unknown (and not cached). We don't want it in the active list because we don't know that it + // is active but we need it in the all plugins list so that it is considered by the autoloader. + $all_plugins[] = $current_plugin; + + // We require uniqueness in the array to avoid processing the same plugin more than once. + $all_plugins = array_values( array_unique( $all_plugins ) ); + + /** @var Latest_Autoloader_Guard $guard */ + $guard = $container->get( Latest_Autoloader_Guard::class ); + if ( $guard->should_stop_init( $current_plugin, $all_plugins, $was_included_by_autoloader ) ) { + return; + } + + // Initialize the autoloader using the handler now that we're ready. + $autoloader_handler->activate_autoloader( $all_plugins ); + + /** @var Hook_Manager $hook_manager */ + $hook_manager = $container->get( Hook_Manager::class ); + + // Register a shutdown handler to clean up the autoloader. + $hook_manager->add_action( 'shutdown', new Shutdown_Handler( $plugins_handler, $cached_plugins, $was_included_by_autoloader ) ); + + // phpcs:enable Generic.Commenting.DocComment.MissingShort + } +} diff --git a/vendor/jetpack-autoloader/class-container.php b/vendor/jetpack-autoloader/class-container.php new file mode 100644 index 0000000..b88813d --- /dev/null +++ b/vendor/jetpack-autoloader/class-container.php @@ -0,0 +1,150 @@ + 'Hook_Manager', + ); + + /** + * A map of all the dependencies we've registered with the container and created. + * + * @var array + */ + protected $dependencies; + + /** + * The constructor. + */ + public function __construct() { + $this->dependencies = array(); + + $this->register_shared_dependencies(); + $this->register_dependencies(); + $this->initialize_globals(); + } + + /** + * Gets a dependency out of the container. + * + * @param string $class The class to fetch. + * + * @return mixed + * @throws \InvalidArgumentException When a class that isn't registered with the container is fetched. + */ + public function get( $class ) { + if ( ! isset( $this->dependencies[ $class ] ) ) { + throw new \InvalidArgumentException( "Class '$class' is not registered with the container." ); + } + + return $this->dependencies[ $class ]; + } + + /** + * Registers all of the dependencies that are shared between all instances of the autoloader. + */ + private function register_shared_dependencies() { + global $jetpack_autoloader_container_shared; + if ( ! isset( $jetpack_autoloader_container_shared ) ) { + $jetpack_autoloader_container_shared = array(); + } + + $key = self::SHARED_DEPENDENCY_KEYS[ Hook_Manager::class ]; + if ( ! isset( $jetpack_autoloader_container_shared[ $key ] ) ) { + require_once __DIR__ . '/class-hook-manager.php'; + $jetpack_autoloader_container_shared[ $key ] = new Hook_Manager(); + } + $this->dependencies[ Hook_Manager::class ] = &$jetpack_autoloader_container_shared[ $key ]; + } + + /** + * Registers all of the dependencies with the container. + */ + private function register_dependencies() { + require_once __DIR__ . '/class-path-processor.php'; + $this->dependencies[ Path_Processor::class ] = new Path_Processor(); + + require_once __DIR__ . '/class-plugin-locator.php'; + $this->dependencies[ Plugin_Locator::class ] = new Plugin_Locator( + $this->get( Path_Processor::class ) + ); + + require_once __DIR__ . '/class-version-selector.php'; + $this->dependencies[ Version_Selector::class ] = new Version_Selector(); + + require_once __DIR__ . '/class-autoloader-locator.php'; + $this->dependencies[ Autoloader_Locator::class ] = new Autoloader_Locator( + $this->get( Version_Selector::class ) + ); + + require_once __DIR__ . '/class-php-autoloader.php'; + $this->dependencies[ PHP_Autoloader::class ] = new PHP_Autoloader(); + + require_once __DIR__ . '/class-manifest-reader.php'; + $this->dependencies[ Manifest_Reader::class ] = new Manifest_Reader( + $this->get( Version_Selector::class ) + ); + + require_once __DIR__ . '/class-plugins-handler.php'; + $this->dependencies[ Plugins_Handler::class ] = new Plugins_Handler( + $this->get( Plugin_Locator::class ), + $this->get( Path_Processor::class ) + ); + + require_once __DIR__ . '/class-autoloader-handler.php'; + $this->dependencies[ Autoloader_Handler::class ] = new Autoloader_Handler( + $this->get( PHP_Autoloader::class ), + $this->get( Hook_Manager::class ), + $this->get( Manifest_Reader::class ), + $this->get( Version_Selector::class ) + ); + + require_once __DIR__ . '/class-latest-autoloader-guard.php'; + $this->dependencies[ Latest_Autoloader_Guard::class ] = new Latest_Autoloader_Guard( + $this->get( Plugins_Handler::class ), + $this->get( Autoloader_Handler::class ), + $this->get( Autoloader_Locator::class ) + ); + + // Register any classes that we will use elsewhere. + require_once __DIR__ . '/class-version-loader.php'; + require_once __DIR__ . '/class-shutdown-handler.php'; + } + + /** + * Initializes any of the globals needed by the autoloader. + */ + private function initialize_globals() { + /* + * This global was retired in version 2.9. The value is set to 'false' to maintain + * compatibility with older versions of the autoloader. + */ + global $jetpack_autoloader_including_latest; + $jetpack_autoloader_including_latest = false; + + // Not all plugins can be found using the locator. In cases where a plugin loads the autoloader + // but was not discoverable, we will record them in this array to track them as "active". + global $jetpack_autoloader_activating_plugins_paths; + if ( ! isset( $jetpack_autoloader_activating_plugins_paths ) ) { + $jetpack_autoloader_activating_plugins_paths = array(); + } + } +} diff --git a/vendor/jetpack-autoloader/class-hook-manager.php b/vendor/jetpack-autoloader/class-hook-manager.php new file mode 100644 index 0000000..c09c3d6 --- /dev/null +++ b/vendor/jetpack-autoloader/class-hook-manager.php @@ -0,0 +1,76 @@ +registered_hooks = array(); + } + + /** + * Adds an action to WordPress and registers it internally. + * + * @param string $tag The name of the action which is hooked. + * @param callable $callable The function to call. + * @param int $priority Used to specify the priority of the action. + * @param int $accepted_args Used to specify the number of arguments the callable accepts. + */ + public function add_action( $tag, $callable, $priority = 10, $accepted_args = 1 ) { + $this->registered_hooks[ $tag ][] = array( + 'priority' => $priority, + 'callable' => $callable, + ); + + add_action( $tag, $callable, $priority, $accepted_args ); + } + + /** + * Adds a filter to WordPress and registers it internally. + * + * @param string $tag The name of the filter which is hooked. + * @param callable $callable The function to call. + * @param int $priority Used to specify the priority of the filter. + * @param int $accepted_args Used to specify the number of arguments the callable accepts. + */ + public function add_filter( $tag, $callable, $priority = 10, $accepted_args = 1 ) { + $this->registered_hooks[ $tag ][] = array( + 'priority' => $priority, + 'callable' => $callable, + ); + + add_filter( $tag, $callable, $priority, $accepted_args ); + } + + /** + * Removes all of the registered hooks. + */ + public function reset() { + foreach ( $this->registered_hooks as $tag => $hooks ) { + foreach ( $hooks as $hook ) { + remove_filter( $tag, $hook['callable'], $hook['priority'] ); + } + } + $this->registered_hooks = array(); + } +} diff --git a/vendor/jetpack-autoloader/class-latest-autoloader-guard.php b/vendor/jetpack-autoloader/class-latest-autoloader-guard.php new file mode 100644 index 0000000..c632e4c --- /dev/null +++ b/vendor/jetpack-autoloader/class-latest-autoloader-guard.php @@ -0,0 +1,86 @@ +plugins_handler = $plugins_handler; + $this->autoloader_handler = $autoloader_handler; + $this->autoloader_locator = $autoloader_locator; + } + + /** + * Indicates whether or not the autoloader should be initialized. Note that this function + * has the side-effect of actually loading the latest autoloader in the event that this + * is not it. + * + * @param string $current_plugin The current plugin we're checking. + * @param string[] $plugins The active plugins to check for autoloaders in. + * @param bool $was_included_by_autoloader Indicates whether or not this autoloader was included by another. + * + * @return bool True if we should stop initialization, otherwise false. + */ + public function should_stop_init( $current_plugin, $plugins, $was_included_by_autoloader ) { + global $jetpack_autoloader_latest_version; + + // We need to reset the autoloader when the plugins change because + // that means the autoloader was generated with a different list. + if ( $this->plugins_handler->have_plugins_changed( $plugins ) ) { + $this->autoloader_handler->reset_autoloader(); + } + + // When the latest autoloader has already been found we don't need to search for it again. + // We should take care however because this will also trigger if the autoloader has been + // included by an older one. + if ( isset( $jetpack_autoloader_latest_version ) && ! $was_included_by_autoloader ) { + return true; + } + + $latest_plugin = $this->autoloader_locator->find_latest_autoloader( $plugins, $jetpack_autoloader_latest_version ); + if ( isset( $latest_plugin ) && $latest_plugin !== $current_plugin ) { + require $this->autoloader_locator->get_autoloader_path( $latest_plugin ); + return true; + } + + return false; + } +} diff --git a/vendor/jetpack-autoloader/class-manifest-reader.php b/vendor/jetpack-autoloader/class-manifest-reader.php new file mode 100644 index 0000000..eec1562 --- /dev/null +++ b/vendor/jetpack-autoloader/class-manifest-reader.php @@ -0,0 +1,99 @@ +version_selector = $version_selector; + } + + /** + * Reads all of the manifests in the given plugin paths. + * + * @param array $plugin_paths The paths to the plugins we're loading the manifest in. + * @param string $manifest_path The path that we're loading the manifest from in each plugin. + * @param array $path_map The path map to add the contents of the manifests to. + * + * @return array $path_map The path map we've built using the manifests in each plugin. + */ + public function read_manifests( $plugin_paths, $manifest_path, &$path_map ) { + $file_paths = array_map( + function ( $path ) use ( $manifest_path ) { + return trailingslashit( $path ) . $manifest_path; + }, + $plugin_paths + ); + + foreach ( $file_paths as $path ) { + $this->register_manifest( $path, $path_map ); + } + + return $path_map; + } + + /** + * Registers a plugin's manifest file with the path map. + * + * @param string $manifest_path The absolute path to the manifest that we're loading. + * @param array $path_map The path map to add the contents of the manifest to. + */ + protected function register_manifest( $manifest_path, &$path_map ) { + if ( ! is_readable( $manifest_path ) ) { + return; + } + + $manifest = require $manifest_path; + if ( ! is_array( $manifest ) ) { + return; + } + + foreach ( $manifest as $key => $data ) { + $this->register_record( $key, $data, $path_map ); + } + } + + /** + * Registers an entry from the manifest in the path map. + * + * @param string $key The identifier for the entry we're registering. + * @param array $data The data for the entry we're registering. + * @param array $path_map The path map to add the contents of the manifest to. + */ + protected function register_record( $key, $data, &$path_map ) { + if ( isset( $path_map[ $key ]['version'] ) ) { + $selected_version = $path_map[ $key ]['version']; + } else { + $selected_version = null; + } + + if ( $this->version_selector->is_version_update_required( $selected_version, $data['version'] ) ) { + $path_map[ $key ] = array( + 'version' => $data['version'], + 'path' => $data['path'], + ); + } + } +} diff --git a/vendor/jetpack-autoloader/class-path-processor.php b/vendor/jetpack-autoloader/class-path-processor.php new file mode 100644 index 0000000..575dee0 --- /dev/null +++ b/vendor/jetpack-autoloader/class-path-processor.php @@ -0,0 +1,194 @@ +get_normalized_constants(); + foreach ( $constants as $constant => $constant_path ) { + $len = strlen( $constant_path ); + if ( substr( $path, 0, $len ) !== $constant_path ) { + continue; + } + + return substr_replace( $path, '{{' . $constant . '}}', 0, $len ); + } + + return $path; + } + + /** + * Given a path this will replace any of the path constant tokens with the expanded path. + * + * @param string $tokenized_path The path we want to process. + * + * @return string The expanded path. + */ + public function untokenize_path_constants( $tokenized_path ) { + $tokenized_path = wp_normalize_path( $tokenized_path ); + + $constants = $this->get_normalized_constants(); + foreach ( $constants as $constant => $constant_path ) { + $constant = '{{' . $constant . '}}'; + + $len = strlen( $constant ); + if ( substr( $tokenized_path, 0, $len ) !== $constant ) { + continue; + } + + return $this->get_real_path( substr_replace( $tokenized_path, $constant_path, 0, $len ) ); + } + + return $tokenized_path; + } + + /** + * Given a file and an array of places it might be, this will find the absolute path and return it. + * + * @param string $file The plugin or theme file to resolve. + * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. + * + * @return string|false Returns the absolute path to the directory, otherwise false. + */ + public function find_directory_with_autoloader( $file, $directories_to_check ) { + $file = wp_normalize_path( $file ); + + if ( ! $this->is_absolute_path( $file ) ) { + $file = $this->find_absolute_plugin_path( $file, $directories_to_check ); + if ( ! isset( $file ) ) { + return false; + } + } + + // We need the real path for consistency with __DIR__ paths. + $file = $this->get_real_path( $file ); + + // phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged + $directory = @is_file( $file ) ? dirname( $file ) : $file; + if ( ! @is_file( $directory . '/vendor/composer/jetpack_autoload_classmap.php' ) ) { + return false; + } + // phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged + + return $directory; + } + + /** + * Fetches an array of normalized paths keyed by the constant they came from. + * + * @return string[] The normalized paths keyed by the constant. + */ + private function get_normalized_constants() { + $raw_constants = array( + // Order the constants from most-specific to least-specific. + 'WP_PLUGIN_DIR', + 'WPMU_PLUGIN_DIR', + 'WP_CONTENT_DIR', + 'ABSPATH', + ); + + $constants = array(); + foreach ( $raw_constants as $raw ) { + if ( ! defined( $raw ) ) { + continue; + } + + $path = wp_normalize_path( constant( $raw ) ); + if ( isset( $path ) ) { + $constants[ $raw ] = $path; + } + } + + return $constants; + } + + /** + * Indicates whether or not a path is absolute. + * + * @param string $path The path to check. + * + * @return bool True if the path is absolute, otherwise false. + */ + private function is_absolute_path( $path ) { + if ( 0 === strlen( $path ) || '.' === $path[0] ) { + return false; + } + + // Absolute paths on Windows may begin with a drive letter. + if ( preg_match( '/^[a-zA-Z]:[\/\\\\]/', $path ) ) { + return true; + } + + // A path starting with / or \ is absolute; anything else is relative. + return ( '/' === $path[0] || '\\' === $path[0] ); + } + + /** + * Given a file and a list of directories to check, this method will try to figure out + * the absolute path to the file in question. + * + * @param string $normalized_path The normalized path to the plugin or theme file to resolve. + * @param array $directories_to_check The directories we should check for the file if it isn't an absolute path. + * + * @return string|null The absolute path to the plugin file, otherwise null. + */ + private function find_absolute_plugin_path( $normalized_path, $directories_to_check ) { + // We're only able to find the absolute path for plugin/theme PHP files. + if ( ! is_string( $normalized_path ) || '.php' !== substr( $normalized_path, -4 ) ) { + return null; + } + + foreach ( $directories_to_check as $directory ) { + $normalized_check = wp_normalize_path( trailingslashit( $directory ) ) . $normalized_path; + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + if ( @is_file( $normalized_check ) ) { + return $normalized_check; + } + } + + return null; + } + + /** + * Given a path this will figure out the real path that we should be using. + * + * @param string $path The path to resolve. + * + * @return string The resolved path. + */ + private function get_real_path( $path ) { + // We want to resolve symbolic links for consistency with __DIR__ paths. + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + $real_path = @realpath( $path ); + if ( false === $real_path ) { + // Let the autoloader deal with paths that don't exist. + $real_path = $path; + } + + // Using realpath will make it platform-specific so we must normalize it after. + if ( $path !== $real_path ) { + $real_path = wp_normalize_path( $real_path ); + } + + return $real_path; + } +} diff --git a/vendor/jetpack-autoloader/class-php-autoloader.php b/vendor/jetpack-autoloader/class-php-autoloader.php new file mode 100644 index 0000000..b5bbda3 --- /dev/null +++ b/vendor/jetpack-autoloader/class-php-autoloader.php @@ -0,0 +1,90 @@ +unregister_autoloader(); + + // Set the global so that it can be used to load classes. + global $jetpack_autoloader_loader; + $jetpack_autoloader_loader = $version_loader; + + // Ensure that the autoloader is first to avoid contention with others. + spl_autoload_register( array( self::class, 'load_class' ), true, true ); + } + + /** + * Unregisters the active autoloader so that it will no longer autoload classes. + */ + public function unregister_autoloader() { + // Remove any v2 autoloader that we've already registered. + $autoload_chain = spl_autoload_functions(); + foreach ( $autoload_chain as $autoloader ) { + // We can identify a v2 autoloader using the namespace. + $namespace_check = null; + + // Functions are recorded as strings. + if ( is_string( $autoloader ) ) { + $namespace_check = $autoloader; + } elseif ( is_array( $autoloader ) && is_string( $autoloader[0] ) ) { + // Static method calls have the class as the first array element. + $namespace_check = $autoloader[0]; + } else { + // Since the autoloader has only ever been a function or a static method we don't currently need to check anything else. + continue; + } + + // Check for the namespace without the generated suffix. + if ( 'Automattic\\Jetpack\\Autoloader\\jp' === substr( $namespace_check, 0, 32 ) ) { + spl_autoload_unregister( $autoloader ); + } + } + + // Clear the global now that the autoloader has been unregistered. + global $jetpack_autoloader_loader; + $jetpack_autoloader_loader = null; + } + + /** + * Loads a class file if one could be found. + * + * Note: This function is static so that the autoloader can be easily unregistered. If + * it was a class method we would have to unwrap the object to check the namespace. + * + * @param string $class_name The name of the class to autoload. + * + * @return bool Indicates whether or not a class file was loaded. + */ + public static function load_class( $class_name ) { + global $jetpack_autoloader_loader; + if ( ! isset( $jetpack_autoloader_loader ) ) { + return; + } + + $file = $jetpack_autoloader_loader->find_class_file( $class_name ); + if ( ! isset( $file ) ) { + return false; + } + + require $file; + return true; + } +} diff --git a/vendor/jetpack-autoloader/class-plugin-locator.php b/vendor/jetpack-autoloader/class-plugin-locator.php new file mode 100644 index 0000000..ba2c6f9 --- /dev/null +++ b/vendor/jetpack-autoloader/class-plugin-locator.php @@ -0,0 +1,153 @@ +path_processor = $path_processor; + } + + /** + * Finds the path to the current plugin. + * + * @return string $path The path to the current plugin. + * + * @throws \RuntimeException If the current plugin does not have an autoloader. + */ + public function find_current_plugin() { + // Escape from `vendor/__DIR__` to root plugin directory. + $plugin_directory = dirname( dirname( __DIR__ ) ); + + // Use the path processor to ensure that this is an autoloader we're referencing. + $path = $this->path_processor->find_directory_with_autoloader( $plugin_directory, array() ); + if ( false === $path ) { + throw new \RuntimeException( 'Failed to locate plugin ' . $plugin_directory ); + } + + return $path; + } + + /** + * Checks a given option for plugin paths. + * + * @param string $option_name The option that we want to check for plugin information. + * @param bool $site_option Indicates whether or not we want to check the site option. + * + * @return array $plugin_paths The list of absolute paths we've found. + */ + public function find_using_option( $option_name, $site_option = false ) { + $raw = $site_option ? get_site_option( $option_name ) : get_option( $option_name ); + if ( false === $raw ) { + return array(); + } + + return $this->convert_plugins_to_paths( $raw ); + } + + /** + * Checks for plugins in the `action` request parameter. + * + * @param string[] $allowed_actions The actions that we're allowed to return plugins for. + * + * @return array $plugin_paths The list of absolute paths we've found. + */ + public function find_using_request_action( $allowed_actions ) { + // phpcs:disable WordPress.Security.NonceVerification.Recommended + + /** + * Note: we're not actually checking the nonce here because it's too early + * in the execution. The pluggable functions are not yet loaded to give + * plugins a chance to plug their versions. Therefore we're doing the bare + * minimum: checking whether the nonce exists and it's in the right place. + * The request will fail later if the nonce doesn't pass the check. + */ + if ( empty( $_REQUEST['_wpnonce'] ) ) { + return array(); + } + + $action = isset( $_REQUEST['action'] ) ? wp_unslash( $_REQUEST['action'] ) : false; + if ( ! in_array( $action, $allowed_actions, true ) ) { + return array(); + } + + $plugin_slugs = array(); + switch ( $action ) { + case 'activate': + case 'deactivate': + if ( empty( $_REQUEST['plugin'] ) ) { + break; + } + + $plugin_slugs[] = wp_unslash( $_REQUEST['plugin'] ); + break; + + case 'activate-selected': + case 'deactivate-selected': + if ( empty( $_REQUEST['checked'] ) ) { + break; + } + + $plugin_slugs = wp_unslash( $_REQUEST['checked'] ); + break; + } + + // phpcs:enable WordPress.Security.NonceVerification.Recommended + return $this->convert_plugins_to_paths( $plugin_slugs ); + } + + /** + * Given an array of plugin slugs or paths, this will convert them to absolute paths and filter + * out the plugins that are not directory plugins. Note that array keys will also be included + * if they are plugin paths! + * + * @param string[] $plugins Plugin paths or slugs to filter. + * + * @return string[] + */ + private function convert_plugins_to_paths( $plugins ) { + if ( ! is_array( $plugins ) || empty( $plugins ) ) { + return array(); + } + + // We're going to look for plugins in the standard directories. + $path_constants = array( WP_PLUGIN_DIR, WPMU_PLUGIN_DIR ); + + $plugin_paths = array(); + foreach ( $plugins as $key => $value ) { + $path = $this->path_processor->find_directory_with_autoloader( $key, $path_constants ); + if ( $path ) { + $plugin_paths[] = $path; + } + + $path = $this->path_processor->find_directory_with_autoloader( $value, $path_constants ); + if ( $path ) { + $plugin_paths[] = $path; + } + } + + return $plugin_paths; + } +} diff --git a/vendor/jetpack-autoloader/class-plugins-handler.php b/vendor/jetpack-autoloader/class-plugins-handler.php new file mode 100644 index 0000000..c132de5 --- /dev/null +++ b/vendor/jetpack-autoloader/class-plugins-handler.php @@ -0,0 +1,164 @@ +plugin_locator = $plugin_locator; + $this->path_processor = $path_processor; + } + + /** + * Gets all of the active plugins we can find. + * + * @param bool $include_deactivating When true, plugins deactivating this request will be considered active. + * @param bool $record_unknown When true, the current plugin will be marked as active and recorded when unknown. + * + * @return string[] + */ + public function get_active_plugins( $include_deactivating, $record_unknown ) { + global $jetpack_autoloader_activating_plugins_paths; + + // We're going to build a unique list of plugins from a few different sources + // to find all of our "active" plugins. While we need to return an integer + // array, we're going to use an associative array internally to reduce + // the amount of time that we're going to spend checking uniqueness + // and merging different arrays together to form the output. + $active_plugins = array(); + + // Make sure that plugins which have activated this request are considered as "active" even though + // they probably won't be present in any option. + if ( is_array( $jetpack_autoloader_activating_plugins_paths ) ) { + foreach ( $jetpack_autoloader_activating_plugins_paths as $path ) { + $active_plugins[ $path ] = $path; + } + } + + // This option contains all of the plugins that have been activated. + $plugins = $this->plugin_locator->find_using_option( 'active_plugins' ); + foreach ( $plugins as $path ) { + $active_plugins[ $path ] = $path; + } + + // This option contains all of the multisite plugins that have been activated. + if ( is_multisite() ) { + $plugins = $this->plugin_locator->find_using_option( 'active_sitewide_plugins', true ); + foreach ( $plugins as $path ) { + $active_plugins[ $path ] = $path; + } + } + + // These actions contain plugins that are being activated/deactivated during this request. + $plugins = $this->plugin_locator->find_using_request_action( array( 'activate', 'activate-selected', 'deactivate', 'deactivate-selected' ) ); + foreach ( $plugins as $path ) { + $active_plugins[ $path ] = $path; + } + + // When the current plugin isn't considered "active" there's a problem. + // Since we're here, the plugin is active and currently being loaded. + // We can support this case (mu-plugins and non-standard activation) + // by adding the current plugin to the active list and marking it + // as an unknown (activating) plugin. This also has the benefit + // of causing a reset because the active plugins list has + // been changed since it was saved in the global. + $current_plugin = $this->plugin_locator->find_current_plugin(); + if ( $record_unknown && ! in_array( $current_plugin, $active_plugins, true ) ) { + $active_plugins[ $current_plugin ] = $current_plugin; + $jetpack_autoloader_activating_plugins_paths[] = $current_plugin; + } + + // When deactivating plugins aren't desired we should entirely remove them from the active list. + if ( ! $include_deactivating ) { + // These actions contain plugins that are being deactivated during this request. + $plugins = $this->plugin_locator->find_using_request_action( array( 'deactivate', 'deactivate-selected' ) ); + foreach ( $plugins as $path ) { + unset( $active_plugins[ $path ] ); + } + } + + // Transform the array so that we don't have to worry about the keys interacting with other array types later. + return array_values( $active_plugins ); + } + + /** + * Gets all of the cached plugins if there are any. + * + * @return string[] + */ + public function get_cached_plugins() { + $cached = get_transient( self::TRANSIENT_KEY ); + if ( ! is_array( $cached ) || empty( $cached ) ) { + return array(); + } + + // We need to expand the tokens to an absolute path for this webserver. + return array_map( array( $this->path_processor, 'untokenize_path_constants' ), $cached ); + } + + /** + * Saves the plugin list to the cache. + * + * @param array $plugins The plugin list to save to the cache. + */ + public function cache_plugins( $plugins ) { + // We store the paths in a tokenized form so that that webservers with different absolute paths don't break. + $plugins = array_map( array( $this->path_processor, 'tokenize_path_constants' ), $plugins ); + + set_transient( self::TRANSIENT_KEY, $plugins ); + } + + /** + * Checks to see whether or not the plugin list given has changed when compared to the + * shared `$jetpack_autoloader_cached_plugin_paths` global. This allows us to deal + * with cases where the active list may change due to filtering.. + * + * @param string[] $plugins The plugins list to check against the global cache. + * + * @return bool True if the plugins have changed, otherwise false. + */ + public function have_plugins_changed( $plugins ) { + global $jetpack_autoloader_cached_plugin_paths; + + if ( $jetpack_autoloader_cached_plugin_paths !== $plugins ) { + $jetpack_autoloader_cached_plugin_paths = $plugins; + return true; + } + + return false; + } +} diff --git a/vendor/jetpack-autoloader/class-shutdown-handler.php b/vendor/jetpack-autoloader/class-shutdown-handler.php new file mode 100644 index 0000000..55f517c --- /dev/null +++ b/vendor/jetpack-autoloader/class-shutdown-handler.php @@ -0,0 +1,92 @@ +plugins_handler = $plugins_handler; + $this->cached_plugins = $cached_plugins; + $this->was_included_by_autoloader = $was_included_by_autoloader; + } + + /** + * Handles the shutdown of the autoloader. + */ + public function __invoke() { + // Don't save a broken cache if an error happens during some plugin's initialization. + if ( ! did_action( 'plugins_loaded' ) ) { + // Ensure that the cache is emptied to prevent consecutive failures if the cache is to blame. + if ( ! empty( $this->cached_plugins ) ) { + $this->plugins_handler->cache_plugins( array() ); + } + + return; + } + + // Load the active plugins fresh since the list we pulled earlier might not contain + // plugins that were activated but did not reset the autoloader. This happens + // when a plugin is in the cache but not "active" when the autoloader loads. + // We also want to make sure that plugins which are deactivating are not + // considered "active" so that they will be removed from the cache now. + try { + $active_plugins = $this->plugins_handler->get_active_plugins( false, ! $this->was_included_by_autoloader ); + } catch ( \Exception $ex ) { + // When the package is deleted before shutdown it will throw an exception. + // In the event this happens we should erase the cache. + if ( ! empty( $this->cached_plugins ) ) { + $this->plugins_handler->cache_plugins( array() ); + } + return; + } + + // The paths should be sorted for easy comparisons with those loaded from the cache. + // Note we don't need to sort the cached entries because they're already sorted. + sort( $active_plugins ); + + // We don't want to waste time saving a cache that hasn't changed. + if ( $this->cached_plugins === $active_plugins ) { + return; + } + + $this->plugins_handler->cache_plugins( $active_plugins ); + } +} diff --git a/vendor/jetpack-autoloader/class-version-loader.php b/vendor/jetpack-autoloader/class-version-loader.php new file mode 100644 index 0000000..bc40b1e --- /dev/null +++ b/vendor/jetpack-autoloader/class-version-loader.php @@ -0,0 +1,164 @@ +version_selector = $version_selector; + $this->classmap = $classmap; + $this->psr4_map = $psr4_map; + $this->filemap = $filemap; + } + + /** + * Finds the file path for the given class. + * + * @param string $class_name The class to find. + * + * @return string|null $file_path The path to the file if found, null if no class was found. + */ + public function find_class_file( $class_name ) { + $data = $this->select_newest_file( + isset( $this->classmap[ $class_name ] ) ? $this->classmap[ $class_name ] : null, + $this->find_psr4_file( $class_name ) + ); + if ( ! isset( $data ) ) { + return null; + } + + return $data['path']; + } + + /** + * Load all of the files in the filemap. + */ + public function load_filemap() { + if ( empty( $this->filemap ) ) { + return; + } + + foreach ( $this->filemap as $file_identifier => $file_data ) { + if ( empty( $GLOBALS['__composer_autoload_files'][ $file_identifier ] ) ) { + require_once $file_data['path']; + + $GLOBALS['__composer_autoload_files'][ $file_identifier ] = true; + } + } + } + + /** + * Compares different class sources and returns the newest. + * + * @param array|null $classmap_data The classmap class data. + * @param array|null $psr4_data The PSR-4 class data. + * + * @return array|null $data + */ + private function select_newest_file( $classmap_data, $psr4_data ) { + if ( ! isset( $classmap_data ) ) { + return $psr4_data; + } elseif ( ! isset( $psr4_data ) ) { + return $classmap_data; + } + + if ( $this->version_selector->is_version_update_required( $classmap_data['version'], $psr4_data['version'] ) ) { + return $psr4_data; + } + + return $classmap_data; + } + + /** + * Finds the file for a given class in a PSR-4 namespace. + * + * @param string $class_name The class to find. + * + * @return array|null $data The version and path path to the file if found, null otherwise. + */ + private function find_psr4_file( $class_name ) { + if ( ! isset( $this->psr4_map ) ) { + return null; + } + + // Don't bother with classes that have no namespace. + $class_index = strrpos( $class_name, '\\' ); + if ( ! $class_index ) { + return null; + } + $class_for_path = str_replace( '\\', '/', $class_name ); + + // Search for the namespace by iteratively cutting off the last segment until + // we find a match. This allows us to check the most-specific namespaces + // first as well as minimize the amount of time spent looking. + for ( + $class_namespace = substr( $class_name, 0, $class_index ); + ! empty( $class_namespace ); + $class_namespace = substr( $class_namespace, 0, strrpos( $class_namespace, '\\' ) ) + ) { + $namespace = $class_namespace . '\\'; + if ( ! isset( $this->psr4_map[ $namespace ] ) ) { + continue; + } + $data = $this->psr4_map[ $namespace ]; + + foreach ( $data['path'] as $path ) { + $path .= '/' . substr( $class_for_path, strlen( $namespace ) ) . '.php'; + if ( file_exists( $path ) ) { + return array( + 'version' => $data['version'], + 'path' => $path, + ); + } + } + } + + return null; + } +} diff --git a/vendor/jetpack-autoloader/class-version-selector.php b/vendor/jetpack-autoloader/class-version-selector.php new file mode 100644 index 0000000..bcd4dc6 --- /dev/null +++ b/vendor/jetpack-autoloader/class-version-selector.php @@ -0,0 +1,69 @@ +is_dev_version( $selected_version ) ) { + return false; + } + + if ( $this->is_dev_version( $compare_version ) ) { + if ( $use_dev_versions ) { + return true; + } else { + return false; + } + } + + if ( version_compare( $selected_version, $compare_version, '<' ) ) { + return true; + } + + return false; + } + + /** + * Checks whether the given package version is a development version. + * + * @param String $version The package version. + * + * @return bool True if the version is a dev version, else false. + */ + public function is_dev_version( $version ) { + if ( 'dev-' === substr( $version, 0, 4 ) || '9999999-dev' === $version ) { + return true; + } + + return false; + } +} diff --git a/vendor/maxmind-db/reader/LICENSE b/vendor/maxmind-db/reader/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/vendor/maxmind-db/reader/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/maxmind-db/reader/autoload.php b/vendor/maxmind-db/reader/autoload.php new file mode 100644 index 0000000..1314b69 --- /dev/null +++ b/vendor/maxmind-db/reader/autoload.php @@ -0,0 +1,45 @@ +class. + * + * @param string $class + * the name of the class to load + */ +function mmdb_autoload($class) +{ + /* + * A project-specific mapping between the namespaces and where + * they're located. By convention, we include the trailing + * slashes. The one-element array here simply makes things easy + * to extend in the future if (for example) the test classes + * begin to use one another. + */ + $namespace_map = ['MaxMind\\Db\\' => __DIR__ . '/src/MaxMind/Db/']; + + foreach ($namespace_map as $prefix => $dir) { + /* First swap out the namespace prefix with a directory... */ + $path = str_replace($prefix, $dir, $class); + + /* replace the namespace separator with a directory separator... */ + $path = str_replace('\\', '/', $path); + + /* and finally, add the PHP file extension to the result. */ + $path = $path . '.php'; + + /* $path should now contain the path to a PHP file defining $class */ + if (file_exists($path)) { + include $path; + } + } +} + +spl_autoload_register('mmdb_autoload'); diff --git a/vendor/maxmind-db/reader/ext/config.m4 b/vendor/maxmind-db/reader/ext/config.m4 new file mode 100644 index 0000000..675e00c --- /dev/null +++ b/vendor/maxmind-db/reader/ext/config.m4 @@ -0,0 +1,40 @@ +PHP_ARG_WITH(maxminddb, + [Whether to enable the MaxMind DB Reader extension], + [ --with-maxminddb Enable MaxMind DB Reader extension support]) + +PHP_ARG_ENABLE(maxminddb-debug, for MaxMind DB debug support, + [ --enable-maxminddb-debug Enable enable MaxMind DB deubg support], no, no) + +if test $PHP_MAXMINDDB != "no"; then + + AC_PATH_PROG(PKG_CONFIG, pkg-config, no) + + AC_MSG_CHECKING(for libmaxminddb) + if test -x "$PKG_CONFIG" && $PKG_CONFIG --exists libmaxminddb; then + dnl retrieve build options from pkg-config + if $PKG_CONFIG libmaxminddb --atleast-version 1.0.0; then + LIBMAXMINDDB_INC=`$PKG_CONFIG libmaxminddb --cflags` + LIBMAXMINDDB_LIB=`$PKG_CONFIG libmaxminddb --libs` + LIBMAXMINDDB_VER=`$PKG_CONFIG libmaxminddb --modversion` + AC_MSG_RESULT(found version $LIBMAXMINDDB_VER) + else + AC_MSG_ERROR(system libmaxminddb must be upgraded to version >= 1.0.0) + fi + PHP_EVAL_LIBLINE($LIBMAXMINDDB_LIB, MAXMINDDB_SHARED_LIBADD) + PHP_EVAL_INCLINE($LIBMAXMINDDB_INC) + else + AC_MSG_RESULT(pkg-config information missing) + AC_MSG_WARN(will use libmaxmxinddb from compiler default path) + + PHP_CHECK_LIBRARY(maxminddb, MMDB_open) + PHP_ADD_LIBRARY(maxminddb, 1, MAXMINDDB_SHARED_LIBADD) + fi + + if test $PHP_MAXMINDDB_DEBUG != "no"; then + CFLAGS="$CFLAGS -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Werror" + fi + + PHP_SUBST(MAXMINDDB_SHARED_LIBADD) + + PHP_NEW_EXTENSION(maxminddb, maxminddb.c, $ext_shared) +fi diff --git a/vendor/maxmind-db/reader/ext/maxminddb.c b/vendor/maxmind-db/reader/ext/maxminddb.c new file mode 100644 index 0000000..a97a3d9 --- /dev/null +++ b/vendor/maxmind-db/reader/ext/maxminddb.c @@ -0,0 +1,704 @@ +/* MaxMind, Inc., licenses this file to you under the Apache License, Version + * 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include "php_maxminddb.h" + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include + +#include "Zend/zend_exceptions.h" +#include "ext/standard/info.h" +#include + +#ifdef ZTS +#include +#endif + +#define __STDC_FORMAT_MACROS +#include + +#define PHP_MAXMINDDB_NS ZEND_NS_NAME("MaxMind", "Db") +#define PHP_MAXMINDDB_READER_NS ZEND_NS_NAME(PHP_MAXMINDDB_NS, "Reader") +#define PHP_MAXMINDDB_READER_EX_NS \ + ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "InvalidDatabaseException") + +#ifdef ZEND_ENGINE_3 +#define Z_MAXMINDDB_P(zv) php_maxminddb_fetch_object(Z_OBJ_P(zv)) +#define _ZVAL_STRING ZVAL_STRING +#define _ZVAL_STRINGL ZVAL_STRINGL +typedef size_t strsize_t; +typedef zend_object free_obj_t; +#else +#define Z_MAXMINDDB_P(zv) \ + (maxminddb_obj *)zend_object_store_get_object(zv TSRMLS_CC) +#define _ZVAL_STRING(a, b) ZVAL_STRING(a, b, 1) +#define _ZVAL_STRINGL(a, b, c) ZVAL_STRINGL(a, b, c, 1) +typedef int strsize_t; +typedef void free_obj_t; +#endif + +/* For PHP 8 compatibility */ +#ifndef TSRMLS_C +#define TSRMLS_C +#endif +#ifndef TSRMLS_CC +#define TSRMLS_CC +#endif +#ifndef TSRMLS_DC +#define TSRMLS_DC +#endif +#ifndef ZEND_ACC_CTOR +#define ZEND_ACC_CTOR 0 +#endif + +#ifdef ZEND_ENGINE_3 +typedef struct _maxminddb_obj { + MMDB_s *mmdb; + zend_object std; +} maxminddb_obj; +#else +typedef struct _maxminddb_obj { + zend_object std; + MMDB_s *mmdb; +} maxminddb_obj; +#endif + +PHP_FUNCTION(maxminddb); + +static int +get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len); +static const MMDB_entry_data_list_s * +handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC); +static const MMDB_entry_data_list_s * +handle_array(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC); +static const MMDB_entry_data_list_s * +handle_map(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC); +static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC); +static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC); +static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC); +static zend_class_entry *lookup_class(const char *name TSRMLS_DC); + +#define CHECK_ALLOCATED(val) \ + if (!val) { \ + zend_error(E_ERROR, "Out of memory"); \ + return; \ + } + +#define THROW_EXCEPTION(name, ...) \ + { \ + zend_class_entry *exception_ce = lookup_class(name TSRMLS_CC); \ + zend_throw_exception_ex(exception_ce, 0 TSRMLS_CC, __VA_ARGS__); \ + } + +#if PHP_VERSION_ID < 50399 +#define object_properties_init(zo, class_type) \ + { \ + zval *tmp; \ + zend_hash_copy((*zo).properties, \ + &class_type->default_properties, \ + (copy_ctor_func_t)zval_add_ref, \ + (void *)&tmp, \ + sizeof(zval *)); \ + } +#endif + +static zend_object_handlers maxminddb_obj_handlers; +static zend_class_entry *maxminddb_ce; + +static inline maxminddb_obj * +php_maxminddb_fetch_object(zend_object *obj TSRMLS_DC) { +#ifdef ZEND_ENGINE_3 + return (maxminddb_obj *)((char *)(obj)-XtOffsetOf(maxminddb_obj, std)); +#else + return (maxminddb_obj *)obj; +#endif +} + +ZEND_BEGIN_ARG_INFO_EX(arginfo_maxmindbreader_construct, 0, 0, 1) +ZEND_ARG_INFO(0, db_file) +ZEND_END_ARG_INFO() + +PHP_METHOD(MaxMind_Db_Reader, __construct) { + char *db_file = NULL; + strsize_t name_len; + zval *_this_zval = NULL; + + if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, + getThis(), + "Os", + &_this_zval, + maxminddb_ce, + &db_file, + &name_len) == FAILURE) { + THROW_EXCEPTION("InvalidArgumentException", + "The constructor takes exactly one argument."); + return; + } + + if (0 != php_check_open_basedir(db_file TSRMLS_CC) || + 0 != access(db_file, R_OK)) { + THROW_EXCEPTION("InvalidArgumentException", + "The file \"%s\" does not exist or is not readable.", + db_file); + return; + } + + MMDB_s *mmdb = (MMDB_s *)ecalloc(1, sizeof(MMDB_s)); + uint16_t status = MMDB_open(db_file, MMDB_MODE_MMAP, mmdb); + + if (MMDB_SUCCESS != status) { + THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, + "Error opening database file (%s). Is this a valid " + "MaxMind DB file?", + db_file); + efree(mmdb); + return; + } + + maxminddb_obj *mmdb_obj = Z_MAXMINDDB_P(getThis()); + mmdb_obj->mmdb = mmdb; +} + +ZEND_BEGIN_ARG_INFO_EX(arginfo_maxmindbreader_get, 0, 0, 1) +ZEND_ARG_INFO(0, ip_address) +ZEND_END_ARG_INFO() + +PHP_METHOD(MaxMind_Db_Reader, get) { + int prefix_len = 0; + get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, return_value, &prefix_len); +} + +PHP_METHOD(MaxMind_Db_Reader, getWithPrefixLen) { + zval *record, *z_prefix_len; +#ifdef ZEND_ENGINE_3 + zval _record, _z_prefix_len; + record = &_record; + z_prefix_len = &_z_prefix_len; +#else + ALLOC_INIT_ZVAL(record); + ALLOC_INIT_ZVAL(z_prefix_len); +#endif + + int prefix_len = 0; + if (get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, record, &prefix_len)) { + return; + } + + array_init(return_value); + add_next_index_zval(return_value, record); + + ZVAL_LONG(z_prefix_len, prefix_len); + add_next_index_zval(return_value, z_prefix_len); +} + +static int +get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len) { + char *ip_address = NULL; + strsize_t name_len; + zval *_this_zval = NULL; + + if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC, + getThis(), + "Os", + &_this_zval, + maxminddb_ce, + &ip_address, + &name_len) == FAILURE) { + THROW_EXCEPTION("InvalidArgumentException", + "Method takes exactly one argument."); + return 1; + } + + const maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(getThis()); + + MMDB_s *mmdb = mmdb_obj->mmdb; + + if (NULL == mmdb) { + THROW_EXCEPTION("BadMethodCallException", + "Attempt to read from a closed MaxMind DB."); + return 1; + } + + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_flags = AI_NUMERICHOST, + // We set ai_socktype so that we only get one result back + .ai_socktype = SOCK_STREAM}; + + struct addrinfo *addresses = NULL; + int gai_status = getaddrinfo(ip_address, NULL, &hints, &addresses); + if (gai_status) { + THROW_EXCEPTION("InvalidArgumentException", + "The value \"%s\" is not a valid IP address.", + ip_address); + return 1; + } + if (!addresses || !addresses->ai_addr) { + THROW_EXCEPTION( + "InvalidArgumentException", + "getaddrinfo was successful but failed to set the addrinfo"); + return 1; + } + + int sa_family = addresses->ai_addr->sa_family; + + int mmdb_error = MMDB_SUCCESS; + MMDB_lookup_result_s result = + MMDB_lookup_sockaddr(mmdb, addresses->ai_addr, &mmdb_error); + + freeaddrinfo(addresses); + + if (MMDB_SUCCESS != mmdb_error) { + char *exception_name; + if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) { + exception_name = "InvalidArgumentException"; + } else { + exception_name = PHP_MAXMINDDB_READER_EX_NS; + } + THROW_EXCEPTION(exception_name, + "Error looking up %s. %s", + ip_address, + MMDB_strerror(mmdb_error)); + return 1; + } + + *prefix_len = result.netmask; + + if (sa_family == AF_INET && mmdb->metadata.ip_version == 6) { + // We return the prefix length given the IPv4 address. If there is + // no IPv4 subtree, we return a prefix length of 0. + *prefix_len = *prefix_len >= 96 ? *prefix_len - 96 : 0; + } + + if (!result.found_entry) { + ZVAL_NULL(record); + return 0; + } + + MMDB_entry_data_list_s *entry_data_list = NULL; + int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list); + + if (MMDB_SUCCESS != status) { + THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, + "Error while looking up data for %s. %s", + ip_address, + MMDB_strerror(status)); + MMDB_free_entry_data_list(entry_data_list); + return 1; + } else if (NULL == entry_data_list) { + THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, + "Error while looking up data for %s. Your database may " + "be corrupt or you have found a bug in libmaxminddb.", + ip_address); + return 1; + } + + handle_entry_data_list(entry_data_list, record TSRMLS_CC); + MMDB_free_entry_data_list(entry_data_list); + return 0; +} + +ZEND_BEGIN_ARG_INFO_EX(arginfo_maxmindbreader_void, 0, 0, 0) +ZEND_END_ARG_INFO() + +PHP_METHOD(MaxMind_Db_Reader, metadata) { + if (ZEND_NUM_ARGS() != 0) { + THROW_EXCEPTION("InvalidArgumentException", + "Method takes no arguments."); + return; + } + + const maxminddb_obj *const mmdb_obj = + (maxminddb_obj *)Z_MAXMINDDB_P(getThis()); + + if (NULL == mmdb_obj->mmdb) { + THROW_EXCEPTION("BadMethodCallException", + "Attempt to read from a closed MaxMind DB."); + return; + } + + const char *const name = ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "Metadata"); + zend_class_entry *metadata_ce = lookup_class(name TSRMLS_CC); + + object_init_ex(return_value, metadata_ce); + +#ifdef ZEND_ENGINE_3 + zval _metadata_array; + zval *metadata_array = &_metadata_array; + ZVAL_NULL(metadata_array); +#else + zval *metadata_array; + ALLOC_INIT_ZVAL(metadata_array); +#endif + + MMDB_entry_data_list_s *entry_data_list; + MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list); + + handle_entry_data_list(entry_data_list, metadata_array TSRMLS_CC); + MMDB_free_entry_data_list(entry_data_list); +#if PHP_VERSION_ID >= 80000 + zend_call_method_with_1_params(Z_OBJ_P(return_value), + metadata_ce, + &metadata_ce->constructor, + ZEND_CONSTRUCTOR_FUNC_NAME, + NULL, + metadata_array); + zval_ptr_dtor(metadata_array); +#elif defined(ZEND_ENGINE_3) + zend_call_method_with_1_params(return_value, + metadata_ce, + &metadata_ce->constructor, + ZEND_CONSTRUCTOR_FUNC_NAME, + NULL, + metadata_array); + zval_ptr_dtor(metadata_array); +#else + zend_call_method_with_1_params(&return_value, + metadata_ce, + &metadata_ce->constructor, + ZEND_CONSTRUCTOR_FUNC_NAME, + NULL, + metadata_array); + zval_ptr_dtor(&metadata_array); +#endif +} + +PHP_METHOD(MaxMind_Db_Reader, close) { + if (ZEND_NUM_ARGS() != 0) { + THROW_EXCEPTION("InvalidArgumentException", + "Method takes no arguments."); + return; + } + + maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(getThis()); + + if (NULL == mmdb_obj->mmdb) { + THROW_EXCEPTION("BadMethodCallException", + "Attempt to close a closed MaxMind DB."); + return; + } + MMDB_close(mmdb_obj->mmdb); + efree(mmdb_obj->mmdb); + mmdb_obj->mmdb = NULL; +} + +static const MMDB_entry_data_list_s * +handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC) { + switch (entry_data_list->entry_data.type) { + case MMDB_DATA_TYPE_MAP: + return handle_map(entry_data_list, z_value TSRMLS_CC); + case MMDB_DATA_TYPE_ARRAY: + return handle_array(entry_data_list, z_value TSRMLS_CC); + case MMDB_DATA_TYPE_UTF8_STRING: + _ZVAL_STRINGL(z_value, + (char *)entry_data_list->entry_data.utf8_string, + entry_data_list->entry_data.data_size); + break; + case MMDB_DATA_TYPE_BYTES: + _ZVAL_STRINGL(z_value, + (char *)entry_data_list->entry_data.bytes, + entry_data_list->entry_data.data_size); + break; + case MMDB_DATA_TYPE_DOUBLE: + ZVAL_DOUBLE(z_value, entry_data_list->entry_data.double_value); + break; + case MMDB_DATA_TYPE_FLOAT: + ZVAL_DOUBLE(z_value, entry_data_list->entry_data.float_value); + break; + case MMDB_DATA_TYPE_UINT16: + ZVAL_LONG(z_value, entry_data_list->entry_data.uint16); + break; + case MMDB_DATA_TYPE_UINT32: + handle_uint32(entry_data_list, z_value TSRMLS_CC); + break; + case MMDB_DATA_TYPE_BOOLEAN: + ZVAL_BOOL(z_value, entry_data_list->entry_data.boolean); + break; + case MMDB_DATA_TYPE_UINT64: + handle_uint64(entry_data_list, z_value TSRMLS_CC); + break; + case MMDB_DATA_TYPE_UINT128: + handle_uint128(entry_data_list, z_value TSRMLS_CC); + break; + case MMDB_DATA_TYPE_INT32: + ZVAL_LONG(z_value, entry_data_list->entry_data.int32); + break; + default: + THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, + "Invalid data type arguments: %d", + entry_data_list->entry_data.type); + return NULL; + } + return entry_data_list; +} + +static const MMDB_entry_data_list_s * +handle_map(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC) { + array_init(z_value); + const uint32_t map_size = entry_data_list->entry_data.data_size; + + uint i; + for (i = 0; i < map_size && entry_data_list; i++) { + entry_data_list = entry_data_list->next; + + char *key = estrndup((char *)entry_data_list->entry_data.utf8_string, + entry_data_list->entry_data.data_size); + if (NULL == key) { + THROW_EXCEPTION(PHP_MAXMINDDB_READER_EX_NS, + "Invalid data type arguments"); + return NULL; + } + + entry_data_list = entry_data_list->next; +#ifdef ZEND_ENGINE_3 + zval _new_value; + zval *new_value = &_new_value; + ZVAL_NULL(new_value); +#else + zval *new_value; + ALLOC_INIT_ZVAL(new_value); +#endif + entry_data_list = + handle_entry_data_list(entry_data_list, new_value TSRMLS_CC); + add_assoc_zval(z_value, key, new_value); + efree(key); + } + return entry_data_list; +} + +static const MMDB_entry_data_list_s * +handle_array(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC) { + const uint32_t size = entry_data_list->entry_data.data_size; + + array_init(z_value); + + uint i; + for (i = 0; i < size && entry_data_list; i++) { + entry_data_list = entry_data_list->next; +#ifdef ZEND_ENGINE_3 + zval _new_value; + zval *new_value = &_new_value; + ZVAL_NULL(new_value); +#else + zval *new_value; + ALLOC_INIT_ZVAL(new_value); +#endif + entry_data_list = + handle_entry_data_list(entry_data_list, new_value TSRMLS_CC); + add_next_index_zval(z_value, new_value); + } + return entry_data_list; +} + +static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC) { + uint64_t high = 0; + uint64_t low = 0; +#if MMDB_UINT128_IS_BYTE_ARRAY + int i; + for (i = 0; i < 8; i++) { + high = (high << 8) | entry_data_list->entry_data.uint128[i]; + } + + for (i = 8; i < 16; i++) { + low = (low << 8) | entry_data_list->entry_data.uint128[i]; + } +#else + high = entry_data_list->entry_data.uint128 >> 64; + low = (uint64_t)entry_data_list->entry_data.uint128; +#endif + + char *num_str; + spprintf(&num_str, 0, "0x%016" PRIX64 "%016" PRIX64, high, low); + CHECK_ALLOCATED(num_str); + + _ZVAL_STRING(z_value, num_str); + efree(num_str); +} + +static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC) { + uint32_t val = entry_data_list->entry_data.uint32; + +#if LONG_MAX >= UINT32_MAX + ZVAL_LONG(z_value, val); + return; +#else + if (val <= LONG_MAX) { + ZVAL_LONG(z_value, val); + return; + } + + char *int_str; + spprintf(&int_str, 0, "%" PRIu32, val); + CHECK_ALLOCATED(int_str); + + _ZVAL_STRING(z_value, int_str); + efree(int_str); +#endif +} + +static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list, + zval *z_value TSRMLS_DC) { + uint64_t val = entry_data_list->entry_data.uint64; + +#if LONG_MAX >= UINT64_MAX + ZVAL_LONG(z_value, val); + return; +#else + if (val <= LONG_MAX) { + ZVAL_LONG(z_value, val); + return; + } + + char *int_str; + spprintf(&int_str, 0, "%" PRIu64, val); + CHECK_ALLOCATED(int_str); + + _ZVAL_STRING(z_value, int_str); + efree(int_str); +#endif +} + +static zend_class_entry *lookup_class(const char *name TSRMLS_DC) { +#ifdef ZEND_ENGINE_3 + zend_string *n = zend_string_init(name, strlen(name), 0); + zend_class_entry *ce = zend_lookup_class(n); + zend_string_release(n); + if (NULL == ce) { + zend_error(E_ERROR, "Class %s not found", name); + } + return ce; +#else + zend_class_entry **ce; + if (FAILURE == zend_lookup_class(name, strlen(name), &ce TSRMLS_CC)) { + zend_error(E_ERROR, "Class %s not found", name); + } + return *ce; +#endif +} + +static void maxminddb_free_storage(free_obj_t *object TSRMLS_DC) { + maxminddb_obj *obj = + php_maxminddb_fetch_object((zend_object *)object TSRMLS_CC); + if (obj->mmdb != NULL) { + MMDB_close(obj->mmdb); + efree(obj->mmdb); + } + + zend_object_std_dtor(&obj->std TSRMLS_CC); +#ifndef ZEND_ENGINE_3 + efree(object); +#endif +} + +#ifdef ZEND_ENGINE_3 +static zend_object *maxminddb_create_handler(zend_class_entry *type TSRMLS_DC) { + maxminddb_obj *obj = (maxminddb_obj *)ecalloc(1, sizeof(maxminddb_obj)); + zend_object_std_init(&obj->std, type TSRMLS_CC); + object_properties_init(&(obj->std), type); + + obj->std.handlers = &maxminddb_obj_handlers; + + return &obj->std; +} +#else +static zend_object_value +maxminddb_create_handler(zend_class_entry *type TSRMLS_DC) { + zend_object_value retval; + + maxminddb_obj *obj = (maxminddb_obj *)ecalloc(1, sizeof(maxminddb_obj)); + zend_object_std_init(&obj->std, type TSRMLS_CC); + object_properties_init(&(obj->std), type); + + retval.handle = zend_objects_store_put( + obj, NULL, maxminddb_free_storage, NULL TSRMLS_CC); + retval.handlers = &maxminddb_obj_handlers; + + return retval; +} +#endif + +// clang-format off +static zend_function_entry maxminddb_methods[] = { + PHP_ME(MaxMind_Db_Reader, __construct, arginfo_maxmindbreader_construct, + ZEND_ACC_PUBLIC | ZEND_ACC_CTOR) + PHP_ME(MaxMind_Db_Reader, close, arginfo_maxmindbreader_void, ZEND_ACC_PUBLIC) + PHP_ME(MaxMind_Db_Reader, get, arginfo_maxmindbreader_get, ZEND_ACC_PUBLIC) + PHP_ME(MaxMind_Db_Reader, getWithPrefixLen, arginfo_maxmindbreader_get, ZEND_ACC_PUBLIC) + PHP_ME(MaxMind_Db_Reader, metadata, arginfo_maxmindbreader_void, ZEND_ACC_PUBLIC) + { NULL, NULL, NULL } +}; +// clang-format on + +PHP_MINIT_FUNCTION(maxminddb) { + zend_class_entry ce; + + INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_NS, maxminddb_methods); + maxminddb_ce = zend_register_internal_class(&ce TSRMLS_CC); + maxminddb_ce->create_object = maxminddb_create_handler; + memcpy(&maxminddb_obj_handlers, + zend_get_std_object_handlers(), + sizeof(zend_object_handlers)); + maxminddb_obj_handlers.clone_obj = NULL; +#ifdef ZEND_ENGINE_3 + maxminddb_obj_handlers.offset = XtOffsetOf(maxminddb_obj, std); + maxminddb_obj_handlers.free_obj = maxminddb_free_storage; +#endif + zend_declare_class_constant_string(maxminddb_ce, + "MMDB_LIB_VERSION", + sizeof("MMDB_LIB_VERSION") - 1, + MMDB_lib_version() TSRMLS_CC); + + return SUCCESS; +} + +static PHP_MINFO_FUNCTION(maxminddb) { + php_info_print_table_start(); + + php_info_print_table_row(2, "MaxMind DB Reader", "enabled"); + php_info_print_table_row( + 2, "maxminddb extension version", PHP_MAXMINDDB_VERSION); + php_info_print_table_row( + 2, "libmaxminddb library version", MMDB_lib_version()); + + php_info_print_table_end(); +} + +zend_module_entry maxminddb_module_entry = {STANDARD_MODULE_HEADER, + PHP_MAXMINDDB_EXTNAME, + NULL, + PHP_MINIT(maxminddb), + NULL, + NULL, + NULL, + PHP_MINFO(maxminddb), + PHP_MAXMINDDB_VERSION, + STANDARD_MODULE_PROPERTIES}; + +#ifdef COMPILE_DL_MAXMINDDB +ZEND_GET_MODULE(maxminddb) +#endif diff --git a/vendor/maxmind-db/reader/ext/php_maxminddb.h b/vendor/maxmind-db/reader/ext/php_maxminddb.h new file mode 100644 index 0000000..75c647b --- /dev/null +++ b/vendor/maxmind-db/reader/ext/php_maxminddb.h @@ -0,0 +1,24 @@ +/* MaxMind, Inc., licenses this file to you under the Apache License, Version + * 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +#include + +#ifndef PHP_MAXMINDDB_H +#define PHP_MAXMINDDB_H 1 +#define PHP_MAXMINDDB_VERSION "1.6.0" +#define PHP_MAXMINDDB_EXTNAME "maxminddb" + +extern zend_module_entry maxminddb_module_entry; +#define phpext_maxminddb_ptr &maxminddb_module_entry + +#endif diff --git a/vendor/maxmind-db/reader/ext/tests/001-load.phpt b/vendor/maxmind-db/reader/ext/tests/001-load.phpt new file mode 100644 index 0000000..09810ee --- /dev/null +++ b/vendor/maxmind-db/reader/ext/tests/001-load.phpt @@ -0,0 +1,12 @@ +--TEST-- +Check for maxminddb presence +--SKIPIF-- + +--FILE-- + +--EXPECT-- +maxminddb extension is available diff --git a/vendor/maxmind-db/reader/ext/tests/002-final.phpt b/vendor/maxmind-db/reader/ext/tests/002-final.phpt new file mode 100644 index 0000000..d91b7d0 --- /dev/null +++ b/vendor/maxmind-db/reader/ext/tests/002-final.phpt @@ -0,0 +1,13 @@ +--TEST-- +Check that Reader class is not final +--SKIPIF-- + +--FILE-- +isFinal()); +?> +--EXPECT-- +bool(false) diff --git a/vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt b/vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt new file mode 100644 index 0000000..26e9781 --- /dev/null +++ b/vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt @@ -0,0 +1,12 @@ +--TEST-- +openbase_dir is followed +--INI-- +open_basedir=/--dne-- +--FILE-- + +--EXPECTREGEX-- +.*open_basedir restriction in effect.* diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php new file mode 100644 index 0000000..3d5a829 --- /dev/null +++ b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php @@ -0,0 +1,327 @@ +fileHandle = @fopen($database, 'rb'); + if ($this->fileHandle === false) { + throw new InvalidArgumentException( + "Error opening \"$database\"." + ); + } + $this->fileSize = @filesize($database); + if ($this->fileSize === false) { + throw new UnexpectedValueException( + "Error determining the size of \"$database\"." + ); + } + + $start = $this->findMetadataStart($database); + $metadataDecoder = new Decoder($this->fileHandle, $start); + list($metadataArray) = $metadataDecoder->decode($start); + $this->metadata = new Metadata($metadataArray); + $this->decoder = new Decoder( + $this->fileHandle, + $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE + ); + $this->ipV4Start = $this->ipV4StartNode(); + } + + /** + * Retrieves the record for the IP address. + * + * @param string $ipAddress + * the IP address to look up + * + * @throws BadMethodCallException if this method is called on a closed database + * @throws InvalidArgumentException if something other than a single IP address is passed to the method + * @throws InvalidDatabaseException + * if the database is invalid or there is an error reading + * from it + * + * @return mixed the record for the IP address + */ + public function get($ipAddress) + { + if (\func_num_args() !== 1) { + throw new InvalidArgumentException( + 'Method takes exactly one argument.' + ); + } + list($record) = $this->getWithPrefixLen($ipAddress); + + return $record; + } + + /** + * Retrieves the record for the IP address and its associated network prefix length. + * + * @param string $ipAddress + * the IP address to look up + * + * @throws BadMethodCallException if this method is called on a closed database + * @throws InvalidArgumentException if something other than a single IP address is passed to the method + * @throws InvalidDatabaseException + * if the database is invalid or there is an error reading + * from it + * + * @return array an array where the first element is the record and the + * second the network prefix length for the record + */ + public function getWithPrefixLen($ipAddress) + { + if (\func_num_args() !== 1) { + throw new InvalidArgumentException( + 'Method takes exactly one argument.' + ); + } + + if (!\is_resource($this->fileHandle)) { + throw new BadMethodCallException( + 'Attempt to read from a closed MaxMind DB.' + ); + } + + if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) { + throw new InvalidArgumentException( + "The value \"$ipAddress\" is not a valid IP address." + ); + } + + list($pointer, $prefixLen) = $this->findAddressInTree($ipAddress); + if ($pointer === 0) { + return [null, $prefixLen]; + } + + return [$this->resolveDataPointer($pointer), $prefixLen]; + } + + private function findAddressInTree($ipAddress) + { + $rawAddress = unpack('C*', inet_pton($ipAddress)); + + $bitCount = \count($rawAddress) * 8; + + // The first node of the tree is always node 0, at the beginning of the + // value + $node = 0; + + $metadata = $this->metadata; + + // Check if we are looking up an IPv4 address in an IPv6 tree. If this + // is the case, we can skip over the first 96 nodes. + if ($metadata->ipVersion === 6) { + if ($bitCount === 32) { + $node = $this->ipV4Start; + } + } elseif ($metadata->ipVersion === 4 && $bitCount === 128) { + throw new InvalidArgumentException( + "Error looking up $ipAddress. You attempted to look up an" + . ' IPv6 address in an IPv4-only database.' + ); + } + + $nodeCount = $metadata->nodeCount; + + for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) { + $tempBit = 0xFF & $rawAddress[($i >> 3) + 1]; + $bit = 1 & ($tempBit >> 7 - ($i % 8)); + + $node = $this->readNode($node, $bit); + } + if ($node === $nodeCount) { + // Record is empty + return [0, $i]; + } elseif ($node > $nodeCount) { + // Record is a data pointer + return [$node, $i]; + } + throw new InvalidDatabaseException('Something bad happened'); + } + + private function ipV4StartNode() + { + // If we have an IPv4 database, the start node is the first node + if ($this->metadata->ipVersion === 4) { + return 0; + } + + $node = 0; + + for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) { + $node = $this->readNode($node, 0); + } + + return $node; + } + + private function readNode($nodeNumber, $index) + { + $baseOffset = $nodeNumber * $this->metadata->nodeByteSize; + + switch ($this->metadata->recordSize) { + case 24: + $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3); + list(, $node) = unpack('N', "\x00" . $bytes); + + return $node; + case 28: + $bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4); + if ($index === 0) { + $middle = (0xF0 & \ord($bytes[3])) >> 4; + } else { + $middle = 0x0F & \ord($bytes[0]); + } + list(, $node) = unpack('N', \chr($middle) . substr($bytes, $index, 3)); + + return $node; + case 32: + $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4); + list(, $node) = unpack('N', $bytes); + + return $node; + default: + throw new InvalidDatabaseException( + 'Unknown record size: ' + . $this->metadata->recordSize + ); + } + } + + private function resolveDataPointer($pointer) + { + $resolved = $pointer - $this->metadata->nodeCount + + $this->metadata->searchTreeSize; + if ($resolved >= $this->fileSize) { + throw new InvalidDatabaseException( + "The MaxMind DB file's search tree is corrupt" + ); + } + + list($data) = $this->decoder->decode($resolved); + + return $data; + } + + /* + * This is an extremely naive but reasonably readable implementation. There + * are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever + * an issue, but I suspect it won't be. + */ + private function findMetadataStart($filename) + { + $handle = $this->fileHandle; + $fstat = fstat($handle); + $fileSize = $fstat['size']; + $marker = self::$METADATA_START_MARKER; + $markerLength = self::$METADATA_START_MARKER_LENGTH; + + $minStart = $fileSize - min(self::$METADATA_MAX_SIZE, $fileSize); + + for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) { + if (fseek($handle, $offset) !== 0) { + break; + } + + $value = fread($handle, $markerLength); + if ($value === $marker) { + return $offset + $markerLength; + } + } + throw new InvalidDatabaseException( + "Error opening database file ($filename). " . + 'Is this a valid MaxMind DB file?' + ); + } + + /** + * @throws InvalidArgumentException if arguments are passed to the method + * @throws BadMethodCallException if the database has been closed + * + * @return Metadata object for the database + */ + public function metadata() + { + if (\func_num_args()) { + throw new InvalidArgumentException( + 'Method takes no arguments.' + ); + } + + // Not technically required, but this makes it consistent with + // C extension and it allows us to change our implementation later. + if (!\is_resource($this->fileHandle)) { + throw new BadMethodCallException( + 'Attempt to read from a closed MaxMind DB.' + ); + } + + return $this->metadata; + } + + /** + * Closes the MaxMind DB and returns resources to the system. + * + * @throws Exception + * if an I/O error occurs + */ + public function close() + { + if (!\is_resource($this->fileHandle)) { + throw new BadMethodCallException( + 'Attempt to close a closed MaxMind DB.' + ); + } + fclose($this->fileHandle); + } +} diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php new file mode 100644 index 0000000..132dae8 --- /dev/null +++ b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php @@ -0,0 +1,356 @@ +fileStream = $fileStream; + $this->pointerBase = $pointerBase; + + $this->pointerBaseByteSize = $pointerBase > 0 ? log($pointerBase, 2) / 8 : 0; + $this->pointerTestHack = $pointerTestHack; + + $this->switchByteOrder = $this->isPlatformLittleEndian(); + } + + public function decode($offset) + { + $ctrlByte = \ord(Util::read($this->fileStream, $offset, 1)); + ++$offset; + + $type = $ctrlByte >> 5; + + // Pointers are a special case, we don't read the next $size bytes, we + // use the size to determine the length of the pointer and then follow + // it. + if ($type === self::_POINTER) { + list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset); + + // for unit testing + if ($this->pointerTestHack) { + return [$pointer]; + } + + list($result) = $this->decode($pointer); + + return [$result, $offset]; + } + + if ($type === self::_EXTENDED) { + $nextByte = \ord(Util::read($this->fileStream, $offset, 1)); + + $type = $nextByte + 7; + + if ($type < 8) { + throw new InvalidDatabaseException( + 'Something went horribly wrong in the decoder. An extended type ' + . 'resolved to a type number < 8 (' + . $type + . ')' + ); + } + + ++$offset; + } + + list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset); + + return $this->decodeByType($type, $offset, $size); + } + + private function decodeByType($type, $offset, $size) + { + switch ($type) { + case self::_MAP: + return $this->decodeMap($size, $offset); + case self::_ARRAY: + return $this->decodeArray($size, $offset); + case self::_BOOLEAN: + return [$this->decodeBoolean($size), $offset]; + } + + $newOffset = $offset + $size; + $bytes = Util::read($this->fileStream, $offset, $size); + switch ($type) { + case self::_BYTES: + case self::_UTF8_STRING: + return [$bytes, $newOffset]; + case self::_DOUBLE: + $this->verifySize(8, $size); + + return [$this->decodeDouble($bytes), $newOffset]; + case self::_FLOAT: + $this->verifySize(4, $size); + + return [$this->decodeFloat($bytes), $newOffset]; + case self::_INT32: + return [$this->decodeInt32($bytes, $size), $newOffset]; + case self::_UINT16: + case self::_UINT32: + case self::_UINT64: + case self::_UINT128: + return [$this->decodeUint($bytes, $size), $newOffset]; + default: + throw new InvalidDatabaseException( + 'Unknown or unexpected type: ' . $type + ); + } + } + + private function verifySize($expected, $actual) + { + if ($expected !== $actual) { + throw new InvalidDatabaseException( + "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)" + ); + } + } + + private function decodeArray($size, $offset) + { + $array = []; + + for ($i = 0; $i < $size; ++$i) { + list($value, $offset) = $this->decode($offset); + array_push($array, $value); + } + + return [$array, $offset]; + } + + private function decodeBoolean($size) + { + return $size === 0 ? false : true; + } + + private function decodeDouble($bits) + { + // This assumes IEEE 754 doubles, but most (all?) modern platforms + // use them. + // + // We are not using the "E" format as that was only added in + // 7.0.15 and 7.1.1. As such, we must switch byte order on + // little endian machines. + list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits)); + + return $double; + } + + private function decodeFloat($bits) + { + // This assumes IEEE 754 floats, but most (all?) modern platforms + // use them. + // + // We are not using the "G" format as that was only added in + // 7.0.15 and 7.1.1. As such, we must switch byte order on + // little endian machines. + list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits)); + + return $float; + } + + private function decodeInt32($bytes, $size) + { + switch ($size) { + case 0: + return 0; + case 1: + case 2: + case 3: + $bytes = str_pad($bytes, 4, "\x00", STR_PAD_LEFT); + break; + case 4: + break; + default: + throw new InvalidDatabaseException( + "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)" + ); + } + + list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes)); + + return $int; + } + + private function decodeMap($size, $offset) + { + $map = []; + + for ($i = 0; $i < $size; ++$i) { + list($key, $offset) = $this->decode($offset); + list($value, $offset) = $this->decode($offset); + $map[$key] = $value; + } + + return [$map, $offset]; + } + + private function decodePointer($ctrlByte, $offset) + { + $pointerSize = (($ctrlByte >> 3) & 0x3) + 1; + + $buffer = Util::read($this->fileStream, $offset, $pointerSize); + $offset = $offset + $pointerSize; + + switch ($pointerSize) { + case 1: + $packed = \chr($ctrlByte & 0x7) . $buffer; + list(, $pointer) = unpack('n', $packed); + $pointer += $this->pointerBase; + break; + case 2: + $packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer; + list(, $pointer) = unpack('N', $packed); + $pointer += $this->pointerBase + 2048; + break; + case 3: + $packed = \chr($ctrlByte & 0x7) . $buffer; + + // It is safe to use 'N' here, even on 32 bit machines as the + // first bit is 0. + list(, $pointer) = unpack('N', $packed); + $pointer += $this->pointerBase + 526336; + break; + case 4: + // We cannot use unpack here as we might overflow on 32 bit + // machines + $pointerOffset = $this->decodeUint($buffer, $pointerSize); + + $byteLength = $pointerSize + $this->pointerBaseByteSize; + + if ($byteLength <= _MM_MAX_INT_BYTES) { + $pointer = $pointerOffset + $this->pointerBase; + } elseif (\extension_loaded('gmp')) { + $pointer = gmp_strval(gmp_add($pointerOffset, $this->pointerBase)); + } elseif (\extension_loaded('bcmath')) { + $pointer = bcadd($pointerOffset, $this->pointerBase); + } else { + throw new RuntimeException( + 'The gmp or bcmath extension must be installed to read this database.' + ); + } + } + + return [$pointer, $offset]; + } + + private function decodeUint($bytes, $byteLength) + { + if ($byteLength === 0) { + return 0; + } + + $integer = 0; + + for ($i = 0; $i < $byteLength; ++$i) { + $part = \ord($bytes[$i]); + + // We only use gmp or bcmath if the final value is too big + if ($byteLength <= _MM_MAX_INT_BYTES) { + $integer = ($integer << 8) + $part; + } elseif (\extension_loaded('gmp')) { + $integer = gmp_strval(gmp_add(gmp_mul($integer, 256), $part)); + } elseif (\extension_loaded('bcmath')) { + $integer = bcadd(bcmul($integer, 256), $part); + } else { + throw new RuntimeException( + 'The gmp or bcmath extension must be installed to read this database.' + ); + } + } + + return $integer; + } + + private function sizeFromCtrlByte($ctrlByte, $offset) + { + $size = $ctrlByte & 0x1f; + + if ($size < 29) { + return [$size, $offset]; + } + + $bytesToRead = $size - 28; + $bytes = Util::read($this->fileStream, $offset, $bytesToRead); + + if ($size === 29) { + $size = 29 + \ord($bytes); + } elseif ($size === 30) { + list(, $adjust) = unpack('n', $bytes); + $size = 285 + $adjust; + } elseif ($size > 30) { + list(, $adjust) = unpack('N', "\x00" . $bytes); + $size = $adjust + 65821; + } + + return [$size, $offset + $bytesToRead]; + } + + private function maybeSwitchByteOrder($bytes) + { + return $this->switchByteOrder ? strrev($bytes) : $bytes; + } + + private function isPlatformLittleEndian() + { + $testint = 0x00FF; + $packed = pack('S', $testint); + + return $testint === current(unpack('v', $packed)); + } +} diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php new file mode 100644 index 0000000..478a22c --- /dev/null +++ b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php @@ -0,0 +1,12 @@ +binaryFormatMajorVersion = + $metadata['binary_format_major_version']; + $this->binaryFormatMinorVersion = + $metadata['binary_format_minor_version']; + $this->buildEpoch = $metadata['build_epoch']; + $this->databaseType = $metadata['database_type']; + $this->languages = $metadata['languages']; + $this->description = $metadata['description']; + $this->ipVersion = $metadata['ip_version']; + $this->nodeCount = $metadata['node_count']; + $this->recordSize = $metadata['record_size']; + $this->nodeByteSize = $this->recordSize / 4; + $this->searchTreeSize = $this->nodeCount * $this->nodeByteSize; + } + + public function __get($var) + { + return $this->$var; + } +} diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php new file mode 100644 index 0000000..87ebbf1 --- /dev/null +++ b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php @@ -0,0 +1,26 @@ + + * @author Roman Ožana + * @author Sander Kruger + * @author Zoli Szabó + */ +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\\-]+)+)/'; + + /** + * Regular expression component matching a static pseudo class in a selector, without the preceding ":", + * for which the applicable elements can be determined (by converting the selector to an XPath expression). + * (Contains alternation without a group and is intended to be placed within a capturing, non-capturing or lookahead + * group, as appropriate for the usage context.) + * + * @var string + */ + const PSEUDO_CLASS_MATCHER = '(?:first|last|nth)-child|nth-of-type|not\\([[:ascii:]]*\\)'; + + /** + * @var string + */ + const CONTENT_TYPE_META_TAG = ''; + + /** + * @var string + */ + const DEFAULT_DOCUMENT_TYPE = ''; + + /** + * @var string Regular expression part to match tag names that PHP's DOMDocument implementation is not aware are + * self-closing. These are mostly HTML5 elements, but for completeness (obsolete) and + * (deprecated) are also included. + * + * @see https://bugs.php.net/bug.php?id=73175 + */ + const PHP_UNRECOGNIZED_VOID_TAGNAME_MATCHER = '(?:command|embed|keygen|source|track|wbr)'; + + /** + * @var \DOMDocument + */ + protected $domDocument = null; + + /** + * @var \DOMXPath + */ + protected $xPath = null; + + /** + * @var string + */ + private $css = ''; + + /** + * @var bool[] + */ + private $excludedSelectors = []; + + /** + * @var string[] + */ + private $unprocessableHtmlTags = ['wbr']; + + /** + * @var bool[] + */ + private $allowedMediaTypes = ['all' => true, 'screen' => true, 'print' => true]; + + /** + * @var mixed[] + */ + private $caches = [ + self::CACHE_KEY_CSS => [], + self::CACHE_KEY_SELECTOR => [], + self::CACHE_KEY_XPATH => [], + self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => [], + self::CACHE_KEY_COMBINED_STYLES => [], + ]; + + /** + * the visited nodes with the XPath paths as array keys + * + * @var \DOMElement[] + */ + private $visitedNodes = []; + + /** + * 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 = []; + + /** + * 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